///|
fn do_convert_document(doc : @html.Document, config : Config) -> String {
let writer = CodeWriter::new(config.indent)
let ctx : ConvertContext = { config, writer }
let body_id = doc.body_element
let children = if body_id >= 0 {
doc.get_children(body_id)
} else {
doc.get_children(0)
}
let effective = filter_meaningful_children(doc, children)
if effective.length() == 1 {
ctx.convert_node(doc, effective[0])
} else if effective.length() > 1 {
ctx.writer
..write(ctx.config.module_prefix)
..write("fragment([")
..newline()
.indent()
for i, child_id in effective {
if i > 0 {
ctx.writer..write(",").newline()
}
ctx.writer.write_indent()
ctx.convert_node(doc, child_id)
}
ctx.writer..newline()..dedent()..write_indent().write("])")
} else {
ctx.writer..write(ctx.config.module_prefix).write("nothing")
}
ctx.writer.to_string()
}
///|
priv struct ConvertContext {
config : Config
writer : CodeWriter
}
///|
fn filter_meaningful_children(
doc : @html.Document,
children : Array[Int],
) -> Array[Int] {
let result : Array[Int] = []
for id in children {
match doc.get_node(id) {
Some(@html.ElementNode(_)) => result.push(id)
Some(@html.TextNode(_)) => {
let text = doc.get_text_content(id)
if !text.trim().is_empty() {
result.push(id)
}
}
Some(@html.CommentNode(_)) => result.push(id)
_ => ()
}
}
result
}
///|
fn ConvertContext::convert_node(
self : ConvertContext,
doc : @html.Document,
node_id : Int,
) -> Unit {
match doc.get_node(node_id) {
Some(@html.ElementNode(elem)) => self.convert_element(doc, elem, node_id)
Some(@html.TextNode(text)) => self.convert_text(text)
Some(@html.CommentNode(text)) => self.convert_comment(text)
_ => ()
}
}
///|
fn ConvertContext::convert_text(self : ConvertContext, text : String) -> Unit {
self.writer.write(escape_moonbit_string(text))
}
///|
fn ConvertContext::convert_comment(
self : ConvertContext,
text : String,
) -> Unit {
if self.config.convert_comments {
for line in text.split("\n") {
self.writer..write("// ")..write(line.trim().to_string()).newline()
}
}
}
///|
fn ConvertContext::convert_element(
self : ConvertContext,
doc : @html.Document,
elem : @html.Element,
node_id : Int,
) -> Unit {
let tag = elem.tag_name.to_lower()
let tag_info = if self.config.use_named_functions {
let info = get_tag_info(tag)
if tag == "a" && self.config.a_tag_mode is Node {
None
} else {
info
}
} else {
None
}
let is_void = is_void_element(tag)
let child_ids = doc.get_children(node_id)
let meaningful_children = filter_meaningful_children(doc, child_ids)
let has_children = meaningful_children.length() > 0
let use_named = tag_info is Some(_)
let fn_name = match tag_info {
Some(info) => info.fn_name
None => "node"
}
let filtered_attrs : Array[(String, String)] = []
let seen : Set[String] = Set::new()
for pair in elem.attributes {
let name = pair.0
let value = pair.1
if seen.contains(name) {
continue
}
seen.add(name)
if name == "style" && value.trim().is_empty() {
continue
}
filtered_attrs.push((name, value))
}
let sep = separate_attrs(filtered_attrs, tag, tag_info, use_named)
self.generate_element_code(
doc,
tag,
fn_name,
use_named,
is_void,
has_children,
meaningful_children,
sep.named_args,
sep.fallback_attrs,
sep.style_decls,
)
}
///|
priv struct SepAttrs {
named_args : Array[(String, String, String)]
fallback_attrs : Array[(String, String)]
style_decls : Array[StyleDecl]
}
///|
fn separate_attrs(
attrs : Array[(String, String)],
tag : String,
tag_info : TagInfo?,
use_named : Bool,
) -> SepAttrs {
let named_args : Array[(String, String, String)] = []
let fallback_attrs : Array[(String, String)] = []
let style_decls : Array[StyleDecl] = []
let tag_params = match tag_info {
Some(info) => info.named_params
None => Map::new()
}
for entry in attrs {
let (name, value) = entry
let kind = classify_attr(name, tag, tag_params)
if !use_named {
match kind {
StyleAttr => {
let decls = parse_style(value)
for decl in decls {
style_decls.push(decl)
}
}
_ => fallback_attrs.push((name, value))
}
} else {
match kind {
CommonNamedParam(param_name) =>
named_args.push((param_name, value, "string"))
TagSpecificNamedParam(param_name) =>
if param_name == "input_type" {
named_args.push((param_name, value, "input_type"))
} else if is_dom_bool_property(name) {
named_args.push((param_name, value, "bool"))
} else {
named_args.push((param_name, value, "string"))
}
DomBoolProperty => named_args.push((name, value, "bool"))
DomStringProperty => named_args.push((name, value, "string"))
StyleAttr => {
let decls = parse_style(value)
for decl in decls {
style_decls.push(decl)
}
}
FallbackAttribute => fallback_attrs.push((name, value))
EventHandler => fallback_attrs.push((name, value))
}
}
}
{ named_args, fallback_attrs, style_decls }
}
///|
fn ConvertContext::generate_element_code(
self : ConvertContext,
doc : @html.Document,
tag : String,
fn_name : String,
use_named : Bool,
is_void : Bool,
has_children : Bool,
children : Array[Int],
named_args : Array[(String, String, String)],
fallback_attrs : Array[(String, String)],
style_decls : Array[StyleDecl],
) -> Unit {
let prefix = self.config.module_prefix
let has_fallback = fallback_attrs.length() > 0
let has_styles = style_decls.length() > 0
if use_named {
self.generate_named_element(
doc, tag, fn_name, is_void, has_children, children, named_args, fallback_attrs,
style_decls, prefix, has_fallback, has_styles,
)
} else {
self.generate_node_element(
doc, tag, children, has_children, fallback_attrs, style_decls, prefix, has_fallback,
has_styles,
)
}
}
///|
fn ConvertContext::generate_named_element(
self : ConvertContext,
doc : @html.Document,
tag : String,
fn_name : String,
is_void : Bool,
has_children : Bool,
children : Array[Int],
named_args : Array[(String, String, String)],
fallback_attrs : Array[(String, String)],
style_decls : Array[StyleDecl],
prefix : String,
has_fallback : Bool,
has_styles : Bool,
) -> Unit {
ignore(tag)
let needs_attrs_chain = has_fallback
self.writer..write(prefix).write(fn_name)
let mut has_parens_content = false
if has_styles {
self.writer.write("(style=[")
for i, decl in style_decls {
if i > 0 {
self.writer.write(", ")
}
self.writer.write(escape_moonbit_string(decl.prop + ":" + decl.value))
}
self.writer.write("]")
has_parens_content = true
}
for tuple in named_args {
let param_name = tuple.0
let value = tuple.1
let type_kind = tuple.2
if !has_parens_content {
self.writer.write("(")
} else {
self.writer.write(", ")
}
self.write_named_arg(param_name, value, type_kind)
has_parens_content = true
}
if needs_attrs_chain {
if !has_parens_content {
self.writer.write("(")
} else {
self.writer.write(", ")
}
self.writer..write("attrs=")..write(prefix).write("Attrs::build()")
for pair in fallback_attrs {
self.write_attr_chain(pair.0, pair.1)
}
has_parens_content = true
}
if !is_void && has_children {
if has_parens_content {
self.writer.write(")")
}
if self.config.use_reverse_pipe {
self.writer.write(" <| ")
self.convert_children_compact(doc, children)
} else {
self.writer.write("(")
self.convert_children_inline(doc, children)
self.writer.write(")")
}
} else if is_void && !has_children {
if has_parens_content {
self.writer.write(")")
} else {
self.writer.write("()")
}
} else if is_void && has_children {
if !has_parens_content {
self.writer.write("(")
}
self.writer.write(", ")
self.convert_children_inline(doc, children)
self.writer.write(")")
} else if has_parens_content {
self.writer.write(")")
}
}
///|
fn ConvertContext::generate_node_element(
self : ConvertContext,
doc : @html.Document,
tag : String,
children : Array[Int],
has_children : Bool,
fallback_attrs : Array[(String, String)],
style_decls : Array[StyleDecl],
prefix : String,
has_fallback : Bool,
has_styles : Bool,
) -> Unit {
self.writer
..write(prefix)
..write("node(")
..write(escape_moonbit_string(tag))
..write(", ")
..write(prefix)
.write("Attrs::build()")
for pair in fallback_attrs {
self.write_attr_chain(pair.0, pair.1)
}
for decl in style_decls {
self.writer
..write(".style(")
..write(escape_moonbit_string(decl.prop))
..write(", ")
..write(escape_moonbit_string(decl.value))
.write(")")
}
ignore(has_fallback)
ignore(has_styles)
if has_children {
if self.config.use_reverse_pipe {
self.writer..write(")").write(" <| ")
self.convert_children_compact(doc, children)
} else {
self.writer.write(", ")
self.convert_children_inline(doc, children)
self.writer.write(")")
}
} else {
self.writer
..write(", ([] : Array[")
..write(prefix[:prefix.length() - 1].to_string())
.write("Html]))")
}
}
///|
fn ConvertContext::write_named_arg(
self : ConvertContext,
param_name : String,
value : String,
type_kind : String,
) -> Unit {
self.writer..write(param_name).write("=")
if type_kind == "bool" {
let b = parse_bool_value(value)
self.writer.write(b.to_string())
} else if type_kind == "input_type" {
self.writer
..write(self.config.module_prefix)
..write("InputType::")
.write(input_type_from_string(value))
} else {
self.writer.write(escape_moonbit_string(value))
}
}
///|
fn parse_bool_value(s : String) -> Bool {
match s.to_lower() {
"" => true
"true" => true
_ => false
}
}
///|
fn input_type_from_string(s : String) -> String {
match s.to_lower() {
"button" => "Button"
"checkbox" => "Checkbox"
"color" => "Color"
"date" => "Date"
"datetime-local" => "DateTimeLocal"
"email" => "Email"
"file" => "File"
"hidden" => "Hidden"
"image" => "Image"
"month" => "Month"
"number" => "Number"
"password" => "Password"
"radio" => "Radio"
"range" => "Range"
"reset" => "Reset"
"search" => "Search"
"submit" => "Submit"
"tel" => "Tel"
"text" => "Text"
"time" => "Time"
"url" => "Url"
"week" => "Week"
_ => "Text"
}
}
///|
fn ConvertContext::write_attr_chain(
self : ConvertContext,
name : String,
value : String,
) -> Unit {
match get_attr_method(name) {
Some(info) => {
self.writer..write(".")..write(info.method_name).write("(")
match info.param_type {
AttrBool => {
let b = parse_bool_value(value)
self.writer..write(b.to_string()).write(")")
}
AttrInt =>
try {
let _ = @string.parse_int(value)
self.writer..write(value).write(")")
} catch {
_ => self.writer..write(escape_moonbit_string(value)).write(")")
}
AttrString =>
self.writer..write(escape_moonbit_string(value)).write(")")
}
}
None =>
if name.has_prefix("data-") {
let data_name = name["data-".length():].to_string()
self.writer
..write(".data_set(")
..write(escape_moonbit_string(data_name))
..write(", ")
..write(escape_moonbit_string(value))
.write(")")
} else if name.has_prefix("aria-") {
self.writer
..write(".")
..write(aria_method_name(name))
..write("(")
..write(escape_moonbit_string(value))
.write(")")
} else {
self.writer..write("// TODO: unsupported attr: ").write(name)
}
}
}
///|
fn aria_method_name(attr_name : String) -> String {
let suffix = attr_name["aria-".length():].to_string()
let parts = suffix.split("-")
let result = StringBuilder::new()
result.write_string("aria_")
for i, part in parts {
if i == 0 {
result.write_view(part)
} else if !part.is_empty() {
result.write_view(part[:1].to_upper())
result.write_view(part[1:].to_upper())
}
}
result.to_string()
}
///|
fn ConvertContext::convert_children_inline(
self : ConvertContext,
doc : @html.Document,
children : Array[Int],
) -> Unit {
if children.length() == 0 {
return
}
if is_single_text_child(doc, children) {
let text = get_text_content(doc, children[0])
self.writer.write(escape_moonbit_string(text))
return
}
if children.length() == 1 {
self.convert_node(doc, children[0])
return
}
self.writer.write("[")
for i, child_id in children {
if i > 0 {
self.writer.write(", ")
}
self.convert_node(doc, child_id)
}
self.writer.write("]")
}
///|
fn ConvertContext::convert_children_compact(
self : ConvertContext,
doc : @html.Document,
children : Array[Int],
) -> Unit {
if children.length() == 0 {
return
}
if is_single_text_child(doc, children) {
let text = get_text_content(doc, children[0])
self.writer.write(escape_moonbit_string(text))
return
}
if children.length() == 1 {
self.convert_node(doc, children[0])
return
}
self.writer.write("[")
for i, child_id in children {
if i > 0 {
self.writer.write(", ")
}
self.convert_node(doc, child_id)
}
self.writer.write("]")
}
///|
fn is_single_text_child(doc : @html.Document, children : Array[Int]) -> Bool {
children is [child] && doc.get_node(child) is Some(@html.TextNode(_))
}
///|
fn get_text_content(doc : @html.Document, node_id : Int) -> String {
match doc.get_node(node_id) {
Some(@html.TextNode(text)) => text
_ => ""
}
}