///| HTML Renderer
///| Renders markdown AST to HTML
///|
/// Render a document to HTML
pub fn render_html(doc : Document) -> String {
let buf = StringBuilder::new()
for block in doc.children {
render_block_html(block, buf)
}
buf.to_string()
}
///|
/// Render a block element to HTML
fn render_block_html(block : Block, buf : StringBuilder) -> Unit {
match block {
Block::Paragraph(children~, ..) => {
buf.write_string("")
render_inlines_html(children, buf)
buf.write_string("
\n")
}
Block::Heading(level~, children~, ..) => {
buf.write_string("')
render_inlines_html(children, buf)
buf.write_string(" \n")
}
Block::ThematicBreak(..) => buf.write_string("
\n")
Block::FencedCode(info~, code~, ..) => {
if info.is_empty() {
buf.write_string("")
} else {
// Extract language from info string (first word)
let parts = info.split(" ").collect()
let lang = if parts.length() > 0 { parts[0].to_string() } else { info }
buf.write_string("")
}
buf.write_string(escape_html(code))
buf.write_string("
\n")
}
Block::IndentedCode(code~, ..) => {
buf.write_string("")
buf.write_string(escape_html(code))
buf.write_string("
\n")
}
Block::Blockquote(children~, ..) => {
buf.write_string("\n")
for child in children {
render_block_html(child, buf)
}
buf.write_string("
\n")
}
Block::BulletList(items~, tight~, ..) => {
// Check if this list contains task items
let has_task = items.iter().any(fn(item) { not(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)
}
buf.write_string("
\n")
}
Block::OrderedList(items~, start~, tight~, ..) => {
let has_task = items.iter().any(fn(item) { not(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)
}
buf.write_string("
\n")
}
Block::HtmlBlock(html~, ..) => {
buf.write_string(html)
if not(html.has_suffix("\n")) {
buf.write_char('\n')
}
}
Block::Table(header~, alignments~, rows~, ..) => {
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)
}
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)
}
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.write_string("\n")
for child in children {
render_block_html(child, buf)
}
buf.write_string("\n")
}
}
}
///|
/// Render a list item to HTML
fn render_list_item_html(
item : ListItem,
buf : StringBuilder,
tight : Bool,
is_task_list : Bool,
) -> Unit {
// Task list items get a special class
if is_task_list && not(item.checked is None) {
buf.write_string("- ")
} else {
buf.write_string("
- ")
}
// Task list checkbox
match item.checked {
Some(true) =>
buf.write_string(" ")
Some(false) => buf.write_string(" ")
None => ()
}
if tight {
// Tight list: render inline content without
wrapper
for child in item.children {
match child {
Paragraph(children~, ..) => render_inlines_html(children, buf)
_ => render_block_html(child, buf)
}
}
} else {
// Loose list: render blocks normally
buf.write_char('\n')
for child in item.children {
render_block_html(child, buf)
}
}
buf.write_string("
\n")
}
///|
/// Render a table cell to HTML
fn render_table_cell_html(
cell : TableCell,
buf : StringBuilder,
tag : String,
align : TableAlign,
) -> 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)
buf.write_string("")
buf.write_string(tag)
buf.write_string(">\n")
}
///|
/// Render inline elements to HTML
fn render_inlines_html(inlines : Array[Inline], buf : StringBuilder) -> Unit {
for inline in inlines {
render_inline_html(inline, buf)
}
}
///|
/// Render a single inline element to HTML
fn render_inline_html(inline : Inline, buf : StringBuilder) -> Unit {
match inline {
Inline::Text(content~, ..) => buf.write_string(escape_html(content))
Inline::Code(content~, ..) => {
buf.write_string("")
buf.write_string(escape_html(content))
buf.write_string("")
}
Inline::Emphasis(children~, ..) => {
buf.write_string("")
render_inlines_html(children, buf)
buf.write_string("")
}
Inline::Strong(children~, ..) => {
buf.write_string("")
render_inlines_html(children, buf)
buf.write_string("")
}
Inline::Strikethrough(children~, ..) => {
buf.write_string("")
render_inlines_html(children, buf)
buf.write_string("")
}
Inline::Link(children~, url~, title~, ..) => {
buf.write_string("')
render_inlines_html(children, buf)
buf.write_string("")
}
Inline::RefLink(children~, label~, ..) => {
// Reference links should be resolved before rendering
// For now, render as plain text with the label
buf.write_char('[')
render_inlines_html(children, buf)
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~, ..) => {
buf.write_string("
")
}
Inline::RefImage(alt~, label~, ..) => {
// Reference images should be resolved before rendering
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("")
}
}
}
///|
/// Escape HTML special characters in text content
fn escape_html(s : String) -> String {
let buf = StringBuilder::new()
for c in s {
match c {
'&' => buf.write_string("&")
'<' => buf.write_string("<")
'>' => buf.write_string(">")
'"' => buf.write_string(""")
_ => buf.write_char(c)
}
}
buf.to_string()
}
///|
/// Escape HTML special characters in attribute values
fn escape_html_attr(s : String) -> String {
let buf = StringBuilder::new()
for c in s {
match c {
'&' => buf.write_string("&")
'<' => buf.write_string("<")
'>' => buf.write_string(">")
'"' => buf.write_string(""")
'\'' => buf.write_string("'")
_ => buf.write_char(c)
}
}
buf.to_string()
}
///|
/// Parse markdown and render to HTML
pub fn md_to_html(source : String) -> String {
let result = parse(source)
render_html(result.document)
}
///|
/// Parse markdown and render to HTML (strict mode)
pub fn md_to_html_strict(source : String) -> String {
let result = parse(source, strict=true)
render_html(result.document)
}