///|
fn markdown_editor_complete_rule_on_space(
  source : String,
  caret : Int,
  selection : @core.TextRange?,
) -> RichTextInputTransform? {
  if markdown_editor_active_selection(selection) is Some(_) {
    return None
  }
  let chars = source.to_array()
  let caret = markdown_editor_clamp_int(caret, 0, chars.length())
  let current : MarkdownEditorSelection = { caret, selection }
  let range = markdown_editor_current_line_range(source, current)
  let line = String::from_array(chars[range.start:range.end])
  markdown_editor_complete_rule_on_enter(chars, range, line, caret)
}

///|
fn markdown_editor_complete_setext_heading_on_space(
  source : String,
  caret : Int,
  selection : @core.TextRange?,
) -> RichTextInputTransform? {
  if markdown_editor_active_selection(selection) is Some(_) {
    return None
  }
  let chars = source.to_array()
  let caret = markdown_editor_clamp_int(caret, 0, chars.length())
  let current : MarkdownEditorSelection = { caret, selection }
  let range = markdown_editor_current_line_range(source, current)
  let line = String::from_array(chars[range.start:range.end])
  markdown_editor_complete_setext_heading_on_enter(chars, range, line, caret)
}

///|
pub(all) struct MarkdownEditorPastedFootnoteDefinition {
  label : String
  text : String
}

///|
pub(all) struct MarkdownEditorFootnoteDefinitionEdit {
  source : String
  anchor_delta : Int
}

///|
pub fn markdown_editor_insert_hard_break(
  source : String,
  caret : Int,
  selection : @core.TextRange?,
) -> RichTextInputTransform? {
  let chars = source.to_array()
  let caret = markdown_editor_clamp_int(caret, 0, chars.length())
  if markdown_editor_table_cell_at_text_offset(source, caret, selection) {
    return None
  }
  if markdown_editor_active_selection(selection) is None &&
    markdown_editor_any_code_block_at_content_offset(source, caret, selection) {
    return markdown_editor_transform_input(source, caret, selection, "\n")
  }
  let replace_range = match markdown_editor_active_selection(selection) {
    Some(range) => range.normalized()
    None => @core.TextRange::new(start=caret, end=caret)
  }
  let current : MarkdownEditorSelection = {
    caret: replace_range.start,
    selection: None,
  }
  let line_range = markdown_editor_current_line_range(source, current)
  let line = String::from_array(chars[line_range.start:line_range.end])
  let insert = "  \n" + markdown_editor_hard_break_continuation_prefix(line)
  Some(
    markdown_editor_transform_result(
      text=String::from_array(chars[:replace_range.start]) +
        insert +
        String::from_array(chars[replace_range.end:]),
      caret=replace_range.start + markdown_editor_text_length(insert),
    ),
  )
}

///|
fn markdown_editor_complete_rule_on_enter(
  chars : Array[Char],
  range : MarkdownSourceRange,
  line : String,
  caret : Int,
) -> RichTextInputTransform? {
  if caret != range.end || !markdown_editor_is_typed_rule_line(line) {
    return None
  }
  let text = String::from_array(chars[:caret]) +
    "\n" +
    String::from_array(chars[caret:])
  Some(markdown_editor_transform_result(text~, caret=caret + 1))
}

///|
fn markdown_editor_complete_setext_heading_on_enter(
  chars : Array[Char],
  range : MarkdownSourceRange,
  line : String,
  caret : Int,
) -> RichTextInputTransform? {
  if caret != range.end || !markdown_editor_is_typed_setext_underline_line(line) {
    return None
  }
  if range.start == 0 || chars[range.start - 1] != '\n' {
    return None
  }
  let previous_end = range.start - 1
  let previous_start = markdown_editor_previous_line_start(chars, previous_end)
  let previous_line = String::from_array(chars[previous_start:previous_end])
  if previous_line.trim().to_owned() == "" ||
    !markdown_editor_is_setext_paragraph_candidate(previous_line) {
    return None
  }
  let text = String::from_array(chars[:caret]) +
    "\n" +
    String::from_array(chars[caret:])
  Some(markdown_editor_transform_result(text~, caret=caret + 1))
}

///|
fn markdown_editor_is_typed_setext_underline_line(line : String) -> Bool {
  let chars = line.to_array()
  let indent = markdown_editor_leading_indent_length(chars)
  if chars.length() < indent + 3 {
    return false
  }
  let marker = chars[indent]
  if marker != '=' && marker != '-' {
    return false
  }
  for index in indent.. Bool {
  let chars = line.to_array()
  let indent = markdown_editor_leading_indent_length(chars)
  if chars.length() != indent + 3 {
    return false
  }
  (chars[indent] == '-' && chars[indent + 1] == '-' && chars[indent + 2] == '-') ||
  (chars[indent] == '*' && chars[indent + 1] == '*' && chars[indent + 2] == '*') ||
  (chars[indent] == '_' && chars[indent + 1] == '_' && chars[indent + 2] == '_')
}

///|
fn markdown_editor_transform_result(
  text~ : String,
  caret~ : Int,
  selection? : @core.TextRange? = None,
) -> RichTextInputTransform {
  markdown_editor_normalize_ordered_transform(
    RichTextInputTransform::new(text~, caret~, selection~),
  )
}

///|
fn markdown_editor_autocomplete_block_marker(
  source : String,
  caret : Int,
  selection : @core.TextRange?,
) -> RichTextInputTransform? {
  if markdown_editor_active_selection(selection) is Some(_) {
    return None
  }
  let chars = source.to_array()
  let caret = markdown_editor_clamp_int(caret, 0, chars.length())
  let current : MarkdownEditorSelection = { caret, selection }
  let range = markdown_editor_current_line_range(source, current)
  let before = String::from_array(chars[range.start:caret])
  let marker = markdown_editor_typed_block_marker(before)
  if marker == "" {
    return None
  }
  let text = String::from_array(chars[:range.start]) +
    marker +
    String::from_array(chars[caret:])
  Some(
    markdown_editor_transform_result(
      text~,
      caret=range.start + markdown_editor_text_length(marker),
    ),
  )
}

///|
fn markdown_editor_typed_block_marker(before : String) -> String {
  let chars = before.to_array()
  let indent = markdown_editor_leading_indent_length(chars)
  let prefix = String::from_array(chars[:indent])
  let token = String::from_array(chars[indent:])
  if markdown_editor_typed_heading_marker(token) is Some(level) {
    prefix + markdown_editor_heading_marker(level) + " "
  } else if token == "-" || token == "*" || token == "+" {
    prefix + "- "
  } else if markdown_editor_typed_task_marker(token, checked=false) {
    prefix + "- [ ] "
  } else if markdown_editor_typed_task_marker(token, checked=true) {
    prefix + "- [x] "
  } else if markdown_editor_typed_quote_marker(token) is Some(depth) {
    prefix + markdown_editor_quote_marker(depth)
  } else if markdown_editor_typed_ordered_marker(token) is Some(number) {
    prefix + number.to_string() + ". "
  } else if markdown_editor_typed_quoted_block_marker(token) is Some(marker) {
    prefix + marker
  } else {
    ""
  }
}

///|
fn markdown_editor_typed_quoted_block_marker(token : String) -> String? {
  let quote = "> "
  if !markdown_editor_source_has_marker(token.to_array(), 0, quote) {
    return None
  }
  let nested = markdown_editor_substring(
    token,
    markdown_editor_text_length(quote),
    markdown_editor_text_length(token),
  )
  let marker = markdown_editor_typed_block_marker(nested)
  if marker == "" {
    None
  } else {
    Some(quote + marker)
  }
}

///|
fn markdown_editor_typed_quote_marker(token : String) -> Int? {
  let chars = token.to_array()
  if chars.length() < 1 {
    return None
  }
  for ch in chars {
    if ch != '>' {
      return None
    }
  }
  Some(chars.length())
}

///|
fn markdown_editor_quote_marker(depth : Int) -> String {
  let parts : Array[String] = []
  for _ in 0..")
  }
  parts.join(" ") + " "
}

///|
fn markdown_editor_typed_task_marker(token : String, checked~ : Bool) -> Bool {
  let checkbox = if checked { ["[x]", "[X]"] } else { ["[]", "[ ]"] }
  for item in checkbox {
    if token == item {
      return true
    }
    if token == "- " + item || token == "* " + item || token == "+ " + item {
      return true
    }
  }
  false
}

///|
fn markdown_editor_typed_heading_marker(token : String) -> Int? {
  let chars = token.to_array()
  if chars.length() < 1 || chars.length() > 6 {
    return None
  }
  for ch in chars {
    if ch != '#' {
      return None
    }
  }
  Some(chars.length())
}

///|
fn markdown_editor_typed_ordered_marker(token : String) -> Int? {
  let chars = token.to_array()
  if chars.length() < 2 ||
    (chars[chars.length() - 1] != '.' && chars[chars.length() - 1] != ')') {
    return None
  }
  for index in 0..<(chars.length() - 1) {
    if !chars[index].is_ascii_digit() {
      return None
    }
  }
  Some(markdown_editor_parse_digits(chars, 0, chars.length() - 1))
}

///|
pub fn markdown_editor_indent_lines(
  source : String,
  caret : Int,
  selection : @core.TextRange?,
) -> RichTextInputTransform {
  markdown_editor_shift_lines(source, caret, selection, indent=true).unwrap()
}

///|
pub fn markdown_editor_outdent_lines(
  source : String,
  caret : Int,
  selection : @core.TextRange?,
) -> RichTextInputTransform? {
  markdown_editor_shift_lines(source, caret, selection, indent=false)
}

///|
fn markdown_editor_shift_lines(
  source : String,
  caret : Int,
  selection : @core.TextRange?,
  indent~ : Bool,
) -> RichTextInputTransform? {
  let current : MarkdownEditorSelection = { caret, selection }
  let range = markdown_editor_selected_line_range(source, current)
  let chars = source.to_array()
  let segment = String::from_array(chars[range.start:range.end])
  let shifted = if indent {
    markdown_editor_indent_segment(segment)
  } else {
    markdown_editor_outdent_segment(segment)
  }
  if shifted == segment {
    return None
  }
  let delta = markdown_editor_text_length(shifted) - (range.end - range.start)
  let text = String::from_array(chars[:range.start]) +
    shifted +
    String::from_array(chars[range.end:])
  let next_selection = match markdown_editor_active_selection(selection) {
    Some(_) =>
      Some(
        @core.TextRange::new(
          start=range.start,
          end=range.start + markdown_editor_text_length(shifted),
        ),
      )
    None => None
  }
  let next_caret = if caret <= range.start {
    caret
  } else if caret <= range.end {
    caret + delta
  } else {
    caret + delta
  }
  Some(
    markdown_editor_transform_result(
      text~,
      caret=next_caret,
      selection=next_selection,
    ),
  )
}

///|
fn markdown_editor_indent_segment(segment : String) -> String {
  let lines = markdown_editor_lines(segment)
  let shifted : Array[String] = []
  for line in lines {
    shifted.push("  " + line.text)
  }
  markdown_join_lines(shifted)
}

///|
fn markdown_editor_outdent_segment(segment : String) -> String {
  let lines = markdown_editor_lines(segment)
  let shifted : Array[String] = []
  for line in lines {
    shifted.push(markdown_editor_outdent_line(line.text))
  }
  markdown_join_lines(shifted)
}

///|
fn markdown_editor_outdent_line(line : String) -> String {
  let chars = line.to_array()
  if chars.length() >= 2 && chars[0] == ' ' && chars[1] == ' ' {
    String::from_array(chars[2:])
  } else if chars.length() >= 1 && chars[0] == ' ' {
    String::from_array(chars[1:])
  } else if chars.length() >= 1 && chars[0] == '\t' {
    String::from_array(chars[1:])
  } else {
    line
  }
}

///|
fn markdown_editor_exit_empty_continuation(
  chars : Array[Char],
  range : MarkdownSourceRange,
  line : String,
) -> RichTextInputTransform? {
  let marker = markdown_editor_continuation_marker(line)
  if marker == "" {
    return None
  }
  if markdown_editor_text_length(line) != markdown_editor_text_length(marker) {
    return None
  }
  match markdown_editor_quoted_continuation_parent(line) {
    Some(parent) =>
      return Some(
        markdown_editor_transform_result(
          text=String::from_array(chars[:range.start]) +
            parent +
            String::from_array(chars[range.end:]),
          caret=range.start + markdown_editor_text_length(parent),
        ),
      )
    None => ()
  }
  if markdown_editor_line_indent_prefix_length(line) > 0 {
    let outdented = markdown_editor_outdent_line(line)
    return Some(
      markdown_editor_transform_result(
        text=String::from_array(chars[:range.start]) +
          outdented +
          String::from_array(chars[range.end:]),
        caret=range.start + markdown_editor_marker_prefix_length(outdented),
      ),
    )
  }
  Some(
    markdown_editor_transform_result(
      text=String::from_array(chars[:range.start]) +
        String::from_array(chars[range.end:]),
      caret=range.start,
    ),
  )
}

///|
fn markdown_editor_backspace_empty_continuation(
  source : String,
  caret : Int,
  selection : @core.TextRange?,
) -> RichTextInputTransform? {
  if markdown_editor_active_selection(selection) is Some(_) {
    return None
  }
  let chars = source.to_array()
  let caret = markdown_editor_clamp_int(caret, 0, chars.length())
  let range = markdown_editor_current_line_range(
    source,
    ({ caret, selection } : MarkdownEditorSelection),
  )
  if caret != range.end {
    return None
  }
  let line = String::from_array(chars[range.start:range.end])
  markdown_editor_exit_empty_continuation(chars, range, line)
}

///|
fn markdown_editor_delete_empty_continuation(
  source : String,
  caret : Int,
  selection : @core.TextRange?,
) -> RichTextInputTransform? {
  if markdown_editor_active_selection(selection) is Some(_) {
    return None
  }
  let chars = source.to_array()
  let caret = markdown_editor_clamp_int(caret, 0, chars.length())
  let range = markdown_editor_current_line_range(
    source,
    ({ caret, selection } : MarkdownEditorSelection),
  )
  if caret != range.start {
    return None
  }
  let line = String::from_array(chars[range.start:range.end])
  markdown_editor_exit_empty_continuation(chars, range, line)
}

///|
fn markdown_editor_exit_empty_heading(
  chars : Array[Char],
  range : MarkdownSourceRange,
  line : String,
  caret : Int,
) -> RichTextInputTransform? {
  if caret != range.end {
    return None
  }
  match markdown_editor_empty_heading_prefix(line) {
    Some(prefix) =>
      Some(
        markdown_editor_transform_result(
          text=String::from_array(chars[:range.start]) +
            prefix +
            String::from_array(chars[range.end:]),
          caret=range.start + markdown_editor_text_length(prefix),
        ),
      )
    None => None
  }
}

///|
fn markdown_editor_empty_heading_prefix(line : String) -> String? {
  let chars = line.to_array()
  let indent = markdown_editor_leading_indent_length(chars)
  if chars.length() >= indent + 2 &&
    chars[indent] == '>' &&
    chars[indent + 1] == ' ' {
    let nested = String::from_array(chars[indent + 2:])
    match markdown_editor_empty_heading_prefix(nested) {
      Some(prefix) =>
        return Some(String::from_array(chars[:indent + 2]) + prefix)
      None => ()
    }
  }
  let mut index = indent
  while index < chars.length() && chars[index] == '#' {
    index = index + 1
  }
  let level = index - indent
  if level < 1 || level > 6 || index >= chars.length() {
    return None
  }
  if chars[index] != ' ' && chars[index] != '\t' {
    return None
  }
  while index < chars.length() {
    if chars[index] != ' ' && chars[index] != '\t' {
      return None
    }
    index = index + 1
  }
  Some(String::from_array(chars[:indent]))
}

///|
fn markdown_editor_quoted_continuation_parent(line : String) -> String? {
  let chars = line.to_array()
  let indent = markdown_editor_leading_indent_length(chars)
  if chars.length() < indent + 3 ||
    chars[indent] != '>' ||
    chars[indent + 1] != ' ' {
    return None
  }
  let nested = String::from_array(chars[indent + 2:])
  let nested_marker = markdown_editor_continuation_marker(nested)
  if nested_marker == "" ||
    markdown_editor_text_length(nested_marker) !=
    markdown_editor_text_length(nested) {
    None
  } else {
    let quote_prefix = String::from_array(chars[:indent + 2])
    match markdown_editor_quoted_continuation_parent(nested) {
      Some(parent) => Some(quote_prefix + parent)
      None => Some(quote_prefix)
    }
  }
}

///|
fn markdown_editor_continuation_marker(line : String) -> String {
  let chars = line.to_array()
  let indent = markdown_editor_leading_indent_length(chars)
  let indent_text = String::from_array(chars[:indent])
  if chars.length() >= indent + 2 &&
    chars[indent] == '>' &&
    chars[indent + 1] == ' ' {
    let nested = String::from_array(chars[indent + 2:])
    let nested_marker = markdown_editor_continuation_marker(nested)
    if nested_marker == "" {
      indent_text + "> "
    } else {
      indent_text + "> " + nested_marker
    }
  } else if chars.length() >= indent + 6 &&
    chars[indent] == '-' &&
    chars[indent + 1] == ' ' &&
    chars[indent + 2] == '[' &&
    (
      chars[indent + 3] == ' ' ||
      chars[indent + 3] == 'x' ||
      chars[indent + 3] == 'X'
    ) &&
    chars[indent + 4] == ']' &&
    chars[indent + 5] == ' ' {
    indent_text + "- [ ] "
  } else if chars.length() >= indent + 2 &&
    (chars[indent] == '-' || chars[indent] == '*' || chars[indent] == '+') &&
    chars[indent + 1] == ' ' {
    indent_text + String::from_array(chars[indent:indent + 2])
  } else {
    markdown_editor_ordered_continuation_marker(chars, indent)
  }
}

///|
fn markdown_editor_ordered_continuation_marker(
  chars : Array[Char],
  indent : Int,
) -> String {
  let mut index = indent
  while index < chars.length() && chars[index].is_ascii_digit() {
    index = index + 1
  }
  if index > 0 &&
    index + 1 < chars.length() &&
    chars[index] == '.' &&
    chars[index + 1] == ' ' {
    String::from_array(chars[:indent]) +
    (markdown_editor_parse_digits(chars, indent, index) + 1).to_string() +
    ". "
  } else {
    ""
  }
}

///|