///|
pub fn render_document(
  blocks : Array[@parsing.Block],
  target~ : KatexTarget,
  disable_math? : Bool = false,
  halfwidth_punct? : Bool = false,
) -> String {
  blocks
  .map(b => render_block(b, target~, disable_math~, halfwidth_punct~))
  .join("\n")
}

///|
/// CSS for the on-screen default view: the document stays centered with a
/// reasonable width. Only applies to non-print pages.
let screen_css : String =
  $|@media screen {
  $|  body {
  $|    display: flex;
  $|    justify-content: center;
  $|    font-size: 20px;
  $|    margin: 0;
  $|    padding: 1rem;
  $|  }
  $|  .paper {
  $|    width: 100%;
  $|    max-width: 50rem;
  $|  }
  $|}
  $|

///|
pub fn render_document_page(
  blocks : Array[@parsing.Block],
  paper~ : Paper,
  target~ : KatexTarget,
  disable_math? : Bool = false,
  halfwidth_punct? : Bool = false,
) -> String {
  render_page(
    render_document(blocks, target~, disable_math~, halfwidth_punct~),
    paper~,
  )
}

///|
/// JS that splits the single `.paper` element into fixed-height pages so
/// every printed page of a slide deck gets its own uniform inset instead of a
/// lone first page. The slide geometry is applied inline only while printing
/// (`beforeprint`) and removed afterwards, so the screen view keeps the plain
/// continuous document and print measurement stays exact. Runs only for slide
/// papers.
fn slide_pagination_js(width : String, height : String) -> String {
  (
    $|
  )
}

///|
pub fn render_page(content : String, paper~ : Paper) -> String {
  let paginate_script = match paper {
    A4 => ""
    Presentation43 => "\n\{slide_pagination_js("10in", "7.5in")}"
    Presentation169 => "\n\{slide_pagination_js("13.33in", "7.5in")}"
  }
  (
    $|
    $|
    $|
    $|
    $|
    $|
    $|
    $|
$|\{content} $|
$|\{paginate_script} $| $| ) } ///| pub fn render_pre(text : String) -> String { "\{open_tag("pre", "")}\{escape_html(text)}\{close_tag("pre")}" } ///| fn escape_html(text : String) -> String { text .replace_all(old="&", new="&") .replace_all(old="<", new="<") .replace_all(old=">", new=">") .replace_all(old="\"", new=""") } ///| fn attr(name : String, value : String) -> String { " \{name}=\"\{escape_html(value)}\"" } ///| fn open_tag(name : String, attrs : String) -> String { "<\{name}\{attrs}>" } ///| fn close_tag(name : String) -> String { "" } ///| fn render_block( block : @parsing.Block, target~ : KatexTarget, disable_math~ : Bool, halfwidth_punct~ : Bool, ) -> String { match block { Heading(level, inlines) => "\{open_tag("h\{level}", "")}\{render_inlines(inlines, target~, disable_math~, halfwidth_punct~)}\{close_tag("h\{level}")}" Paragraph(inlines) => { let content = render_inlines( inlines, target~, disable_math~, halfwidth_punct~, ) guard !content.trim().is_empty() else { "" } "\{open_tag("p", "")}\{content}\{close_tag("p")}" } Blockquote(blocks) => "\{open_tag("blockquote", "")}\n\{blocks.map(b => render_block(b, target~, disable_math~, halfwidth_punct~)).join("\n")}\n\{close_tag("blockquote")}" UnorderedList(items) => render_list(items, "ul", target~, disable_math~, halfwidth_punct~) OrderedList(items) => render_list(items, "ol", target~, disable_math~, halfwidth_punct~) FencedCode(_, code) => "\{open_tag("pre", "")}\{open_tag("code", "")}\{escape_html(code)}\{close_tag("code")}\{close_tag("pre")}" ThematicBreak => "
" DisplayMath(latex) => { guard !disable_math else { escape_html("$$\{latex}$$") } let rendered = render_math(latex, true, target~) guard !rendered.is_empty() else { "" } let attrs = attr("class", "math-display") + attr("style", "display: block; margin: 1em 0; text-align: center") "\{open_tag("span", attrs)}\{rendered}\{close_tag("span")}" } Table(header, rows) => render_table(header, rows, target~, disable_math~, halfwidth_punct~) FootnoteDef(label, blocks) => "\{open_tag("div", attr("id", "fn:\{label}"))}\n\{blocks.map(b => render_block(b, target~, disable_math~, halfwidth_punct~)).join("\n")}\n\{close_tag("div")}" } } ///| fn render_list( items : Array[@parsing.MdListItem], kind : String, target~ : KatexTarget, disable_math~ : Bool, halfwidth_punct~ : Bool, ) -> String { "\{open_tag(kind, "")}\n\{items.map(e => render_list_item(e, target~, disable_math~, halfwidth_punct~)).join("\n")}\n\{close_tag(kind)}" } ///| fn render_list_item( item : @parsing.MdListItem, target~ : KatexTarget, disable_math~ : Bool, halfwidth_punct~ : Bool, ) -> String { let checkbox = match item.checked { Some(true) => open_tag( "input", attr("checked", "") + attr("disabled", "") + attr("type", "checkbox"), ) Some(false) => open_tag("input", attr("disabled", "") + attr("type", "checkbox")) None => "" } let content = render_inlines( item.content, target~, disable_math~, halfwidth_punct~, ) let children = item.children .map(b => render_block(b, target~, disable_math~, halfwidth_punct~)) .join("\n") "\{open_tag("li", "")}\{checkbox}\{content}\n\{children}\{close_tag("li")}" } ///| fn render_table( header : @parsing.TableRow, rows : Array[@parsing.TableRow], target~ : KatexTarget, disable_math~ : Bool, halfwidth_punct~ : Bool, ) -> String { let thead = "\{open_tag("thead", "")}\{render_table_row(header, "th", target~, disable_math~, halfwidth_punct~)}\{close_tag("thead")}" let tbody = "\{open_tag("tbody", "")}\n\{rows.map(r => render_table_row(r, "td", target~, disable_math~, halfwidth_punct~)).join("\n")}\n\{close_tag("tbody")}" "\{open_tag("table", "")}\n\{thead}\n\{tbody}\n\{close_tag("table")}" } ///| fn render_table_row( row : @parsing.TableRow, cell_tag : String, target~ : KatexTarget, disable_math~ : Bool, halfwidth_punct~ : Bool, ) -> String { "\{open_tag("tr", "")}\{row.cells.map(c => render_table_cell(c, cell_tag, target~, disable_math~, halfwidth_punct~)).join("")}\{close_tag("tr")}" } ///| fn render_table_cell( cells : Array[@parsing.Inline], cell_tag : String, target~ : KatexTarget, disable_math~ : Bool, halfwidth_punct~ : Bool, ) -> String { "\{open_tag(cell_tag, "")}\{render_inlines(cells, target~, disable_math~, halfwidth_punct~)}\{close_tag(cell_tag)}" } ///| fn render_inlines( inlines : Array[@parsing.Inline], target~ : KatexTarget, disable_math~ : Bool, halfwidth_punct~ : Bool, prev~ : Char? = None, next~ : Char? = None, ) -> String { let buf = StringBuilder() for i in 0.. 0 { inline_last_char(inlines[i - 1]) } else { prev } let next_c = if i + 1 < inlines.length() { inline_first_char(inlines[i + 1]) } else { next } let convert = halfwidth_punct buf.write_string( render_inline( inlines[i], target~, disable_math~, convert~, prev=prev_c, next=next_c, ), ) } buf.to_string() } ///| fn render_inline( inline : @parsing.Inline, target~ : KatexTarget, disable_math~ : Bool, convert~ : Bool, prev~ : Char?, next~ : Char?, ) -> String { match inline { Plain(text) => if convert { escape_html(to_halfwidth_punct(text, prev~, next~)) } else { escape_html(text) } Strong(inlines) => "\{open_tag("strong", "")}\{render_inlines(inlines, target~, disable_math~, halfwidth_punct=convert, prev~, next~)}\{close_tag("strong")}" Emph(inlines) => "\{open_tag("em", "")}\{render_inlines(inlines, target~, disable_math~, halfwidth_punct=convert, prev~, next~)}\{close_tag("em")}" Code(text) => "\{open_tag("code", "")}\{escape_html(text)}\{close_tag("code")}" Image(alt, url) => open_tag( "img", attr("src", url) + attr("alt", if convert { to_halfwidth_punct(alt) } else { alt }), ) SoftBreak => "\n" HardBreak => "
" InlineMath(latex) => { guard !disable_math else { escape_html("$\{latex}$") } let rendered = render_math(latex, false, target~) guard !rendered.is_empty() else { "" } rendered } FootnoteRef(label) => "\{open_tag("sup", "")}\{open_tag("a", attr("href", "#fn:\{label}"))}\{escape_html(label)}\{close_tag("a")}\{close_tag("sup")}" } }