///|
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 {
"\{name}>"
}
///|
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")}"
}
}