///|
using @bobzhang/html_parser {
  type HtmlError,
  type ParsedHtml,
  type Node,
  type NodeKind,
  parse_fragment,
  parse,
}

///|
/// Convert parsed html from @bobzhang/html_parser to the MoonBit Html DSL
fn convert_parsed_html(nodes : ParsedHtml) -> @h.Html {
  return convert_node(nodes.root)
}

///|
/// Convert raw html string to the MoonBit Html DSL.
///
/// Inputs that contain an explicit ``, ``, or `` start tag
/// are parsed as a full document, which preserves those structural tags.
/// Everything else is parsed as a fragment.
fn convert_to_dsl(str : String) -> @h.Html raise HtmlError {
  if has_document_tags(str) {
    str |> parse() |> convert_parsed_html()
  } else {
    str |> parse_fragment() |> convert_parsed_html()
  }
}

///|
/// Check whether the input contains an explicit ``, ``, or
/// `` start tag. Tag names are matched case-insensitively; comments
/// and raw-text bodies (script/style/textarea) are ignored because the
/// tokenizer already treats them as character data.
fn has_document_tags(str : String) -> Bool {
  for token in @bobzhang/html_parser.tokenize(str).tokens {
    match token {
      @bobzhang/html_parser.HtmlToken::Tag(t) =>
        if t.kind == @bobzhang/html_parser.TagKind::StartTag &&
          (t.name == "html" || t.name == "head" || t.name == "body") {
          return true
        }
      _ => ()
    }
  }
  false
}

///|
/// Convert raw html string to the MoonBit Html DSL
pub fn convert_html(str : String) -> String raise HtmlError {
  let code = str |> minify_html() |> convert_to_dsl() |> print_html_dsl()
  "let html_code =  \{code}" |> @fmt.format()
}

///|
/// Convert a single @bobzhang/html_parser node to the MoonBit Html DSL.
///
/// Document and fragment roots become `@h.fragment`, comments become
/// `@h.comment`, and doctypes become `@h.doctype`.
fn convert_node(node : Node) -> @h.Html {
  match node.kind() {
    NodeKind::Document | NodeKind::Fragment =>
      @h.fragment(node.children().map(convert_node))
    NodeKind::Element => convert_element(node)
    NodeKind::Text => @h.text(node.data())
    NodeKind::Comment => @h.comment(node.data())
    NodeKind::Doctype => convert_doctype(node)
  }
}

///|
/// Convert a doctype node to a DSL doctype declaration.
fn convert_doctype(node : Node) -> @h.Html {
  match (node.public_id, node.system_id) {
    (Some(public_id), Some(system_id)) =>
      @h.doctype(name=node.data(), public_id~, system_id~)
    (Some(public_id), None) => @h.doctype(name=node.data(), public_id~)
    (None, Some(system_id)) => @h.doctype(name=node.data(), system_id~)
    (None, None) => @h.doctype(name=node.data())
  }
}

///|
/// Convert an element node: tag name, attributes, and recursively converted
/// children.
///
/// SVG elements go through the `@svg` builders; `