" + inner + "")
},
None => processInlineFrom(text, i + 1, len, acc + ch)
}
// [text](url)
else if ch == "[" then
match findClosing(text, i + 1, len, "](") {
Some(end) => {
let linkText = String.substring(text, i + 1, end);
match findClosing(text, end + 2, len, ")") {
Some(urlEnd) => {
let url = String.substring(text, end + 2, urlEnd);
processInlineFrom(text, urlEnd + 1, len, acc + "" + processInline(linkText) + "")
},
None => processInlineFrom(text, i + 1, len, acc + ch)
}
},
None => processInlineFrom(text, i + 1, len, acc + ch)
}
//  — images
else if ch == "!" then
if i + 1 < len then
if String.substring(text, i + 1, i + 2) == "[" then
match findClosing(text, i + 2, len, "](") {
Some(end) => {
let alt = String.substring(text, i + 2, end);
match findClosing(text, end + 2, len, ")") {
Some(urlEnd) => {
let url = String.substring(text, end + 2, urlEnd);
processInlineFrom(text, urlEnd + 1, len, acc + "" + processInline(String.trim(para)) + "
\n" // Flush accumulated blockquote fn flushBq(html: String, bqLines: String): String = if bqLines == "" then html else html + "\n" + convert(bqLines) + "\n" // Flush accumulated list fn flushList(html: String, listItems: String, ordered: Bool): String = if listItems == "" then html else { let tag = if ordered then "ol" else "ul"; html + "<" + tag + ">\n" + listItems + "" + tag + ">\n" } // Parse markdown blocks from text pub fn parseBlocks(text: String): String = { let lines = String.lines(text); let init = BlockState("", "", false, "", "", false, "", false, "", false); let final = List.fold(lines, init, fn(state: BlockState, line: String): BlockState => { let html = bsHtml(state); let para = bsPara(state); let inCode = bsInCode(state); let codeLang = bsCodeLang(state); let codeLines = bsCodeLines(state); let inBq = bsInBq(state); let bqLines = bsBqLines(state); let inList = bsInList(state); let listItems = bsListItems(state); let ordered = bsOrdered(state); // Inside code block if inCode then if String.startsWith(line, "```") then { // End code block let codeHtml = if codeLang == "" then "
" + codeLines + "\n"
else
"" + codeLines + "\n";
BlockState(html + codeHtml, "", false, "", "", false, "", false, "", false)
} else
BlockState(html, para, true, codeLang, codeLines + escapeHtmlCode(line) + "\n", false, "", false, "", false)
// Start code block
else if String.startsWith(line, "```") then {
let h2 = flushPara(html, para);
let h3 = flushBq(h2, bqLines);
let h4 = flushList(h3, listItems, ordered);
let lang = String.trim(String.substring(line, 3, String.length(line)));
BlockState(h4, "", true, lang, "", false, "", false, "", false)
}
// Blockquote line
else if String.startsWith(line, "> ") then {
let h2 = flushPara(html, para);
let h3 = flushList(h2, listItems, ordered);
let bqContent = String.substring(line, 2, String.length(line));
BlockState(h3, "", false, "", "", true, bqLines + bqContent + "\n", false, "", false)
}
else if String.trim(line) == ">" then {
// Empty blockquote continuation
let h2 = flushPara(html, para);
let h3 = flushList(h2, listItems, ordered);
BlockState(h3, "", false, "", "", true, bqLines + "\n", false, "", false)
}
// End of blockquote (non-bq line after bq lines)
else if inBq then {
let h2 = flushBq(html, bqLines);
// Re-process this line
processLine(BlockState(h2, para, false, "", "", false, "", inList, listItems, ordered), line)
}
// Unordered list item
else if String.startsWith(line, "- ") then {
let h2 = flushPara(html, para);
let item = String.substring(line, 2, String.length(line));
BlockState(h2, "", false, "", "", false, "", true, listItems + "" + processInline(trimmed) + "
\n", "", false, "", "", false, "", false, "", false) } // Continuation of list (indented or sub-item) else if inList then if String.startsWith(line, " ") then { // Indented content under a list item — append to last item BlockState(html, para, false, "", "", false, "", true, listItems, ordered) } else { // Not a list item — flush list, treat as paragraph let h2 = flushList(html, listItems, ordered); BlockState(h2, para + trimmed + " ", false, "", "", false, "", false, "", false) } // Regular text — accumulate into paragraph else BlockState(html, para + trimmed + " ", false, "", "", false, "", false, "", false) } fn isOrderedListItem(line: String): Bool = { let trimmed = String.trim(line); if String.length(trimmed) < 3 then false else { let first = String.substring(trimmed, 0, 1); let isDigit = first == "0" || first == "1" || first == "2" || first == "3" || first == "4" || first == "5" || first == "6" || first == "7" || first == "8" || first == "9"; if isDigit then String.contains(trimmed, ". ") else false } } // Escape HTML special chars in code blocks (no inline processing) fn escapeHtmlCode(s: String): String = String.replace(String.replace(String.replace(s, "&", "&"), "<", "<"), ">", ">") // === Main convert function === // Convert full markdown text to HTML pub fn convert(markdown: String): String = parseBlocks(markdown)