///|
/// Render a node into `out` (port of `Converter.handleRenderNode`):
/// text nodes go through the text pipeline, elements through the static
/// renderer dispatch, everything unhandled through the fallback.
fn render_node(ctx : RenderCtx, out : StringBuilder, node : @dom.Node) -> Unit {
  if node.kind is Text {
    render_text(ctx, out, node)
    return
  }
  if render_element(ctx, out, node) {
    return
  }
  render_fallback(ctx, out, node)
}

///|
/// Render all children of `node` (iterates over a copy, mirroring
/// `dom.AllChildNodes`).
fn render_children(
  ctx : RenderCtx,
  out : StringBuilder,
  node : @dom.Node,
) -> Unit {
  for child in node.children.copy() {
    render_node(ctx, out, child)
  }
}

///|
/// Static dispatch to the commonmark renderers (port of
/// `commonmark.handleRender`). Returns false when no renderer handled the
/// node, like `RenderTryNext`.
fn render_element(
  ctx : RenderCtx,
  out : StringBuilder,
  node : @dom.Node,
) -> Bool {
  match @domext.node_name(node) {
    "strong" | "b" | "em" | "i" => {
      render_bold_italic(ctx, out, node)
      true
    }
    "s" | "del" | "strike" => {
      render_strikethrough(ctx, out, node)
      true
    }
    "table" => render_table(ctx, out, node)
    "hr" => {
      render_divider(ctx, out, node)
      true
    }
    "br" => {
      render_break(ctx, out, node)
      true
    }
    "ul" | "ol" => {
      render_list_container(ctx, out, node)
      true
    }
    "pre" => {
      render_block_code(ctx, out, node)
      true
    }
    "code" | "var" | "samp" | "kbd" | "tt" => {
      render_inline_code(ctx, out, node)
      true
    }
    "blockquote" => {
      render_blockquote(ctx, out, node)
      true
    }
    "h1" | "h2" | "h3" | "h4" | "h5" | "h6" => {
      render_heading(ctx, out, node)
      true
    }
    "img" => render_image(ctx, out, node)
    "a" => render_link(ctx, out, node)
    "#comment" => render_comment(ctx, out, node)
    _ => false
  }
}

///|
/// Fallback renderer (port of `handleRenderFallback`): block tags get blank
/// lines around their children, everything else renders children in place.
fn render_fallback(
  ctx : RenderCtx,
  out : StringBuilder,
  node : @dom.Node,
) -> Unit {
  let is_block = @domext.is_block_tag_type(@domext.node_name(node))
  if is_block {
    out.write_string("\n\n")
  }
  render_children(ctx, out, node)
  if is_block {
    out.write_string("\n\n")
  }
}

///|
/// Text pipeline (port of `base.handleTextTransform` +
/// `commonmark.handleTextTransform`): entity-escape angle brackets, insert
/// escape markers, then (inside links) force-escape "]".
fn render_text(ctx : RenderCtx, out : StringBuilder, node : @dom.Node) -> Unit {
  let content = protect_internal_marker_literals(node.data)
    .replace_all(old="<", new="<")
    .replace_all(old=">", new=">")
  let content = escape(ctx, content)
  let content = if ctx.is_inside_link {
    // The "]" would prematurely close the link text.
    content.replace_all(old=@escape.marker_escaping + "]", new="\\]")
  } else {
    content
  }
  out.write_string(content)
}