///|
/// Pre-render phase ① (port of `base.preRenderRemove` at PriorityEarly):
/// drop all nodes whose tag type resolves to Remove, then merge text nodes
/// that became adjacent.
fn pre_render_remove(doc : @dom.Node) -> Unit {
  fn finder(node : @dom.Node) {
    if @domext.tag_type(@domext.node_name(node)) is Some(Remove) {
      @domext.remove_node(node)
      return
    }
    for child in node.children.copy() {
      finder(child)
    }
  }

  finder(doc)
  @domutils.merge_adjacent_text_nodes(doc)
}

///|
fn name_is_bold(node : @dom.Node) -> Bool {
  @domext.node_name(node) is ("strong" | "b")
}

///|
fn name_is_italic(node : @dom.Node) -> Bool {
  @domext.node_name(node) is ("em" | "i")
}

///|
fn name_is_bold_or_italic(node : @dom.Node) -> Bool {
  name_is_bold(node) || name_is_italic(node)
}

///|
fn name_is_strikethrough(node : @dom.Node) -> Bool {
  match @domext.node_name(node) {
    "s" | "del" | "strike" => true
    _ => false
  }
}

///|
fn name_is_both_bold_or_italic(a : @dom.Node, b : @dom.Node) -> Bool {
  (name_is_bold(a) && name_is_bold(b)) ||
  (name_is_italic(a) && name_is_italic(b))
}

///|
fn name_is_both_strikethrough(a : @dom.Node, b : @dom.Node) -> Bool {
  name_is_strikethrough(a) && name_is_strikethrough(b)
}

///|
fn name_is_pre(node : @dom.Node) -> Bool {
  @domext.node_name(node) is "pre"
}

///|
fn name_is_inline_code(node : @dom.Node) -> Bool {
  @domext.node_name(node) is ("code" | "var" | "samp" | "kbd" | "tt")
}

///|
fn name_is_link(node : @dom.Node) -> Bool {
  @domext.node_name(node) is "a"
}

///|
fn name_is_both_link(a : @dom.Node, b : @dom.Node) -> Bool {
  name_is_link(a) && name_is_link(b)
}

///|
fn name_is_heading(node : @dom.Node) -> Bool {
  @domext.is_heading_name(@domext.node_name(node))
}

///|
/// Commonmark DOM normalization passes (port of
/// `commonmark.handlePreRender`), run in the same order as the Go original.
fn pre_render_commonmark(doc : @dom.Node) -> Unit {
  @domutils.rename_fake_spans(doc)

  // Bold / Italic
  @domutils.remove_redundant(doc, name_is_both_bold_or_italic)
  @domutils.merge_adjacent(doc, name_is_bold_or_italic)

  // Strikethrough
  @domutils.remove_redundant(doc, name_is_both_strikethrough)
  @domutils.merge_adjacent(doc, name_is_strikethrough)

  // Code
  @domutils.remove_empty_code(doc)
  @domutils.swap_tags(doc, name_is_inline_code, name_is_pre)
  @domutils.merge_adjacent(doc, name_is_inline_code)
  @domutils.add_space(doc, name_is_bold_or_italic, name_is_inline_code)

  // Link
  @domutils.remove_redundant(doc, name_is_both_link)
  @domutils.swap_tags(doc, name_is_bold_or_italic, name_is_link)

  // Heading
  @domutils.swap_tags(doc, name_is_link, name_is_heading)
  @domutils.leaf_block_alternatives(doc)

  // List
  @domutils.move_list_items(doc)
}

///|
/// The fixed conversion pipeline: pre-render DOM passes, recursive render,
/// post-render text cleanup. Mirrors `Converter.ConvertNode` with the `base`
/// and `commonmark` plugins registered (at their respective priorities).
fn run_pipeline(doc : @dom.Node, options : Options) -> String {
  let ctx = RenderCtx::of(options)

  // ① Pre-Render: remove (early) -> commonmark (standard) -> collapse (late)
  //    -> list end comments (late+100)
  pre_render_remove(doc)
  pre_render_commonmark(doc)
  @collapse.collapse(doc)
  if options.list_end_comment {
    @domutils.add_list_end_comments(doc)
  }

  // ② Render
  let out = StringBuilder()
  render_node(ctx, out, doc)

  // ③ Post-Render: trim (standard) -> unescape (standard+20) ->
  //    code-block newline restore (late)
  let result = out.to_string()
  let result = @textutils.trim_space(result)
  let result = @textutils.trim_consecutive_newlines(result)
  let result = @textutils.trim_unnecessary_hard_line_breaks(result)
  let result = unescape(ctx, result)
  let result = result.replace_all(
    old=@escape.marker_code_block_newline,
    new="\n",
  )
  restore_internal_marker_literals(result)
}