///|
/// Recursively walk a `@html_dsl.Html` node and print the MoonBit source
/// code that defines it, using the html_dsl builder functions.
///
/// Example: a `div` with id/class attributes wrapping a paragraph becomes
/// `div(id="divid-01", class="flex flex-row", [p([text("hello")])])`.
///
/// Attributes that a tag's builder cannot express as labelled arguments are
/// kept in an `attrs=[Attr::...]` list, and tags without a dedicated builder
/// fall back to the generic `node("tag", attrs=[...], [children])` builder.
fn print_html_dsl(node : @h.Html) -> String {
  match node {
    @h.Html::Text(s) => "text(\{str_lit(s)})"
    @h.Html::Nothing => "nothing()"
    @h.Html::Unsafe(s) => "raw_html(\{str_lit(s)})"
    @h.Html::Fragment(children) => "fragment([\{print_children(children)}])"
    @h.Html::Comment(data) => "comment(\{str_lit(data)})"
    @h.Html::Node(tag, attrs, children) => print_element(tag, attrs, children)
  }
}

///|
/// Join the printed children of an element or fragment.
fn print_children(children : Array[@h.Html]) -> String {
  children.map(print_html_dsl).join(", ")
}

///|
/// Print an element node: known tags use their dedicated builder with
/// labelled arguments, unknown tags fall back to the generic `node` builder.
fn print_element(
  tag : String,
  attrs : Array[@h.Attr],
  children : Array[@h.Html],
) -> String {
  if tag == "script" {
    return print_script(attrs, children)
  }
  if tag == "title" {
    return print_title(attrs, children)
  }
  if tag == "head" {
    if attrs.is_empty() {
      return "head([\{print_children(children)}])"
    }
    return node_literal(tag, attrs, children)
  }
  if tag == "html" {
    return print_html_el(attrs, children)
  }
  if tag == "br" || tag == "hr" {
    if attrs.is_empty() {
      return "\{tag}()"
    }
    return node_literal(tag, attrs, children)
  }
  if tag == "embed" || tag == "source" || tag == "track" {
    return print_media(tag, attrs, children)
  }
  match svg_tag_shape(tag) {
    Some(shape) => return print_svg_element(tag, attrs, children, shape)
    None => ()
  }
  match tag_shape(tag) {
    None => node_literal(tag, attrs, children)
    Some(shape) => {
      for required in shape.required {
        if !has_named(attrs, required) {
          return node_literal(tag, attrs, children)
        }
      }
      if !shape.children && !children.is_empty() && !is_void_tag(tag) {
        return node_literal(tag, attrs, children)
      }
      let parts : Array[String] = []
      let rest : Array[String] = []
      for attr in attrs {
        match attr_named(attr) {
          Some((name, value)) if shape.args.contains(name) =>
            parts.push("\{name}=\{value}")
          _ => rest.push(attr_literal(attr))
        }
      }
      if !rest.is_empty() {
        parts.push("attrs=[\{rest.join(", ")}]")
      }
      if shape.children {
        parts.push("[\{print_children(children)}]")
      }
      "\{shape.name}(\{parts.join(", ")})"
    }
  }
}

///|
/// Print a `