///|
fn escape_html(source : String) -> String {
let out = StringBuilder::new(size_hint=source.length())
for i in 0.. out.write_string("&")
'<' => out.write_string("<")
'>' => out.write_string(">")
'"' => out.write_string(""")
'\'' => out.write_string("'")
ch => out.write_char(ch)
}
}
out.to_string()
}
///|
fn slugify(title : String) -> String {
let out = StringBuilder::new()
let mut separator = false
for i in 0..= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch.to_int() > 127 {
out.write_char(ch)
separator = false
} else if ch >= 'A' && ch <= 'Z' {
out.write_char(Int::unsafe_to_char(ch.to_int() + 32))
separator = false
} else if !separator {
out.write_char('-')
separator = true
}
}
out.to_string().trim(chars="-").to_owned()
}
///|
/// Extract level 2-4 headings for the page-local table of contents.
pub fn extract_sections(source : String) -> Array[Section] {
let sections : Array[Section] = []
let mut in_code = false
for raw_line in source.split("\n") {
let line = raw_line.trim().to_owned()
if line.has_prefix("```") {
in_code = !in_code
continue
}
if in_code || !line.has_prefix("#") {
continue
}
let mut level = 0
while level < line.length() &&
level < 6 &&
line.code_unit_at(level).to_int() == '#'.to_int() {
level += 1
}
if level >= 2 && level <= 4 {
let title = line[level:].trim().to_owned()
sections.push({ title, id: slugify(title), level })
}
}
sections
}
///|
fn inline_markdown(source : String) -> String {
let out = StringBuilder::new()
let mut pos = 0
while pos < source.length() {
if source.code_unit_at(pos).to_int() == '`'.to_int() {
let close = find_text(source, "`", pos + 1)
if close > pos {
out.write_string("")
out.write_string(escape_html(source[pos + 1:close].to_owned()))
out.write_string("")
pos = close + 1
continue
}
}
if pos + 1 < source.length() && source[pos:pos + 2].to_owned() == "**" {
let close = find_text(source, "**", pos + 2)
if close > pos {
out.write_string("")
out.write_string(inline_markdown(source[pos + 2:close].to_owned()))
out.write_string("")
pos = close + 2
continue
}
}
if pos + 1 < source.length() && source[pos:pos + 2].to_owned() == "~~" {
let close = find_text(source, "~~", pos + 2)
if close > pos {
out.write_string("")
out.write_string(inline_markdown(source[pos + 2:close].to_owned()))
out.write_string("")
pos = close + 2
continue
}
}
if pos + 1 < source.length() && source[pos:pos + 2].to_owned() == "==" {
let close = find_text(source, "==", pos + 2)
if close > pos {
out.write_string("")
out.write_string(inline_markdown(source[pos + 2:close].to_owned()))
out.write_string("")
pos = close + 2
continue
}
}
if pos + 1 < source.length() && source[pos:pos + 2].to_owned() == "
let close = if middle >= 0 {
find_text(source, ")", middle + 2)
} else {
-1
}
if middle > pos && close > middle {
let alt = source[pos + 2:middle].to_owned()
let src = source[middle + 2:close].to_owned()
out.write_string("
")
pos = close + 1
continue
}
}
if source.code_unit_at(pos).to_int() == '['.to_int() {
let middle = find_text(source, "](", pos + 1)
let close = if middle >= 0 {
find_text(source, ")", middle + 2)
} else {
-1
}
if middle > pos && close > middle {
let label = source[pos + 1:middle].to_owned()
let href = source[middle + 2:close].to_owned()
out.write_string("")
out.write_string(inline_markdown(label))
out.write_string("")
pos = close + 1
continue
}
}
if source.code_unit_at(pos).to_int() == '*'.to_int() {
let close = find_text(source, "*", pos + 1)
if close > pos + 1 {
out.write_string("")
out.write_string(inline_markdown(source[pos + 1:close].to_owned()))
out.write_string("")
pos = close + 1
continue
}
}
if source.code_unit_at(pos).to_int() == '<'.to_int() {
let close = find_text(source, ">", pos + 1)
if close > pos {
let url = source[pos + 1:close].to_owned()
if url.has_prefix("https://") || url.has_prefix("http://") {
out.write_string("")
out.write_string(escape_html(url))
out.write_string("")
pos = close + 1
continue
}
}
}
let ch = Int::unsafe_to_char(source.code_unit_at(pos).to_int())
match ch {
'&' => out.write_string("&")
'<' => out.write_string("<")
'>' => out.write_string(">")
_ => out.write_char(ch)
}
pos += 1
}
out.to_string()
}
///|
fn ordered_item(line : String) -> String? {
let mut pos = 0
while pos < line.length() {
let ch = Int::unsafe_to_char(line.code_unit_at(pos).to_int())
if ch < '0' || ch > '9' {
break
}
pos += 1
}
if pos > 0 &&
pos + 1 < line.length() &&
line.code_unit_at(pos).to_int() == '.'.to_int() &&
line.code_unit_at(pos + 1).to_int() == ' '.to_int() {
Some(line[pos + 2:].to_owned())
} else {
None
}
}
///|
fn split_cells(line : String) -> Array[String] {
let cells : Array[String] = []
let clean = line.trim().trim(chars="|").to_owned()
for cell in clean.split("|") {
cells.push(cell.trim().to_owned())
}
cells
}
///|
fn is_table_rule(line : String) -> Bool {
let cells = split_cells(line)
if cells.is_empty() {
return false
}
for cell in cells {
let clean = cell.trim(chars=" :-").to_owned()
if !clean.is_empty() || !cell.contains("-") {
return false
}
}
true
}
///|
fn render_table_row(out : StringBuilder, line : String, header : Bool) -> Unit {
let tag = if header { "th" } else { "td" }
out.write_string("")
for cell in split_cells(line) {
out.write_string("<\{tag}>")
out.write_string(inline_markdown(cell))
out.write_string("\{tag}>")
}
out.write_string(" \n")
}
///|
/// Render a safe, practical Markdown subset without emitting raw HTML.
pub fn render_markdown(source : String) -> String {
let lines : Array[String] = []
for line in source.split("\n") {
lines.push(line.to_owned())
}
let out = StringBuilder::new(size_hint=source.length() + 256)
let mut i = 0
let mut in_code = false
let mut code_language = ""
let mut in_list = false
let mut ordered_list = false
while i < lines.length() {
let line = lines[i]
let trimmed = line.trim().to_owned()
if trimmed.has_prefix("```") {
if in_code {
out.write_string(" \n")
in_code = false
} else {
if in_list {
out.write_string(if ordered_list { "\n" } else { "\n" })
in_list = false
}
code_language = trimmed[3:].trim().to_owned()
out.write_string("")
in_code = true
}
i += 1
continue
}
if in_code {
out.write_string(escape_html(line))
out.write_char('\n')
i += 1
continue
}
if i + 1 < lines.length() &&
trimmed.contains("|") &&
is_table_rule(lines[i + 1]) {
if in_list {
out.write_string(if ordered_list { "\n" } else { "\n" })
in_list = false
}
out.write_string("")
render_table_row(out, line, true)
out.write_string("\n")
i += 2
while i < lines.length() &&
lines[i].contains("|") &&
!lines[i].trim().is_empty() {
render_table_row(out, lines[i], false)
i += 1
}
out.write_string("
\n")
continue
}
let numbered = ordered_item(trimmed)
let unordered = trimmed.has_prefix("- ") || trimmed.has_prefix("* ")
if unordered || numbered is Some(_) {
let wants_ordered = numbered is Some(_)
if in_list && wants_ordered != ordered_list {
out.write_string(if ordered_list { "\n" } else { "\n" })
in_list = false
}
if !in_list {
ordered_list = wants_ordered
out.write_string(if ordered_list { "\n" } else { "\n" })
in_list = true
}
let mut item = match numbered {
Some(content) => content
None => trimmed[2:].to_owned()
}
let mut checkbox = ""
if item.has_prefix("[ ] ") {
checkbox = " "
item = item[4:].to_owned()
} else if item.has_prefix("[x] ") || item.has_prefix("[X] ") {
checkbox = " "
item = item[4:].to_owned()
}
out.write_string("- \{checkbox}\{inline_markdown(item)}
\n")
i += 1
continue
}
if in_list {
out.write_string(if ordered_list { "
\n" } else { "\n" })
in_list = false
}
if trimmed.is_empty() {
i += 1
continue
}
if trimmed.has_prefix("#") {
let mut level = 0
while level < trimmed.length() &&
level < 6 &&
trimmed.code_unit_at(level).to_int() == 35 {
level += 1
}
let title = trimmed[level:].trim().to_owned()
let slug = slugify(title)
out.write_string(
"#\{inline_markdown(title)} \n",
)
} else if trimmed.has_prefix("> [!") && trimmed.has_suffix("]") {
let kind = trimmed[4:trimmed.length() - 1].to_lower().to_owned()
let title = kind.to_upper()
out.write_string(
"\n")
continue
} else if trimmed.has_prefix("> ") {
out.write_string(
"\{inline_markdown(trimmed[2:].to_owned())}
\n",
)
} else if trimmed == "---" || trimmed == "***" {
out.write_string("
\n")
} else {
out.write_string("\{inline_markdown(trimmed)}
\n")
}
i += 1
}
if in_code {
out.write_string("\n")
}
if in_list {
out.write_string(if ordered_list { "\n" } else { "\n" })
}
out.to_string()
}
///|
fn plain_text(markdown : String) -> String {
let out = StringBuilder::new()
let mut space = false
for i in 0..