///| HTML Renderer

///| Renders markdown AST to HTML

///|
/// Output buffer that remembers whether it is at the start of a line, so that
/// block-level elements can always begin on one — the `cr()` of the reference
/// implementation's HTML renderer.
priv struct HtmlBuf {
  inner : StringBuilder
  mut at_line_start : Bool
}

///|
fn HtmlBuf::new() -> HtmlBuf {
  { inner: StringBuilder(), at_line_start: true }
}

///|
fn HtmlBuf::write_string(self : HtmlBuf, s : String) -> Unit {
  if s.is_empty() {
    return
  }
  self.inner.write_string(s)
  self.at_line_start = s.unsafe_get(s.length() - 1) == '\n'
}

///|
fn HtmlBuf::write_char(self : HtmlBuf, c : Char) -> Unit {
  self.inner.write_char(c)
  self.at_line_start = c == '\n'
}

///|
/// Start a new line unless the output already is at one.
fn HtmlBuf::cr(self : HtmlBuf) -> Unit {
  if !self.at_line_start {
    self.write_char('\n')
  }
}

///|
fn HtmlBuf::to_string(self : HtmlBuf) -> String {
  self.inner.to_string()
}

///|
/// Everything the block/inline renderers need to know beyond the node itself.
priv struct RenderCtx {
  autolink : Bool
  defs : Map[String, LinkDefinition]
}

///|
/// Render a document to HTML
pub fn render_html(doc : Document, autolink? : Bool = true) -> String {
  let defs : Map[String, LinkDefinition] = Map(
    [],
    capacity=doc.definitions.length(),
  )
  for def in doc.definitions {
    let key = normalize_label(def.label)
    if !defs.contains(key) {
      defs[key] = def
    }
  }
  let ctx = RenderCtx::{ autolink, defs }
  let buf = HtmlBuf::new()
  for block in doc.children {
    render_block_html(block, buf, ctx)
  }
  buf.to_string()
}

///|
/// Render a block element to HTML
fn render_block_html(block : Block, buf : HtmlBuf, ctx : RenderCtx) -> Unit {
  match block {
    Block::Paragraph(children~, ..) => {
      buf.cr()
      buf.write_string("

") render_inlines_html(children, buf, ctx) buf.write_string("

\n") } Block::Heading(level~, children~, ..) => { buf.cr() buf.write_string("') render_inlines_html(children, buf, ctx) buf.write_string("\n") } Block::ThematicBreak(..) => { buf.cr() buf.write_string("
\n") } Block::FencedCode(info~, code~, ..) => { buf.cr() let lang = code_block_language(info) if lang.is_empty() { buf.write_string("
")
      } else {
        buf.write_string("
")
      }
      write_code_content(code, buf)
      buf.write_string("
\n") } Block::IndentedCode(code~, ..) => { buf.cr() buf.write_string("
")
      write_code_content(code, buf)
      buf.write_string("
\n") } Block::Blockquote(children~, ..) => { buf.cr() buf.write_string("
\n") for child in children { render_block_html(child, buf, ctx) } buf.write_string("
\n") } Block::BulletList(items~, tight~, ..) => { buf.cr() // Check if this list contains task items let has_task = items.iter().any(fn(item) { !(item.checked is None) }) if has_task { buf.write_string("
    \n") } else { buf.write_string("
      \n") } for item in items { render_list_item_html(item, buf, tight, has_task, ctx) } buf.write_string("
    \n") } Block::OrderedList(items~, start~, tight~, ..) => { buf.cr() let has_task = items.iter().any(fn(item) { !(item.checked is None) }) if start == 1 { if has_task { buf.write_string("
      \n") } else { buf.write_string("
        \n") } } else { if has_task { buf.write_string("
          \n") } for item in items { render_list_item_html(item, buf, tight, has_task, ctx) } buf.write_string("
        \n") } Block::HtmlBlock(html~, ..) => { buf.cr() buf.write_string(html) if !html.has_suffix("\n") { buf.write_char('\n') } } Block::Table(header~, alignments~, rows~, ..) => { buf.cr() buf.write_string("\n\n\n") for i, cell in header { let align = if i < alignments.length() { alignments[i] } else { TableAlign::None } render_table_cell_html(cell, buf, "th", align, ctx) } buf.write_string("\n\n") if rows.length() > 0 { buf.write_string("\n") for row in rows { buf.write_string("\n") for i, cell in row { let align = if i < alignments.length() { alignments[i] } else { TableAlign::None } render_table_cell_html(cell, buf, "td", align, ctx) } buf.write_string("\n") } buf.write_string("\n") } buf.write_string("
        \n") } Block::BlankLines(..) => () // Blank lines don't produce HTML output Block::FootnoteDefinition(label~, children~, ..) => { buf.cr() buf.write_string("
        \n") for child in children { render_block_html(child, buf, ctx) } buf.write_string("
        \n") } } } ///| /// The language class of a fenced code block: the first word of the info /// string, with escapes and entity references resolved. fn code_block_language(info : String) -> String { let decoded = unescape_string(info) let len = decoded.length() let mut start = 0 while start < len && is_html_space(decoded.unsafe_get(start)) { start = start + 1 } let mut end = start while end < len && !is_html_space(decoded.unsafe_get(end)) { end = end + 1 } decoded.unsafe_substring(start~, end~) } ///| /// Write code-block content, guaranteeing the trailing newline CommonMark /// requires inside `
        `.
        fn write_code_content(code : String, buf : HtmlBuf) -> Unit {
          if code.is_empty() {
            return
          }
          buf.write_string(escape_html(code))
          if !code.has_suffix("\n") {
            buf.write_char('\n')
          }
        }
        
        ///|
        /// Render a list item to HTML
        fn render_list_item_html(
          item : ListItem,
          buf : HtmlBuf,
          tight : Bool,
          is_task_list : Bool,
          ctx : RenderCtx,
        ) -> Unit {
          buf.cr()
          // Task list items get a special class
          if is_task_list && !(item.checked is None) {
            buf.write_string("
      1. ") } else { buf.write_string("
      2. ") } // Task list checkbox match item.checked { Some(true) => buf.write_string(" ") Some(false) => buf.write_string(" ") None => () } for child in item.children { match child { // A tight list drops the

        wrapper around its items' paragraphs. Paragraph(children~, ..) if tight => render_inlines_html(children, buf, ctx) _ => render_block_html(child, buf, ctx) } } buf.write_string("

      3. \n") } ///| /// Render a table cell to HTML fn render_table_cell_html( cell : TableCell, buf : HtmlBuf, tag : String, align : TableAlign, ctx : RenderCtx, ) -> Unit { buf.write_char('<') buf.write_string(tag) match align { TableAlign::Left => buf.write_string(" align=\"left\"") TableAlign::Center => buf.write_string(" align=\"center\"") TableAlign::Right => buf.write_string(" align=\"right\"") TableAlign::None => () } buf.write_char('>') render_inlines_html(cell.children, buf, ctx) buf.write_string("\n") } ///| /// Render inline elements to HTML fn render_inlines_html( inlines : Array[Inline], buf : HtmlBuf, ctx : RenderCtx, ) -> Unit { for inline in inlines { render_inline_html(inline, buf, ctx) } } ///| /// Build the rendered destination for a wiki link. fn render_wikilink_destination(target : String, fragment : String) -> String { if fragment.is_empty() { target } else { target + "#" + fragment } } ///| /// Render text content, optionally turning bare http(s) URLs into anchors. fn render_text_html( content : String, buf : HtmlBuf, autolink? : Bool = true, ) -> Unit { if !autolink { buf.write_string(escape_html(content)) return } let len = content.length() let mut pos = 0 while pos < len { match find_next_url_start(content, pos) { Some(start) => { write_escaped_text_range(content, pos, start, buf) let raw_end = find_url_raw_end(content, start) let url_end = trim_url_end(content, start, raw_end) if url_end > start { let url = content.unsafe_substring(start~, end=url_end) buf.write_string("") buf.write_string(escape_html(url)) buf.write_string("") pos = url_end } else { write_escaped_text_range(content, start, raw_end, buf) pos = raw_end } } None => { write_escaped_text_range(content, pos, len, buf) pos = len } } } } ///| fn write_escaped_text_range( content : String, start : Int, end : Int, buf : HtmlBuf, ) -> Unit { if end > start { buf.write_string(escape_html(content.unsafe_substring(start~, end~))) } } ///| /// Resolve a reference link/image label against the document's definitions. fn RenderCtx::lookup(self : RenderCtx, label : String) -> LinkDefinition? { self.defs.get(normalize_label(label)) } ///| /// Render a single inline element to HTML fn render_inline_html(inline : Inline, buf : HtmlBuf, ctx : RenderCtx) -> Unit { match inline { Inline::Text(content~, ..) => render_text_html(content, buf, autolink=ctx.autolink) Inline::Code(content~, ..) => { buf.write_string("") buf.write_string(escape_html(normalize_code_span(content))) buf.write_string("") } Inline::WikiLink(target~, label~, fragment~, ..) => { let href = render_wikilink_destination(target, fragment) let text = if label.is_empty() { href } else { label } buf.write_string("") buf.write_string(escape_html(text)) buf.write_string("") } Inline::Emphasis(children~, ..) => { buf.write_string("") render_inlines_html(children, buf, ctx) buf.write_string("") } Inline::Strong(children~, ..) => { buf.write_string("") render_inlines_html(children, buf, ctx) buf.write_string("") } Inline::Strikethrough(children~, ..) => { buf.write_string("") render_inlines_html(children, buf, ctx) buf.write_string("") } Inline::Link(children~, url~, title~, ..) => write_anchor(children, url, title, buf, ctx) Inline::RefLink(children~, label~, ..) => match ctx.lookup(label) { Some(def) => write_anchor(children, def.url, def.title, buf, ctx) None => { buf.write_char('[') render_inlines_html(children, buf, ctx) buf.write_string("][") buf.write_string(escape_html(label)) buf.write_char(']') } } Inline::Autolink(url~, is_email~, ..) => { buf.write_string("") buf.write_string(escape_html(url)) buf.write_string("") } Inline::Image(alt~, url~, title~, ..) => write_image(alt, url, title, buf, ctx) Inline::RefImage(alt~, label~, ..) => match ctx.lookup(label) { Some(def) => write_image(alt, def.url, def.title, buf, ctx) None => { buf.write_string("![") buf.write_string(escape_html(alt)) buf.write_string("][") buf.write_string(escape_html(label)) buf.write_char(']') } } Inline::HtmlInline(html~, ..) => buf.write_string(html) Inline::SoftBreak(..) => buf.write_char('\n') Inline::HardBreak(..) => buf.write_string("
        \n") Inline::FootnoteReference(label~, ..) => { buf.write_string("") buf.write_string(escape_html(label)) buf.write_string("") } } } ///| fn write_anchor( children : Array[Inline], url : String, title : String, buf : HtmlBuf, ctx : RenderCtx, ) -> Unit { buf.write_string("') render_inlines_html(children, buf, { ..ctx, autolink: false }) buf.write_string("") } ///| fn write_image( alt : String, url : String, title : String, buf : HtmlBuf, ctx : RenderCtx, ) -> Unit { buf.write_string("\"")") } ///| /// An image's `alt` keeps the source of its label so it round-trips; the HTML /// form is that label rendered as plain text. fn render_alt_text(alt : String, ctx : RenderCtx) -> String { let buf = StringBuilder() write_plain_text(parse_inlines_with_defs(alt, ctx.defs, false), buf) buf.to_string() } ///| fn write_plain_text(inlines : Array[Inline], buf : StringBuilder) -> Unit { for inline in inlines { match inline { Inline::Text(content~, ..) => buf.write_string(content) Inline::Code(content~, ..) => buf.write_string(normalize_code_span(content)) Inline::HtmlInline(html~, ..) => buf.write_string(html) Inline::SoftBreak(..) | Inline::HardBreak(..) => buf.write_char('\n') Inline::Emphasis(children~, ..) | Inline::Strong(children~, ..) | Inline::Strikethrough(children~, ..) | Inline::Link(children~, ..) | Inline::RefLink(children~, ..) => write_plain_text(children, buf) Inline::Image(alt~, ..) | Inline::RefImage(alt~, ..) => buf.write_string(alt) Inline::Autolink(url~, ..) => buf.write_string(url) Inline::WikiLink(target~, label~, ..) => buf.write_string(if label.is_empty() { target } else { label }) Inline::FootnoteReference(label~, ..) => buf.write_string(label) } } } ///| /// Escape HTML special characters in text content. /// Pass `attr=true` to also escape `'` (for attribute values). /// /// Most input strings don't contain any HTML-special characters, so a /// quick scan-pass lets us return the input unchanged in that common /// case and avoid building a new buffer. fn escape_html(s : String, attr? : Bool = false) -> String { let mut needs_escape = false for c in s { match c { '&' | '<' | '>' | '"' => { needs_escape = true break } '\'' if attr => { needs_escape = true break } _ => () } } if !needs_escape { return s } let buf = StringBuilder() for c in s { match c { '&' => buf.write_string("&") '<' => buf.write_string("<") '>' => buf.write_string(">") '"' => buf.write_string(""") '\'' if attr => buf.write_string("'") _ => buf.write_char(c) } } buf.to_string() } ///| /// Characters that may appear literally in an `href`; everything else is /// percent-encoded (or, for `&` and `'`, written as a character reference). /// The table matches the reference implementation's. fn is_href_safe(code : Int) -> Bool { if code < 0x21 || code > 0x7E { return false } match code { 0x22 | 0x26 | 0x27 | 0x3C | 0x3E | 0x5B | 0x5C | 0x5D | 0x5E | 0x60 | 0x7B | 0x7C | 0x7D => false _ => true } } ///| fn write_hex_byte(byte : Int, buf : HtmlBuf) -> Unit { let digits = "0123456789ABCDEF" buf.write_char('%') buf.write_string( digits.unsafe_substring( start=(byte >> 4) & 0xF, end=((byte >> 4) & 0xF) + 1, ), ) buf.write_string( digits.unsafe_substring(start=byte & 0xF, end=(byte & 0xF) + 1), ) } ///| /// Percent-encode a URL for use in an `href`/`src` attribute. fn write_escaped_href(url : String, buf : HtmlBuf) -> Unit { for c in url { let code = c.to_int() if is_href_safe(code) { buf.write_char(c) } else if code == 0x26 { buf.write_string("&") } else if code == 0x27 { buf.write_string("'") } else if code < 0x80 { write_hex_byte(code, buf) } else if code < 0x800 { write_hex_byte(0xC0 | (code >> 6), buf) write_hex_byte(0x80 | (code & 0x3F), buf) } else if code < 0x10000 { write_hex_byte(0xE0 | (code >> 12), buf) write_hex_byte(0x80 | ((code >> 6) & 0x3F), buf) write_hex_byte(0x80 | (code & 0x3F), buf) } else { write_hex_byte(0xF0 | (code >> 18), buf) write_hex_byte(0x80 | ((code >> 12) & 0x3F), buf) write_hex_byte(0x80 | ((code >> 6) & 0x3F), buf) write_hex_byte(0x80 | (code & 0x3F), buf) } } } ///| /// Parse markdown and render to HTML pub fn md_to_html( source : String, wikilinks? : Bool = false, autolink? : Bool = true, ) -> String { let result = parse(source, wikilinks~) render_html(result.document, autolink~) } ///| /// Parse markdown and render to HTML (strict mode) /// /// Kept for backwards compatibility: the parser is now always spec-strict. pub fn md_to_html_strict( source : String, wikilinks? : Bool = false, autolink? : Bool = true, ) -> String { md_to_html(source, wikilinks~, autolink~) }