///|
fn get_heading_level(name : String) -> Int {
  match name {
    "h1" => 1
    "h2" => 2
    "h3" => 3
    "h4" => 4
    "h5" => 5
    _ => 6
  }
}

///|
/// Width of the widest line in chars, not counting escape markers; setext
/// underlines below 3 chars could be ambiguous (port of
/// `commonmark.getUnderlineWidth`).
fn get_underline_width(content : String, min_val : Int) -> Int {
  let mut width = 0
  for part in content.split("\n") {
    let mut w = 0
    for ch in part {
      if ch != @escape.MARKER_ESCAPING {
        w += 1
      }
    }
    if w > width {
      width = w
    }
  }
  if width < min_val {
    min_val
  } else {
    width
  }
}

///|
/// Collapse runs of 2+ spaces to a single space (Go regex `  +`).
fn collapse_multiple_spaces(content : String) -> String {
  let out = StringBuilder(size_hint=content.length())
  let mut spaces = 0
  for ch in content {
    if ch == ' ' {
      spaces += 1
    } else {
      if spaces == 1 {
        out.write_char(' ')
      } else if spaces > 1 {
        out.write_char(' ')
      }
      spaces = 0
      out.write_char(ch)
    }
  }
  if spaces > 0 {
    out.write_char(' ')
  }
  out.to_string()
}

///|
/// An ATX heading would lose a trailing "#", so force-escape it by turning
/// its escape marker into a backslash (port of
/// `commonmark.escapePoundSignAtEnd`).
fn escape_pound_sign_at_end(content : String) -> String {
  let len = content.length()
  if len == 0 || content[len - 1] != '#' {
    return content
  }
  if len >= 3 && content[len - 3] == '\\' {
    // Already escaped.
    return content
  }
  if len >= 2 && content[len - 2] == '\u{0007}' {
    // Override the placeholder with a real backslash.
    content[:len - 2].to_owned() + "\\#"
  } else {
    // No marker (escaping disabled): leave the content untouched.
    content
  }
}

///|
/// Render h1..h6 in ATX or setext style (port of
/// `commonmark.renderHeading`).
fn render_heading(
  ctx : RenderCtx,
  out : StringBuilder,
  node : @dom.Node,
) -> Unit {
  let level = get_heading_level(@domext.node_name(node))
  let buf = StringBuilder()
  render_children(ctx, buf, node)
  let content = buf.to_string()
  if @textutils.trim_space(content) is "" {
    return
  }
  if ctx.options.heading_style is Setext && level < 3 {
    // EscapeMultiLine takes care of remaining newlines.
    let content = @textutils.trim_consecutive_newlines(content)
    let content = @textutils.escape_multiline(content)
    let width = get_underline_width(content, 3)
    let underline_char = if level == 1 { "=" } else { "-" }
    out.write_string("\n\n")
    out.write_string(content)
    out.write_char('\n')
    out.write_string(underline_char.repeat(width))
    out.write_string("\n\n")
  } else {
    let content = content
      .replace_all(old="\n", new=" ")
      .replace_all(old="\r", new=" ")
      |> collapse_multiple_spaces
      |> @textutils.trim_space
      |> escape_pound_sign_at_end
    out.write_string("\n\n")
    out.write_string("#".repeat(level))
    out.write_char(' ')
    out.write_string(content)
    out.write_string("\n\n")
  }
}