///|
/// Standalone HTML export for Markdown documents, extracted from MoMark's
/// app layer. The output is a minimal static document: block structure +
/// inline markup, no CSS, embedder-styled.

///|
/// Render a full standalone HTML document from a snapshot.
pub fn markdown_html_document(
  snapshot : @richtext.MarkdownEditorSnapshot,
  title : String,
) -> String {
  @richtext.markdown_join_lines([
    "",
    "",
    "",
    "  ",
    "  " + markdown_html_escape_text(title) + "",
    "",
    "",
    markdown_html_blocks(snapshot.blocks),
    "",
    "",
  ])
}

///|
/// Render block-level HTML for parsed editor blocks.
pub fn markdown_html_blocks(
  blocks : Array[@richtext.MarkdownEditorBlock],
) -> String {
  let parts : Array[String] = []
  let mut index = 0
  while index < blocks.length() {
    let block = blocks[index].block
    match block.kind {
      UnorderedListItem | TaskListItem(_) => {
        let group = markdown_html_unordered_list(blocks, index)
        parts.push(group.html)
        index = group.next_index
        continue
      }
      OrderedListItem(_) => {
        let group = markdown_html_ordered_list(blocks, index)
        parts.push(group.html)
        index = group.next_index
        continue
      }
      Paragraph =>
        if block.text.trim().to_owned() != "" {
          parts.push("

" + markdown_html_inlines(block.inlines) + "

") } Heading(level) => { let tag = "h" + @richtext.markdown_editor_clamp_int(level, 1, 6).to_string() parts.push( "<" + tag + ">" + markdown_html_inlines(block.inlines) + "", ) } Blockquote => parts.push( "

" + markdown_html_inlines(block.inlines) + "

", ) CodeBlock => parts.push(markdown_html_code_block(block)) HorizontalRule => parts.push("
") Table => parts.push(markdown_html_table(block)) FootnoteDefinition(label) => parts.push( "

" + markdown_html_escape_text(label) + " " + markdown_html_inlines(block.inlines) + "

", ) HtmlBlock => parts.push(block.text) FrontMatter => () } index = index + 1 } @richtext.markdown_join_lines(parts) } ///| priv struct MarkdownHtmlListGroup { html : String next_index : Int } ///| fn markdown_html_unordered_list( blocks : Array[@richtext.MarkdownEditorBlock], start : Int, ) -> MarkdownHtmlListGroup { let items : Array[String] = [] let mut index = start while index < blocks.length() { let block = blocks[index].block match block.kind { UnorderedListItem => items.push("
  • " + markdown_html_inlines(block.inlines) + "
  • ") TaskListItem(checked) => { let checked_attribute = if checked { " checked" } else { "" } items.push( "
  • " + markdown_html_inlines(block.inlines) + "
  • ", ) } _ => break } index = index + 1 } { html: "", next_index: index, } } ///| fn markdown_html_ordered_list( blocks : Array[@richtext.MarkdownEditorBlock], start : Int, ) -> MarkdownHtmlListGroup { let items : Array[String] = [] let mut index = start let mut first_number = 1 while index < blocks.length() { let block = blocks[index].block match block.kind { OrderedListItem(number) => { if index == start { first_number = number } items.push("
  • " + markdown_html_inlines(block.inlines) + "
  • ") } _ => break } index = index + 1 } let start_attr = if first_number == 1 { "" } else { " start=\"" + first_number.to_string() + "\"" } { html: "\n" + @richtext.markdown_join_lines(items) + "\n", next_index: index, } } ///| fn markdown_html_code_block(block : @richtext.MarkdownBlock) -> String { let class_attr = match block.code_language { Some(language) => if language.trim().to_owned() == "" { "" } else { " class=\"language-" + markdown_html_escape_attribute(language.trim().to_owned()) + "\"" } None => "" } "
    " +
      markdown_html_escape_text(block.text) +
      "
    " } ///| fn markdown_html_table(block : @richtext.MarkdownBlock) -> String { let lines = @richtext.markdown_split_lines(block.text) if lines.length() == 0 { return "" } let rows : Array[String] = [] for index in 0.." + markdown_html_inlines(@richtext.parse_markdown_inlines(text)) + "", ) } rows.push("" + cell_html.join("") + "") } let head = "\n" + rows[0] + "\n" let body_rows : Array[String] = [] for index in 1..\n" + @richtext.markdown_join_lines(body_rows) + "\n" } "\n" + head + body + "\n
    " } ///| fn markdown_html_table_align_attribute( alignments : Array[@core.TextAlign?], index : Int, ) -> String { if index >= alignments.length() { return "" } match alignments[index] { Some(@core.TextAlign::TextStart) => " style=\"text-align:left\"" Some(@core.TextAlign::TextCenter) => " style=\"text-align:center\"" Some(@core.TextAlign::TextEnd) => " style=\"text-align:right\"" None => "" } } ///| /// Render inline-level HTML for parsed inlines. pub fn markdown_html_inlines( inlines : Array[@richtext.MarkdownInline], ) -> String { let parts : Array[String] = [] for inline in inlines { parts.push(markdown_html_inline(inline)) } parts.join("") } ///| fn markdown_html_inline(inline : @richtext.MarkdownInline) -> String { let text = markdown_html_escape_text(inline.text) match inline.kind { Plain => text Bold => "" + text + "" Italic => "" + text + "" Code => "" + text + "" Strikethrough => "" + text + "" Highlight => "" + text + "" Link | Autolink => { let target = match inline.target { Some(target) => target None => inline.text } "" + text + "" } Image => { let target = match inline.target { Some(target) => target None => "" } "\""" } FootnoteReference => "" + text + "" HtmlInline => inline.text } } ///| /// Escape `&`, `<`, `>` for HTML text content. pub fn markdown_html_escape_text(text : String) -> String { let chars : Array[Char] = [] for ch in text { if ch == '&' { chars.append("&".to_array()) } else if ch == '<' { chars.append("<".to_array()) } else if ch == '>' { chars.append(">".to_array()) } else { chars.push(ch) } } String::from_array(chars) } ///| /// Escape `&`, `<`, `>`, `"`, `'` for HTML attribute values. pub fn markdown_html_escape_attribute(text : String) -> String { let chars : Array[Char] = [] for ch in text { if ch == '&' { chars.append("&".to_array()) } else if ch == '<' { chars.append("<".to_array()) } else if ch == '>' { chars.append(">".to_array()) } else if ch == '"' { chars.append(""".to_array()) } else if ch == '\'' { chars.append("'".to_array()) } else { chars.push(ch) } } String::from_array(chars) }