// Writing the tree back as HTML. The output is the one the CommonMark
// specification's examples were produced with, so that a document rendered here
// and a document run through the reference implementation agree byte for byte.

///|
/// The tags the filter keeps out of a page, because a browser would treat them
/// as a new context.
let filtered_tags : Array[String] = [
  "title", "textarea", "style", "xmp", "iframe", "noembed", "noframes", "script",
  "plaintext",
]

///|
/// Render the document as HTML. With `safe` set, which is the default, raw HTML
/// is escaped and links to dangerous destinations lose their anchor; pass
/// `safe=false` to write a document that may embed HTML of its own.
pub fn Document::html(self : Document, safe~ : Bool = true) -> String {
  let out = HtmlOut::new()
  let permit_html = !safe
  let blocks = self.blocks
  // The footnotes, if there are any, were gathered to the end of the document
  // and are the last blocks; they are written inside the notes section.
  let footnotes_start = find_footnotes_start(blocks)
  render_blocks(
    out,
    blocks.clamped_view(start=0, end=footnotes_start),
    self.flavor,
    permit_html,
    false,
  )
  if footnotes_start < blocks.length() {
    out.put_string("
\n
    \n") render_blocks( out, blocks.clamped_view(start=footnotes_start, end=blocks.length()), self.flavor, permit_html, false, ) out.put_string("
\n
\n") } out.buf.to_string() } ///| /// The first of the trailing footnote blocks, or the end of the array when /// there are none. fn find_footnotes_start(blocks : Array[Block]) -> Int32 { let mut i = blocks.length() - 1 while i >= 0 { match blocks[i] { Footnote(_) => i = i - 1 _ => break } } i + 1 } ///| /// A buffer that knows whether it is at the start of a line, which is what the /// blank line rules of the output are written in terms of. priv struct HtmlOut { buf : StringBuilder mut at_line_start : Bool } ///| fn HtmlOut::new() -> HtmlOut { { buf: StringBuilder(), at_line_start: true, } } ///| fn HtmlOut::put(self : HtmlOut, text : StringView) -> Unit { self.buf.write_view(text) if text.length() > 0 { self.at_line_start = cu(text, text.length() - 1) == 0x0A } } ///| fn HtmlOut::put_string(self : HtmlOut, text : String) -> Unit { self.put(text[:]) } ///| fn HtmlOut::put_char(self : HtmlOut, c : Char) -> Unit { self.buf.write_char(c) self.at_line_start = c == '\n' } ///| /// Start a new line unless the output is already at one. fn HtmlOut::cr(self : HtmlOut) -> Unit { if !self.at_line_start { self.buf.write_char('\n') self.at_line_start = true } } ///| fn HtmlOut::escaped(self : HtmlOut, text : StringView) -> Unit { write_html_escaped(self.buf, text) if text.length() > 0 { self.at_line_start = cu(text, text.length() - 1) == 0x0A } } ///| /// Whether the raw HTML at `start` opens one of the filtered tags. fn is_filtered_tag(text : StringView, start : Int32) -> Bool { for tag in filtered_tags { let tag_view : StringView = tag[:] let mut i = start + 1 if cu(text, i) == 0x2F { i = i + 1 } let name_start = i let length = tag_view.length() if i + length > text.length() { continue } if text .clamped_view(start=name_start, end=name_start + length) .equal_ignore_ascii_case(tag_view) { let after = cu(text, name_start + length) if after == 0x20 || after == 0x09 || after == 0x0A || after == 0x0C || after == 0x0D || after == 0x3E { return true } if after == 0x2F && cu(text, name_start + length + 1) == 0x3E { return true } } } false } ///| /// Write raw HTML, escaping the `<` of the tags the filter keeps out. fn filtered_html(out : HtmlOut, text : StringView, flavor : Flavor) -> Unit { if !flavor.tag_filter { out.put(text) return } let pieces = StringBuilder() let n = text.length() let mut i = 0 while i < n { let c = char_at(text, i) if c == '<' && is_filtered_tag(text, i) { out.put(pieces.to_string()[:]) pieces.reset() out.put_string("<") } else { pieces.write_char(c) } i = i + c.utf16_len() } out.put(pieces.to_string()[:]) } ///| fn render_blocks( out : HtmlOut, blocks : ArrayView[Block], flavor : Flavor, permit_html : Bool, tight : Bool, ) -> Unit { for block in blocks { render_block(out, block, flavor, permit_html, tight) } } ///| fn render_block( out : HtmlOut, block : Block, flavor : Flavor, permit_html : Bool, tight : Bool, ) -> Unit { match block { Heading(heading) => { out.cr() out.put_string("') render_inlines(out, heading.content, flavor, permit_html, false, false) out.put_string("\n") } Paragraph(content) => { if !tight { out.cr() out.put_string("

") } render_inlines(out, content, flavor, permit_html, false, false) if !tight { out.put_string("

\n") } } Code(code) => { out.cr() if code.info.is_empty() { out.put_string("
")
      } else {
        let info = code.info[:]
        let mut first_word = 0
        while first_word < info.length() &&
              !(cu(info, first_word) == 0x20 ||
              cu(info, first_word) == 0x09 ||
              cu(info, first_word) == 0x0A ||
              cu(info, first_word) == 0x0B ||
              cu(info, first_word) == 0x0C ||
              cu(info, first_word) == 0x0D) {
          first_word = first_word + 1
        }
        out.put_string("
")
      }
      out.escaped(code.text[:])
      out.put_string("
\n") } Quote(children) => { out.cr() out.put_string("
\n") render_blocks(out, children, flavor, permit_html, false) out.cr() out.put_string("
\n") } List(list) => render_list(out, list, flavor, permit_html) Table(table) => render_table(out, table, flavor, permit_html) Html(text) => { out.cr() if permit_html { filtered_html(out, text[:], flavor) } else { out.escaped(text[:]) } out.cr() } Break => { out.cr() out.put_string("
\n") } Footnote(footnote) => render_footnote(out, footnote, flavor, permit_html) } } ///| /// One footnote: a list item whose last line carries the link back to the /// reference it is reached from, and one more for every further reference to /// the same footnote. fn render_footnote( out : HtmlOut, footnote : Footnote, flavor : Flavor, permit_html : Bool, ) -> Unit { out.put_string("
  • \n") let blocks = footnote.blocks let mut backref_written = false let mut i = 0 for block in blocks { let is_last = i == blocks.length() - 1 if is_last { match block { // The backref goes inside the last paragraph, the way a paragraph at // the end of a footnote reads. Paragraph(content) => { out.cr() out.put_string("

    ") render_inlines(out, content, flavor, permit_html, false, false) out.put_char(' ') write_footnote_backref(out, footnote) out.put_string("

    \n") backref_written = true } _ => render_block(out, block, flavor, permit_html, false) } } else { render_block(out, block, flavor, permit_html, false) } i = i + 1 } if !backref_written { // The body does not end in a paragraph, so the backref stands alone. write_footnote_backref(out, footnote) out.put_char('\n') } out.put_string("
  • \n") } ///| /// The links back to the references a footnote is reached from. fn write_footnote_backref(out : HtmlOut, footnote : Footnote) -> Unit { out.put_string("↩") for i = 2; i <= footnote.backrefs; i = i + 1 { out.put_string(" ↩") out.put_string(i.to_string()) out.put_string("") } } ///| fn render_list( out : HtmlOut, list : ListBlock, flavor : Flavor, permit_html : Bool, ) -> Unit { match list.kind { Bullet(_) => { out.cr() out.put_string("
      \n") } Ordered(start, _) => { out.cr() if start == 1 { out.put_string("
        \n") } else { out.put_string("
          \n") } } } for item in list.items { out.cr() out.put_string("
        1. ") match item.task { Checked => out.put_string( " ", ) Unchecked => out.put_string(" ") Plain => () } render_blocks(out, item.blocks, flavor, permit_html, list.tight) out.put_string("
        2. \n") } match list.kind { Bullet(_) => out.put_string("
    \n") Ordered(_, _) => out.put_string("\n") } } ///| fn render_table( out : HtmlOut, table : Table, flavor : Flavor, permit_html : Bool, ) -> Unit { out.cr() out.put_string("") out.cr() out.put_string("") out.cr() out.put_string("") let mut column = 0 for cell in table.head { out.cr() out.put_string("") render_inlines(out, cell, flavor, permit_html, false, false) out.put_string("") column = column + 1 } out.cr() out.put_string("") out.cr() out.put_string("") if table.body.length() > 0 { out.cr() out.put_string("") for row in table.body { out.cr() out.put_string("") let mut i = 0 for cell in row { out.cr() out.put_string("") render_inlines(out, cell, flavor, permit_html, false, false) out.put_string("") i = i + 1 } out.cr() out.put_string("") } out.cr() out.put_string("") } out.cr() out.put_string("
    ") out.cr() } ///| fn render_align(out : HtmlOut, align : Array[Align], column : Int32) -> Unit { if column >= align.length() { return } match align[column] { Left => out.put_string(" align=\"left\"") Center => out.put_string(" align=\"center\"") Right => out.put_string(" align=\"right\"") Default => () } } ///| fn render_inlines( out : HtmlOut, inlines : Array[Inline], flavor : Flavor, permit_html : Bool, plain : Bool, inside_strong : Bool, ) -> Unit { for inline in inlines { render_inline(out, inline, flavor, permit_html, plain, inside_strong) } } ///| fn render_inline( out : HtmlOut, inline : Inline, flavor : Flavor, permit_html : Bool, plain : Bool, inside_strong : Bool, ) -> Unit { match inline { Text(text) => out.escaped(text[:]) Code(code) => if plain { out.escaped(code[:]) } else { out.put_string("") out.escaped(code[:]) out.put_string("") } Html(html) => if plain { out.escaped(html[:]) } else if permit_html { filtered_html(out, html[:], flavor) } else { out.escaped(html[:]) } Emphasis(children) => if plain { render_inlines(out, children, flavor, permit_html, true, inside_strong) } else { out.put_string("") render_inlines(out, children, flavor, permit_html, false, inside_strong) out.put_string("") } Strong(children) => if plain { render_inlines(out, children, flavor, permit_html, true, inside_strong) } else { out.put_string("") render_inlines(out, children, flavor, permit_html, false, true) out.put_string("") } Strike(children) => if plain { render_inlines(out, children, flavor, permit_html, true, inside_strong) } else { out.put_string("") render_inlines(out, children, flavor, permit_html, false, inside_strong) out.put_string("") } Break(Soft) => if plain { out.put_char(' ') } else { out.put_char('\n') } Break(Hard) => if plain { out.put_char(' ') } else { out.put_string("
    \n") } Link(link) => { let dangerous = is_dangerous_url(link.dest[:]) if plain { render_inlines( out, link.content, flavor, permit_html, true, inside_strong, ) } else if dangerous && !permit_html { // A destination that would run script keeps its text and loses its link. render_inlines( out, link.content, flavor, permit_html, false, inside_strong, ) } else { out.put_string("") render_inlines( out, link.content, flavor, permit_html, false, inside_strong, ) out.put_string("") } } Image(image) => { let dangerous = is_dangerous_url(image.dest[:]) if plain { render_inlines( out, image.content, flavor, permit_html, true, inside_strong, ) } else if dangerous && !permit_html { // Without a source there is nothing to show but the alternative text. render_inlines( out, image.content, flavor, permit_html, true, inside_strong, ) } else { out.put_string("\"")") } } FootnoteRef(fnref) => if plain { out.escaped(fnref.label[:]) } else { out.put_string(" 1 { out.put_char('-') out.put_string(fnref.ref_ix.to_string()) } out.put_string("\" data-footnote-ref>") out.put_string(fnref.number.to_string()) out.put_string("") } } }