///|
/// Standalone HTML export for Markdown documents, extracted from MoMark's
/// app layer. The output is a minimal static document: block structure +
/// inline markup, no CSS, embedder-styled.
///|
/// Render a full standalone HTML document from a snapshot.
pub fn markdown_html_document(
snapshot : @richtext.MarkdownEditorSnapshot,
title : String,
) -> String {
@richtext.markdown_join_lines([
"",
"",
"",
" ",
" " + markdown_html_escape_text(title) + " ",
"",
"",
markdown_html_blocks(snapshot.blocks),
"",
"",
])
}
///|
/// Render block-level HTML for parsed editor blocks.
pub fn markdown_html_blocks(
blocks : Array[@richtext.MarkdownEditorBlock],
) -> String {
let parts : Array[String] = []
let mut index = 0
while index < blocks.length() {
let block = blocks[index].block
match block.kind {
UnorderedListItem | TaskListItem(_) => {
let group = markdown_html_unordered_list(blocks, index)
parts.push(group.html)
index = group.next_index
continue
}
OrderedListItem(_) => {
let group = markdown_html_ordered_list(blocks, index)
parts.push(group.html)
index = group.next_index
continue
}
Paragraph =>
if block.text.trim().to_owned() != "" {
parts.push("" + markdown_html_inlines(block.inlines) + "
")
}
Heading(level) => {
let tag = "h" +
@richtext.markdown_editor_clamp_int(level, 1, 6).to_string()
parts.push(
"<" +
tag +
">" +
markdown_html_inlines(block.inlines) +
"" +
tag +
">",
)
}
Blockquote =>
parts.push(
"" +
markdown_html_inlines(block.inlines) +
"
",
)
CodeBlock => parts.push(markdown_html_code_block(block))
HorizontalRule => parts.push("
")
Table => parts.push(markdown_html_table(block))
FootnoteDefinition(label) =>
parts.push(
"" +
markdown_html_escape_text(label) +
" " +
markdown_html_inlines(block.inlines) +
"
",
)
HtmlBlock => parts.push(block.text)
FrontMatter => ()
}
index = index + 1
}
@richtext.markdown_join_lines(parts)
}
///|
priv struct MarkdownHtmlListGroup {
html : String
next_index : Int
}
///|
fn markdown_html_unordered_list(
blocks : Array[@richtext.MarkdownEditorBlock],
start : Int,
) -> MarkdownHtmlListGroup {
let items : Array[String] = []
let mut index = start
while index < blocks.length() {
let block = blocks[index].block
match block.kind {
UnorderedListItem =>
items.push("" + markdown_html_inlines(block.inlines) + " ")
TaskListItem(checked) => {
let checked_attribute = if checked { " checked" } else { "" }
items.push(
" " +
markdown_html_inlines(block.inlines) +
" ",
)
}
_ => break
}
index = index + 1
}
{
html: "\n" + @richtext.markdown_join_lines(items) + "\n
",
next_index: index,
}
}
///|
fn markdown_html_ordered_list(
blocks : Array[@richtext.MarkdownEditorBlock],
start : Int,
) -> MarkdownHtmlListGroup {
let items : Array[String] = []
let mut index = start
let mut first_number = 1
while index < blocks.length() {
let block = blocks[index].block
match block.kind {
OrderedListItem(number) => {
if index == start {
first_number = number
}
items.push("" + markdown_html_inlines(block.inlines) + " ")
}
_ => break
}
index = index + 1
}
let start_attr = if first_number == 1 {
""
} else {
" start=\"" + first_number.to_string() + "\""
}
{
html: "\n" +
@richtext.markdown_join_lines(items) +
"\n
",
next_index: index,
}
}
///|
fn markdown_html_code_block(block : @richtext.MarkdownBlock) -> String {
let class_attr = match block.code_language {
Some(language) =>
if language.trim().to_owned() == "" {
""
} else {
" class=\"language-" +
markdown_html_escape_attribute(language.trim().to_owned()) +
"\""
}
None => ""
}
"" +
markdown_html_escape_text(block.text) +
"
"
}
///|
fn markdown_html_table(block : @richtext.MarkdownBlock) -> String {
let lines = @richtext.markdown_split_lines(block.text)
if lines.length() == 0 {
return ""
}
let rows : Array[String] = []
for index in 0.." +
markdown_html_inlines(@richtext.parse_markdown_inlines(text)) +
"" +
tag +
">",
)
}
rows.push("" + cell_html.join("") + " ")
}
let head = "\n" + rows[0] + "\n"
let body_rows : Array[String] = []
for index in 1..\n" + @richtext.markdown_join_lines(body_rows) + "\n"
}
"\n" + head + body + "\n
"
}
///|
fn markdown_html_table_align_attribute(
alignments : Array[@core.TextAlign?],
index : Int,
) -> String {
if index >= alignments.length() {
return ""
}
match alignments[index] {
Some(@core.TextAlign::TextStart) => " style=\"text-align:left\""
Some(@core.TextAlign::TextCenter) => " style=\"text-align:center\""
Some(@core.TextAlign::TextEnd) => " style=\"text-align:right\""
None => ""
}
}
///|
/// Render inline-level HTML for parsed inlines.
pub fn markdown_html_inlines(
inlines : Array[@richtext.MarkdownInline],
) -> String {
let parts : Array[String] = []
for inline in inlines {
parts.push(markdown_html_inline(inline))
}
parts.join("")
}
///|
fn markdown_html_inline(inline : @richtext.MarkdownInline) -> String {
let text = markdown_html_escape_text(inline.text)
match inline.kind {
Plain => text
Bold => "" + text + ""
Italic => "" + text + ""
Code => "" + text + ""
Strikethrough => "" + text + ""
Highlight => "" + text + ""
Link | Autolink => {
let target = match inline.target {
Some(target) => target
None => inline.text
}
"" +
text +
""
}
Image => {
let target = match inline.target {
Some(target) => target
None => ""
}
"
"
}
FootnoteReference =>
"" +
text +
""
HtmlInline => inline.text
}
}
///|
/// Escape `&`, `<`, `>` for HTML text content.
pub fn markdown_html_escape_text(text : String) -> String {
let chars : Array[Char] = []
for ch in text {
if ch == '&' {
chars.append("&".to_array())
} else if ch == '<' {
chars.append("<".to_array())
} else if ch == '>' {
chars.append(">".to_array())
} else {
chars.push(ch)
}
}
String::from_array(chars)
}
///|
/// Escape `&`, `<`, `>`, `"`, `'` for HTML attribute values.
pub fn markdown_html_escape_attribute(text : String) -> String {
let chars : Array[Char] = []
for ch in text {
if ch == '&' {
chars.append("&".to_array())
} else if ch == '<' {
chars.append("<".to_array())
} else if ch == '>' {
chars.append(">".to_array())
} else if ch == '"' {
chars.append(""".to_array())
} else if ch == '\'' {
chars.append("'".to_array())
} else {
chars.push(ch)
}
}
String::from_array(chars)
}