///|
fn markdown_editor_normalize_ordered_transform(
  edit : RichTextInputTransform,
) -> RichTextInputTransform {
  let normalized = markdown_editor_renumber_ordered_lists(edit.text)
  if normalized.text == edit.text {
    edit
  } else {
    RichTextInputTransform::new(
      text=normalized.text,
      caret=markdown_editor_map_offset_after_replacements(
        edit.caret,
        normalized.replacements,
      ),
      selection=match edit.selection {
        Some(selection) =>
          Some(
            @core.TextRange::new(
              start=markdown_editor_map_offset_after_replacements(
                selection.start,
                normalized.replacements,
              ),
              end=markdown_editor_map_offset_after_replacements(
                selection.end,
                normalized.replacements,
              ),
            ),
          )
        None => None
      },
    )
  }
}

///|
fn markdown_editor_normalize_ordered_edit_result(
  edit : MarkdownEditorEditResult,
) -> MarkdownEditorEditResult {
  let normalized = markdown_editor_renumber_ordered_lists(edit.source)
  if normalized.text == edit.source {
    edit
  } else {
    {
      source: normalized.text,
      caret: markdown_editor_map_offset_after_replacements(
        edit.caret,
        normalized.replacements,
      ),
      selection: match edit.selection {
        Some(selection) =>
          Some(
            @core.TextRange::new(
              start=markdown_editor_map_offset_after_replacements(
                selection.start,
                normalized.replacements,
              ),
              end=markdown_editor_map_offset_after_replacements(
                selection.end,
                normalized.replacements,
              ),
            ),
          )
        None => None
      },
    }
  }
}

///|
pub(all) struct MarkdownEditorOffsetReplacement {
  start : Int
  old_end : Int
  new_end : Int
} derive(Eq, Debug)

///|
pub(all) struct MarkdownEditorRenumberResult {
  text : String
  replacements : Array[MarkdownEditorOffsetReplacement]
} derive(Eq, Debug)

///|
fn markdown_editor_renumber_ordered_lists(
  source : String,
) -> MarkdownEditorRenumberResult {
  let lines = markdown_editor_lines(source)
  let rendered : Array[String] = []
  let replacements : Array[MarkdownEditorOffsetReplacement] = []
  let contexts : Array[MarkdownEditorOrderedRenumberContext] = []
  for line in lines {
    match markdown_editor_ordered_line_info(line.text) {
      Some(info) => {
        markdown_editor_trim_ordered_renumber_contexts(contexts, info.indent)
        let next_number = markdown_editor_next_ordered_number(contexts, info)
        let next_digits = next_number.to_string()
        if next_digits == info.digits {
          rendered.push(line.text)
        } else {
          let chars = line.text.to_array()
          rendered.push(
            String::from_array(chars[:info.digit_start]) +
            next_digits +
            String::from_array(chars[info.digit_end:]),
          )
          replacements.push({
            start: line.start + info.digit_start,
            old_end: line.start + info.digit_end,
            new_end: line.start +
            info.digit_start +
            markdown_editor_text_length(next_digits),
          })
        }
      }
      None => {
        markdown_editor_reset_ordered_renumber_contexts(contexts, line.text)
        rendered.push(line.text)
      }
    }
  }
  { text: markdown_join_lines(rendered), replacements }
}

///|
pub(all) struct MarkdownEditorOrderedRenumberContext {
  indent : Int
  context_prefix : String
  expected : Int
} derive(Eq, Debug)

///|
fn markdown_editor_trim_ordered_renumber_contexts(
  contexts : Array[MarkdownEditorOrderedRenumberContext],
  indent : Int,
) -> Unit {
  while contexts.length() > 0 && contexts[contexts.length() - 1].indent > indent {
    ignore(contexts.pop())
  }
}

///|
fn markdown_editor_next_ordered_number(
  contexts : Array[MarkdownEditorOrderedRenumberContext],
  info : MarkdownEditorOrderedLineInfo,
) -> Int {
  if contexts.length() > 0 &&
    contexts[contexts.length() - 1].indent == info.indent &&
    contexts[contexts.length() - 1].context_prefix == info.context_prefix {
    let context = contexts[contexts.length() - 1]
    ignore(contexts.pop())
    contexts.push({
      indent: info.indent,
      context_prefix: info.context_prefix,
      expected: context.expected + 1,
    })
    context.expected
  } else {
    while contexts.length() > 0 &&
          contexts[contexts.length() - 1].indent >= info.indent {
      ignore(contexts.pop())
    }
    contexts.push({
      indent: info.indent,
      context_prefix: info.context_prefix,
      expected: info.number + 1,
    })
    info.number
  }
}

///|
fn markdown_editor_reset_ordered_renumber_contexts(
  contexts : Array[MarkdownEditorOrderedRenumberContext],
  line : String,
) -> Unit {
  if contexts.length() == 0 || markdown_editor_ordered_renumber_blank_line(line) {
    return
  }
  let indent = markdown_editor_ordered_line_context_indent(line)
  if indent > contexts[contexts.length() - 1].indent {
    return
  }
  contexts.clear()
}

///|
fn markdown_editor_ordered_renumber_blank_line(line : String) -> Bool {
  let chars = line.to_array()
  let indent = markdown_editor_ordered_line_context_indent(line)
  String::from_array(chars[indent:]).trim().to_owned() == ""
}

///|
pub(all) struct MarkdownEditorOrderedLineInfo {
  indent : Int
  context_prefix : String
  digit_start : Int
  digit_end : Int
  digits : String
  number : Int
} derive(Eq, Debug)

///|
fn markdown_editor_ordered_line_info(
  line : String,
) -> MarkdownEditorOrderedLineInfo? {
  let chars = line.to_array()
  let indent = markdown_editor_ordered_line_context_indent(line)
  let digit_end = markdown_editor_ordered_digit_end_at(chars, indent)
  if digit_end <= indent ||
    digit_end + 1 >= chars.length() ||
    chars[digit_end] != '.' ||
    chars[digit_end + 1] != ' ' {
    None
  } else {
    Some({
      indent,
      context_prefix: String::from_array(chars[:indent]),
      digit_start: indent,
      digit_end,
      digits: String::from_array(chars[indent:digit_end]),
      number: markdown_editor_parse_digits(chars, indent, digit_end),
    })
  }
}

///|
fn markdown_editor_ordered_line_context_indent(line : String) -> Int {
  let chars = line.to_array()
  let mut index = 0
  let mut scanning = true
  while scanning {
    while index < chars.length() &&
          (chars[index] == ' ' || chars[index] == '\t') {
      index = index + 1
    }
    if index + 1 < chars.length() &&
      chars[index] == '>' &&
      chars[index + 1] == ' ' {
      index = index + 2
    } else {
      scanning = false
    }
  }
  index
}

///|
fn markdown_editor_ordered_digit_end_at(
  chars : Array[Char],
  start : Int,
) -> Int {
  let mut index = markdown_editor_clamp_int(start, 0, chars.length())
  while index < chars.length() && chars[index].is_ascii_digit() {
    index = index + 1
  }
  index
}

///|
fn markdown_editor_map_offset_after_replacements(
  offset : Int,
  replacements : Array[MarkdownEditorOffsetReplacement],
) -> Int {
  let mut mapped = offset
  for replacement in replacements {
    let delta = replacement.new_end - replacement.old_end
    if offset > replacement.old_end {
      mapped = mapped + delta
    } else if offset >= replacement.start {
      mapped = replacement.new_end
    }
  }
  mapped
}

///|
fn markdown_editor_parse_digits(
  chars : Array[Char],
  start : Int,
  end : Int,
) -> Int {
  let mut value = 0
  for index in start..