Add frontmatter, markdown, path, xml, rss, and web packages
Sync local packages into the registry repo and update index.json and README.md to include all 9 packages.
This commit is contained in:
2
packages/markdown/.gitignore
vendored
Normal file
2
packages/markdown/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
test
|
||||
.lux_packages/
|
||||
82
packages/markdown/README.md
Normal file
82
packages/markdown/README.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# markdown
|
||||
|
||||
A Markdown to HTML converter for [Lux](https://github.com/thebrandonlucas/lux).
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
lux pkg add markdown --git https://git.qrty.ink/blu/markdown
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```lux
|
||||
import markdown
|
||||
|
||||
fn main(): Unit with {Console} = {
|
||||
let html = markdown.toHtml("# Hello **world**")
|
||||
Console.print(html)
|
||||
}
|
||||
|
||||
let _ = run main() with {}
|
||||
```
|
||||
|
||||
## API
|
||||
|
||||
### `toHtml(markdown: String): String`
|
||||
|
||||
Convert a full markdown document to HTML.
|
||||
|
||||
### `inlineToHtml(text: String): String`
|
||||
|
||||
Convert inline markdown only (bold, italic, links, etc.) without block-level processing.
|
||||
|
||||
### `escapeHtml(s: String): String`
|
||||
|
||||
Escape HTML entities (`&`, `<`, `>`).
|
||||
|
||||
## Supported Markdown
|
||||
|
||||
### Block elements
|
||||
- Headings (`# h1` through `#### h4`)
|
||||
- Paragraphs (auto-wrapped in `<p>`)
|
||||
- Fenced code blocks (` ``` ` with optional language)
|
||||
- Blockquotes (`> text`)
|
||||
- Unordered lists (`- item`)
|
||||
- Ordered lists (`1. item`)
|
||||
- Horizontal rules (`---`, `***`, `___`)
|
||||
- Images on their own line (``)
|
||||
- Raw HTML pass-through (lines starting with `<`)
|
||||
|
||||
### Inline elements
|
||||
- **Bold** (`**text**`)
|
||||
- *Italic* (`*text*` or `_text_`)
|
||||
- ~~Strikethrough~~ (`~~text~~`)
|
||||
- `Code` (`` `code` ``)
|
||||
- [Links](url) (`[text](url)`)
|
||||
- Images (``)
|
||||
- Raw HTML tags pass through
|
||||
|
||||
### Special features
|
||||
- Headings inside list items (`- ### Title` renders correctly)
|
||||
- Nested inline formatting (`**[bold link](url)**`)
|
||||
- Code blocks with syntax highlighting class (`language-*`)
|
||||
- Recursive blockquote content processing
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
lux test.lux
|
||||
```
|
||||
|
||||
## Known Limitations
|
||||
|
||||
- No nested lists (indented sub-items)
|
||||
- No reference-style links (`[text][ref]`)
|
||||
- No tables
|
||||
- No task lists (`- [ ] item`)
|
||||
- The C backend does not support module imports; use the interpreter (`lux`) or include the source directly for compiled binaries
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
374
packages/markdown/lib.lux
Normal file
374
packages/markdown/lib.lux
Normal file
@@ -0,0 +1,374 @@
|
||||
// markdown - A Markdown to HTML converter for Lux
|
||||
//
|
||||
// Public API:
|
||||
// toHtml(markdown: String): String - Convert full markdown document to HTML
|
||||
// inlineToHtml(text: String): String - Convert inline markdown only
|
||||
// escapeHtml(s: String): String - Escape HTML entities
|
||||
|
||||
// Block parser state: tracks where we are while folding over lines
|
||||
// Fields: html, para, inCode, codeLang, codeLines, inBq, bqLines, inList, listItems, ordered
|
||||
type BState =
|
||||
| BState(String, String, Bool, String, String, Bool, String, Bool, String, Bool)
|
||||
|
||||
// --- BState accessors ---
|
||||
|
||||
fn bsHtml(s: BState): String =
|
||||
match s { BState(h, _, _, _, _, _, _, _, _, _) => h }
|
||||
|
||||
fn bsPara(s: BState): String =
|
||||
match s { BState(_, p, _, _, _, _, _, _, _, _) => p }
|
||||
|
||||
fn bsInCode(s: BState): Bool =
|
||||
match s { BState(_, _, c, _, _, _, _, _, _, _) => c }
|
||||
|
||||
fn bsCodeLang(s: BState): String =
|
||||
match s { BState(_, _, _, l, _, _, _, _, _, _) => l }
|
||||
|
||||
fn bsCodeLines(s: BState): String =
|
||||
match s { BState(_, _, _, _, cl, _, _, _, _, _) => cl }
|
||||
|
||||
fn bsInBq(s: BState): Bool =
|
||||
match s { BState(_, _, _, _, _, bq, _, _, _, _) => bq }
|
||||
|
||||
fn bsBqLines(s: BState): String =
|
||||
match s { BState(_, _, _, _, _, _, bl, _, _, _) => bl }
|
||||
|
||||
fn bsInList(s: BState): Bool =
|
||||
match s { BState(_, _, _, _, _, _, _, il, _, _) => il }
|
||||
|
||||
fn bsListItems(s: BState): String =
|
||||
match s { BState(_, _, _, _, _, _, _, _, li, _) => li }
|
||||
|
||||
fn bsOrdered(s: BState): Bool =
|
||||
match s { BState(_, _, _, _, _, _, _, _, _, o) => o }
|
||||
|
||||
// --- HTML escaping ---
|
||||
|
||||
pub fn escapeHtml(s: String): String =
|
||||
String.replace(String.replace(String.replace(s, "&", "&"), "<", "<"), ">", ">")
|
||||
|
||||
// --- Delimiter finding ---
|
||||
|
||||
fn findClosingFrom(text: String, i: Int, len: Int, delim: String, delimLen: Int): Option<Int> =
|
||||
if i + delimLen > len then None
|
||||
else if String.substring(text, i, i + delimLen) == delim then Some(i)
|
||||
else findClosingFrom(text, i + 1, len, delim, delimLen)
|
||||
|
||||
fn findClosing(text: String, start: Int, len: Int, delim: String): Option<Int> =
|
||||
findClosingFrom(text, start, len, delim, String.length(delim))
|
||||
|
||||
// --- Character classification for HTML pass-through ---
|
||||
|
||||
fn isLetterOrSlash(ch: String): Bool =
|
||||
ch == "/" || ch == "!" ||
|
||||
ch == "a" || ch == "b" || ch == "c" || ch == "d" || ch == "e" ||
|
||||
ch == "f" || ch == "g" || ch == "h" || ch == "i" || ch == "j" ||
|
||||
ch == "k" || ch == "l" || ch == "m" || ch == "n" || ch == "o" ||
|
||||
ch == "p" || ch == "q" || ch == "r" || ch == "s" || ch == "t" ||
|
||||
ch == "u" || ch == "v" || ch == "w" || ch == "x" || ch == "y" || ch == "z" ||
|
||||
ch == "A" || ch == "B" || ch == "C" || ch == "D" || ch == "E" ||
|
||||
ch == "F" || ch == "G" || ch == "H" || ch == "I" || ch == "J" ||
|
||||
ch == "K" || ch == "L" || ch == "M" || ch == "N" || ch == "O" ||
|
||||
ch == "P" || ch == "Q" || ch == "R" || ch == "S" || ch == "T" ||
|
||||
ch == "U" || ch == "V" || ch == "W" || ch == "X" || ch == "Y" || ch == "Z"
|
||||
|
||||
// --- Inline markdown processing ---
|
||||
// Handles: **bold**, *italic*, _italic_, `code`, ~~strikethrough~~,
|
||||
// [links](url), , <html> pass-through
|
||||
|
||||
fn processInlineFrom(text: String, i: Int, len: Int, acc: String): String = {
|
||||
if i >= len then acc
|
||||
else {
|
||||
let ch = String.substring(text, i, i + 1)
|
||||
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) => processInlineFrom(text, end + 2, len, acc + "<strong>" + processInline(String.substring(text, i + 2, end)) + "</strong>"),
|
||||
None => processInlineFrom(text, i + 1, len, acc + ch),
|
||||
}
|
||||
else
|
||||
match findClosing(text, i + 1, len, "*") {
|
||||
Some(end) => processInlineFrom(text, end + 1, len, acc + "<em>" + processInline(String.substring(text, i + 1, end)) + "</em>"),
|
||||
None => processInlineFrom(text, i + 1, len, acc + ch),
|
||||
}
|
||||
else acc + ch
|
||||
else if ch == "_" then
|
||||
if i + 1 < len then
|
||||
match findClosing(text, i + 1, len, "_") {
|
||||
Some(end) => processInlineFrom(text, end + 1, len, acc + "<em>" + processInline(String.substring(text, i + 1, end)) + "</em>"),
|
||||
None => processInlineFrom(text, i + 1, len, acc + ch),
|
||||
}
|
||||
else acc + ch
|
||||
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) => processInlineFrom(text, end + 2, len, acc + "<del>" + processInline(String.substring(text, i + 2, end)) + "</del>"),
|
||||
None => processInlineFrom(text, i + 1, len, acc + ch),
|
||||
}
|
||||
else processInlineFrom(text, i + 1, len, acc + ch)
|
||||
else acc + ch
|
||||
else if ch == "`" then
|
||||
match findClosing(text, i + 1, len, "`") {
|
||||
Some(end) => processInlineFrom(text, end + 1, len, acc + "<code>" + escapeHtml(String.substring(text, i + 1, end)) + "</code>"),
|
||||
None => processInlineFrom(text, i + 1, len, acc + ch),
|
||||
}
|
||||
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) => match findClosing(text, end + 2, len, ")") {
|
||||
Some(urlEnd) => {
|
||||
let alt = String.substring(text, i + 2, end)
|
||||
let src = String.substring(text, end + 2, urlEnd)
|
||||
let imgTag = acc + "<img src=\"" + src + "\" alt=\"" + alt + "\">"
|
||||
processInlineFrom(text, urlEnd + 1, len, imgTag)
|
||||
},
|
||||
None => processInlineFrom(text, i + 1, len, acc + ch),
|
||||
},
|
||||
None => processInlineFrom(text, i + 1, len, acc + ch),
|
||||
}
|
||||
else processInlineFrom(text, i + 1, len, acc + ch)
|
||||
else acc + ch
|
||||
else if ch == "[" then
|
||||
match findClosing(text, i + 1, len, "](") {
|
||||
Some(end) => match findClosing(text, end + 2, len, ")") {
|
||||
Some(urlEnd) => {
|
||||
let linkText = String.substring(text, i + 1, end)
|
||||
let href = String.substring(text, end + 2, urlEnd)
|
||||
let linkTag = acc + "<a href=\"" + href + "\">" + processInline(linkText) + "</a>"
|
||||
processInlineFrom(text, urlEnd + 1, len, linkTag)
|
||||
},
|
||||
None => processInlineFrom(text, i + 1, len, acc + ch),
|
||||
},
|
||||
None => processInlineFrom(text, i + 1, len, acc + ch),
|
||||
}
|
||||
else if ch == "<" then
|
||||
if i + 1 < len then
|
||||
if isLetterOrSlash(String.substring(text, i + 1, i + 2)) then
|
||||
match findClosing(text, i + 1, len, ">") {
|
||||
Some(end) => processInlineFrom(text, end + 1, len, acc + String.substring(text, i, end + 1)),
|
||||
None => processInlineFrom(text, i + 1, len, acc + "<"),
|
||||
}
|
||||
else processInlineFrom(text, i + 1, len, acc + "<")
|
||||
else acc + "<"
|
||||
else if ch == ">" then
|
||||
processInlineFrom(text, i + 1, len, acc + ">")
|
||||
else
|
||||
processInlineFrom(text, i + 1, len, acc + ch)
|
||||
}
|
||||
}
|
||||
|
||||
fn processInline(text: String): String =
|
||||
processInlineFrom(text, 0, String.length(text), "")
|
||||
|
||||
// --- List item processing ---
|
||||
// Handles heading prefixes inside list items: "- ### Title" renders as <li><h3>Title</h3></li>
|
||||
|
||||
fn processListItemContent(content: String): String = {
|
||||
let trimmed = String.trim(content)
|
||||
if String.startsWith(trimmed, "#### ") then
|
||||
"<h4>" + processInline(String.substring(trimmed, 5, String.length(trimmed))) + "</h4>"
|
||||
else if String.startsWith(trimmed, "### ") then
|
||||
"<h3>" + processInline(String.substring(trimmed, 4, String.length(trimmed))) + "</h3>"
|
||||
else if String.startsWith(trimmed, "## ") then
|
||||
"<h2>" + processInline(String.substring(trimmed, 3, String.length(trimmed))) + "</h2>"
|
||||
else if String.startsWith(trimmed, "# ") then
|
||||
"<h1>" + processInline(String.substring(trimmed, 2, String.length(trimmed))) + "</h1>"
|
||||
else
|
||||
processInline(trimmed)
|
||||
}
|
||||
|
||||
// --- Block-level flush helpers ---
|
||||
|
||||
fn flushPara(html: String, para: String): String =
|
||||
if para == "" then html
|
||||
else html + "<p>" + processInline(String.trim(para)) + "</p>
|
||||
"
|
||||
|
||||
fn flushBq(html: String, bqLines: String): String =
|
||||
if bqLines == "" then html
|
||||
else html + "<blockquote>
|
||||
" + parseBlocks(bqLines) + "</blockquote>
|
||||
"
|
||||
|
||||
fn flushList(html: String, listItems: String, ordered: Bool): String =
|
||||
if listItems == "" then html
|
||||
else {
|
||||
let tag = if ordered then "ol" else "ul"
|
||||
html + "<" + tag + ">
|
||||
" + listItems + "</" + tag + ">
|
||||
"
|
||||
}
|
||||
|
||||
// --- Ordered list detection ---
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// --- Non-list block line processing ---
|
||||
// Called when a line doesn't match code, blockquote, or list patterns
|
||||
|
||||
fn processBlockLine(html: String, para: String, inList: Bool, listItems: String, ordered: Bool, line: String): BState = {
|
||||
let trimmed = String.trim(line)
|
||||
if trimmed == "" then {
|
||||
let h2 = flushPara(html, para)
|
||||
let h3 = flushList(h2, listItems, ordered)
|
||||
BState(h3, "", false, "", "", false, "", false, "", false)
|
||||
}
|
||||
else if String.startsWith(trimmed, "#### ") then {
|
||||
let h2 = flushPara(html, para)
|
||||
let h3 = flushList(h2, listItems, ordered)
|
||||
BState(h3 + "<h4>" + processInline(String.substring(trimmed, 5, String.length(trimmed))) + "</h4>
|
||||
", "", false, "", "", false, "", false, "", false)
|
||||
}
|
||||
else if String.startsWith(trimmed, "### ") then {
|
||||
let h2 = flushPara(html, para)
|
||||
let h3 = flushList(h2, listItems, ordered)
|
||||
BState(h3 + "<h3>" + processInline(String.substring(trimmed, 4, String.length(trimmed))) + "</h3>
|
||||
", "", false, "", "", false, "", false, "", false)
|
||||
}
|
||||
else if String.startsWith(trimmed, "## ") then {
|
||||
let h2 = flushPara(html, para)
|
||||
let h3 = flushList(h2, listItems, ordered)
|
||||
BState(h3 + "<h2>" + processInline(String.substring(trimmed, 3, String.length(trimmed))) + "</h2>
|
||||
", "", false, "", "", false, "", false, "", false)
|
||||
}
|
||||
else if String.startsWith(trimmed, "# ") then {
|
||||
let h2 = flushPara(html, para)
|
||||
let h3 = flushList(h2, listItems, ordered)
|
||||
BState(h3 + "<h1>" + processInline(String.substring(trimmed, 2, String.length(trimmed))) + "</h1>
|
||||
", "", false, "", "", false, "", false, "", false)
|
||||
}
|
||||
else if trimmed == "---" || trimmed == "***" || trimmed == "___" then {
|
||||
let h2 = flushPara(html, para)
|
||||
let h3 = flushList(h2, listItems, ordered)
|
||||
BState(h3 + "<hr>
|
||||
", "", false, "", "", false, "", false, "", false)
|
||||
}
|
||||
else if String.startsWith(trimmed, "<") then {
|
||||
let h2 = flushPara(html, para)
|
||||
let h3 = flushList(h2, listItems, ordered)
|
||||
BState(h3 + trimmed + "
|
||||
", "", false, "", "", false, "", false, "", false)
|
||||
}
|
||||
else if String.startsWith(trimmed, "![") then {
|
||||
let h2 = flushPara(html, para)
|
||||
let h3 = flushList(h2, listItems, ordered)
|
||||
BState(h3 + "<p>" + processInline(trimmed) + "</p>
|
||||
", "", false, "", "", false, "", false, "", false)
|
||||
}
|
||||
else if inList then {
|
||||
let h2 = flushList(html, listItems, ordered)
|
||||
BState(h2, para + trimmed + " ", false, "", "", false, "", false, "", false)
|
||||
}
|
||||
else
|
||||
BState(html, para + trimmed + " ", false, "", "", false, "", false, "", false)
|
||||
}
|
||||
|
||||
// --- Main block fold function ---
|
||||
// Processes one line at a time, maintaining state across the fold
|
||||
|
||||
fn blockFoldLine(state: BState, line: String): BState = {
|
||||
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 a fenced code block
|
||||
if inCode then
|
||||
if String.startsWith(line, "```") then {
|
||||
let codeHtml = if codeLang == "" then
|
||||
"<pre><code>" + codeLines + "</code></pre>
|
||||
"
|
||||
else
|
||||
"<pre><code class=\"language-" + codeLang + "\">" + codeLines + "</code></pre>
|
||||
"
|
||||
BState(html + codeHtml, "", false, "", "", false, "", false, "", false)
|
||||
}
|
||||
else
|
||||
BState(html, para, true, codeLang, codeLines + escapeHtml(line) + "
|
||||
", false, "", false, "", false)
|
||||
// Opening a fenced 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)))
|
||||
BState(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))
|
||||
BState(h3, "", false, "", "", true, bqLines + bqContent + "
|
||||
", false, "", false)
|
||||
}
|
||||
// Empty blockquote continuation
|
||||
else if String.trim(line) == ">" then {
|
||||
let h2 = flushPara(html, para)
|
||||
let h3 = flushList(h2, listItems, ordered)
|
||||
BState(h3, "", false, "", "", true, bqLines + "
|
||||
", false, "", false)
|
||||
}
|
||||
// Exiting a blockquote
|
||||
else if inBq then {
|
||||
let h2 = flushBq(html, bqLines)
|
||||
processBlockLine(h2, para, 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))
|
||||
BState(h2, "", false, "", "", false, "", true, listItems + "<li>" + processListItemContent(item) + "</li>
|
||||
", false)
|
||||
}
|
||||
// Ordered list item
|
||||
else if isOrderedListItem(line) then {
|
||||
let h2 = flushPara(html, para)
|
||||
let dotIdx = match String.indexOf(line, ". ") {
|
||||
Some(idx) => idx,
|
||||
None => 0,
|
||||
}
|
||||
let item = String.substring(line, dotIdx + 2, String.length(line))
|
||||
BState(h2, "", false, "", "", false, "", true, listItems + "<li>" + processListItemContent(item) + "</li>
|
||||
", true)
|
||||
}
|
||||
// Everything else (headings, paragraphs, hr, html, images)
|
||||
else
|
||||
processBlockLine(html, para, inList, listItems, ordered, line)
|
||||
}
|
||||
|
||||
// --- Core block parser ---
|
||||
|
||||
fn parseBlocks(text: String): String = {
|
||||
let lines = String.lines(text)
|
||||
let init = BState("", "", false, "", "", false, "", false, "", false)
|
||||
let final = List.fold(lines, init, blockFoldLine)
|
||||
let h = flushPara(bsHtml(final), bsPara(final))
|
||||
let h2 = flushBq(h, bsBqLines(final))
|
||||
flushList(h2, bsListItems(final), bsOrdered(final))
|
||||
}
|
||||
|
||||
// --- Public API ---
|
||||
|
||||
// Convert a full markdown document to HTML
|
||||
pub fn toHtml(markdown: String): String = parseBlocks(markdown)
|
||||
|
||||
// Convert inline markdown only (no block-level elements)
|
||||
pub fn inlineToHtml(text: String): String = processInline(text)
|
||||
8
packages/markdown/lux.toml
Normal file
8
packages/markdown/lux.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
[project]
|
||||
name = "markdown"
|
||||
version = "0.1.0"
|
||||
description = "Markdown to HTML converter for Lux"
|
||||
authors = ["Brandon Lucas"]
|
||||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
92
packages/markdown/test_integration.lux
Normal file
92
packages/markdown/test_integration.lux
Normal file
@@ -0,0 +1,92 @@
|
||||
import lib
|
||||
|
||||
// Integration tests: converting complex markdown documents
|
||||
|
||||
// Full blog post with mixed content
|
||||
fn test_full_blog_post(): Unit with {Test} = {
|
||||
let md = "# Welcome to My Blog\n\nThis is my **first post** about *Lux*.\n\n## Getting Started\n\nHere's some code:\n\n```lux\nfn main(): Unit = Console.print(\"hello\")\n```\n\nCheck out [the docs](https://example.com) for more.\n\n---\n\nThanks for reading!"
|
||||
let html = lib.toHtml(md)
|
||||
Test.assert(String.contains(html, "<h1>Welcome to My Blog</h1>"), "has h1")
|
||||
Test.assert(String.contains(html, "<strong>first post</strong>"), "has bold")
|
||||
Test.assert(String.contains(html, "<em>Lux</em>"), "has italic")
|
||||
Test.assert(String.contains(html, "<h2>Getting Started</h2>"), "has h2")
|
||||
Test.assert(String.contains(html, "<pre><code class=\"language-lux\">"), "has code block with lang")
|
||||
Test.assert(String.contains(html, "<a href=\"https://example.com\">the docs</a>"), "has link")
|
||||
Test.assert(String.contains(html, "<hr>"), "has horizontal rule")
|
||||
Test.assert(String.contains(html, "Thanks for reading!"), "has closing paragraph")
|
||||
}
|
||||
|
||||
// Document with various list types
|
||||
fn test_mixed_lists(): Unit with {Test} = {
|
||||
let md = "Shopping list:\n\n- Apples\n- Bananas\n- Cherries\n\nSteps:\n\n1. Preheat oven\n2. Mix ingredients\n3. Bake for 30 min"
|
||||
let html = lib.toHtml(md)
|
||||
Test.assert(String.contains(html, "<ul>"), "has unordered list")
|
||||
Test.assert(String.contains(html, "<ol>"), "has ordered list")
|
||||
Test.assert(String.contains(html, "<li>Apples</li>"), "has UL item")
|
||||
Test.assert(String.contains(html, "<li>Preheat oven</li>"), "has OL item")
|
||||
}
|
||||
|
||||
// Nested inline formatting
|
||||
fn test_nested_inline(): Unit with {Test} = {
|
||||
let result = lib.inlineToHtml("**bold and *italic* inside**")
|
||||
Test.assert(String.contains(result, "<strong>"), "has strong tag")
|
||||
Test.assert(String.contains(result, "<em>italic</em>"), "has nested italic")
|
||||
}
|
||||
|
||||
// Multiple paragraphs
|
||||
fn test_multiple_paragraphs(): Unit with {Test} = {
|
||||
let md = "First paragraph.\n\nSecond paragraph.\n\nThird paragraph."
|
||||
let html = lib.toHtml(md)
|
||||
Test.assert(String.contains(html, "<p>First paragraph.</p>"), "first para")
|
||||
Test.assert(String.contains(html, "<p>Second paragraph.</p>"), "second para")
|
||||
Test.assert(String.contains(html, "<p>Third paragraph.</p>"), "third para")
|
||||
}
|
||||
|
||||
// Code block preserves content exactly
|
||||
fn test_code_block_preserves_content(): Unit with {Test} = {
|
||||
let md = "```html\n<div>\n <p>Hello & world</p>\n</div>\n```"
|
||||
let html = lib.toHtml(md)
|
||||
Test.assert(String.contains(html, "language-html"), "has language class")
|
||||
Test.assert(String.contains(html, "<div>"), "HTML tags in code are escaped")
|
||||
Test.assert(String.contains(html, "& world"), "ampersand in code is escaped")
|
||||
}
|
||||
|
||||
// Heading with various inline elements
|
||||
fn test_heading_with_link(): Unit with {Test} = {
|
||||
let md = "## [Lux](https://example.com) Language"
|
||||
let html = lib.toHtml(md)
|
||||
Test.assert(String.contains(html, "<h2>"), "has h2")
|
||||
Test.assert(String.contains(html, "<a href=\"https://example.com\">Lux</a>"), "has link in heading")
|
||||
}
|
||||
|
||||
// Blockquote with inline formatting
|
||||
fn test_blockquote_with_formatting(): Unit with {Test} = {
|
||||
let md = "> This is a **bold** quote"
|
||||
let html = lib.toHtml(md)
|
||||
Test.assert(String.contains(html, "<blockquote>"), "has blockquote")
|
||||
Test.assert(String.contains(html, "<strong>bold</strong>"), "has bold in blockquote")
|
||||
}
|
||||
|
||||
// Document starting with code block
|
||||
fn test_leading_code_block(): Unit with {Test} = {
|
||||
let md = "```\ncode first\n```\n\nThen text."
|
||||
let html = lib.toHtml(md)
|
||||
Test.assert(String.contains(html, "<pre><code>code first"), "code block first")
|
||||
Test.assert(String.contains(html, "<p>Then text.</p>"), "paragraph after code")
|
||||
}
|
||||
|
||||
// Images in context
|
||||
fn test_image_in_paragraph(): Unit with {Test} = {
|
||||
let md = "Here is a photo:\n\n\n\nBeautiful, right?"
|
||||
let html = lib.toHtml(md)
|
||||
Test.assert(String.contains(html, "<img src=\"sunset.jpg\" alt=\"Sunset\">"), "has image")
|
||||
}
|
||||
|
||||
// List items with headings
|
||||
fn test_list_with_headings(): Unit with {Test} = {
|
||||
let md = "- ## Chapter 1\n- ## Chapter 2\n- ## Chapter 3"
|
||||
let html = lib.toHtml(md)
|
||||
Test.assert(String.contains(html, "<li><h2>Chapter 1</h2></li>"), "heading in list item 1")
|
||||
Test.assert(String.contains(html, "<li><h2>Chapter 2</h2></li>"), "heading in list item 2")
|
||||
Test.assert(String.contains(html, "<li><h2>Chapter 3</h2></li>"), "heading in list item 3")
|
||||
}
|
||||
64
packages/markdown/test_markdown.lux
Normal file
64
packages/markdown/test_markdown.lux
Normal file
@@ -0,0 +1,64 @@
|
||||
import lib
|
||||
|
||||
fn test_escape_html(): Unit with {Test} = {
|
||||
Test.assertEqualMsg("<div>", lib.escapeHtml("<div>"), "angle brackets")
|
||||
Test.assertEqualMsg("a & b", lib.escapeHtml("a & b"), "ampersand")
|
||||
Test.assertEqualMsg("hello", lib.escapeHtml("hello"), "no special chars")
|
||||
}
|
||||
|
||||
fn test_inline_formatting(): Unit with {Test} = {
|
||||
Test.assertEqualMsg("<strong>bold</strong>", lib.inlineToHtml("**bold**"), "bold")
|
||||
Test.assertEqualMsg("<em>italic</em>", lib.inlineToHtml("*italic*"), "italic with asterisk")
|
||||
Test.assertEqualMsg("<em>italic</em>", lib.inlineToHtml("_italic_"), "italic with underscore")
|
||||
Test.assertEqualMsg("<code>code</code>", lib.inlineToHtml("`code`"), "inline code")
|
||||
Test.assertEqualMsg("<del>strike</del>", lib.inlineToHtml("~~strike~~"), "strikethrough")
|
||||
Test.assertEqualMsg("plain text", lib.inlineToHtml("plain text"), "plain text unchanged")
|
||||
}
|
||||
|
||||
fn test_links(): Unit with {Test} = {
|
||||
Test.assertEqualMsg("<a href=\"https://example.com\">click</a>", lib.inlineToHtml("[click](https://example.com)"), "link")
|
||||
Test.assertEqualMsg("<img src=\"img.png\" alt=\"alt\">", lib.inlineToHtml(""), "image")
|
||||
Test.assertEqualMsg("<strong><a href=\"url\">bold link</a></strong>", lib.inlineToHtml("**[bold link](url)**"), "bold link")
|
||||
}
|
||||
|
||||
fn test_headings(): Unit with {Test} = {
|
||||
Test.assertEqualMsg("<h1>Hello</h1>\n", lib.toHtml("# Hello"), "h1")
|
||||
Test.assertEqualMsg("<h2>World</h2>\n", lib.toHtml("## World"), "h2")
|
||||
Test.assertEqualMsg("<h3>Sub</h3>\n", lib.toHtml("### Sub"), "h3")
|
||||
Test.assertEqualMsg("<h4>Deep</h4>\n", lib.toHtml("#### Deep"), "h4")
|
||||
}
|
||||
|
||||
fn test_paragraphs(): Unit with {Test} = {
|
||||
Test.assertEqualMsg("<p>hello world</p>\n", lib.toHtml("hello world"), "single line paragraph")
|
||||
}
|
||||
|
||||
fn test_code_blocks(): Unit with {Test} = {
|
||||
let result = lib.toHtml("```\nhello\n```")
|
||||
Test.assertEqualMsg("<pre><code>hello\n</code></pre>\n", result, "simple code block")
|
||||
}
|
||||
|
||||
fn test_lists(): Unit with {Test} = {
|
||||
Test.assertEqualMsg("<ul>\n<li>item one</li>\n<li>item two</li>\n</ul>\n", lib.toHtml("- item one\n- item two"), "unordered list")
|
||||
Test.assertEqualMsg("<ol>\n<li>first</li>\n<li>second</li>\n</ol>\n", lib.toHtml("1. first\n2. second"), "ordered list")
|
||||
}
|
||||
|
||||
fn test_heading_in_list(): Unit with {Test} = {
|
||||
Test.assertEqualMsg("<ul>\n<li><h3>Title</h3></li>\n</ul>\n", lib.toHtml("- ### Title"), "h3 inside list item")
|
||||
Test.assertEqualMsg("<ul>\n<li><h2>Big Title</h2></li>\n</ul>\n", lib.toHtml("- ## Big Title"), "h2 inside list item")
|
||||
Test.assertEqualMsg("<ul>\n<li><h3><a href=\"/url\">Link</a></h3></li>\n</ul>\n", lib.toHtml("- ### [Link](/url)"), "h3 with link inside list item")
|
||||
}
|
||||
|
||||
fn test_blockquotes(): Unit with {Test} = {
|
||||
Test.assertEqualMsg("<blockquote>\n<p>quoted text</p>\n</blockquote>\n", lib.toHtml("> quoted text"), "simple blockquote")
|
||||
}
|
||||
|
||||
fn test_horizontal_rule(): Unit with {Test} = {
|
||||
Test.assertEqualMsg("<hr>\n", lib.toHtml("---"), "hr with dashes")
|
||||
Test.assertEqualMsg("<hr>\n", lib.toHtml("***"), "hr with asterisks")
|
||||
Test.assertEqualMsg("<hr>\n", lib.toHtml("___"), "hr with underscores")
|
||||
}
|
||||
|
||||
fn test_html_passthrough(): Unit with {Test} = {
|
||||
Test.assertEqualMsg("<div class=\"foo\">bar</div>\n", lib.toHtml("<div class=\"foo\">bar</div>"), "html element passes through")
|
||||
Test.assertEqualMsg("<picture>\n<source srcset=\"img.avif\">\n</picture>\n", lib.toHtml("<picture>\n<source srcset=\"img.avif\">\n</picture>"), "multi-line html passthrough")
|
||||
}
|
||||
112
packages/markdown/test_snapshot.lux
Normal file
112
packages/markdown/test_snapshot.lux
Normal file
@@ -0,0 +1,112 @@
|
||||
import lib
|
||||
|
||||
// Snapshot tests: compare full markdown→HTML conversion against golden output
|
||||
|
||||
// Snapshot: simple document with heading and paragraph
|
||||
fn test_snapshot_heading_and_para(): Unit with {Test} = {
|
||||
let md = "# Hello World\n\nThis is a paragraph."
|
||||
let expected = "<h1>Hello World</h1>\n<p>This is a paragraph.</p>\n"
|
||||
Test.assertEqualMsg(expected, lib.toHtml(md), "snap: heading + paragraph")
|
||||
}
|
||||
|
||||
// Snapshot: all heading levels
|
||||
fn test_snapshot_all_headings(): Unit with {Test} = {
|
||||
Test.assertEqualMsg("<h1>H1</h1>\n", lib.toHtml("# H1"), "snap: h1")
|
||||
Test.assertEqualMsg("<h2>H2</h2>\n", lib.toHtml("## H2"), "snap: h2")
|
||||
Test.assertEqualMsg("<h3>H3</h3>\n", lib.toHtml("### H3"), "snap: h3")
|
||||
Test.assertEqualMsg("<h4>H4</h4>\n", lib.toHtml("#### H4"), "snap: h4")
|
||||
}
|
||||
|
||||
// Snapshot: inline formatting combinations
|
||||
fn test_snapshot_inline_combos(): Unit with {Test} = {
|
||||
Test.assertEqualMsg("<strong>bold</strong>", lib.inlineToHtml("**bold**"), "snap: bold")
|
||||
Test.assertEqualMsg("<em>italic</em>", lib.inlineToHtml("*italic*"), "snap: italic")
|
||||
Test.assertEqualMsg("<code>code</code>", lib.inlineToHtml("`code`"), "snap: code")
|
||||
Test.assertEqualMsg("<del>strike</del>", lib.inlineToHtml("~~strike~~"), "snap: strike")
|
||||
Test.assertEqualMsg("<a href=\"/\">link</a>", lib.inlineToHtml("[link](/)"), "snap: link")
|
||||
Test.assertEqualMsg("<img src=\"i.png\" alt=\"a\">", lib.inlineToHtml(""), "snap: image")
|
||||
}
|
||||
|
||||
// Snapshot: unordered list
|
||||
fn test_snapshot_unordered_list(): Unit with {Test} = {
|
||||
let md = "- Alpha\n- Beta\n- Gamma"
|
||||
let expected = "<ul>\n<li>Alpha</li>\n<li>Beta</li>\n<li>Gamma</li>\n</ul>\n"
|
||||
Test.assertEqualMsg(expected, lib.toHtml(md), "snap: unordered list")
|
||||
}
|
||||
|
||||
// Snapshot: ordered list
|
||||
fn test_snapshot_ordered_list(): Unit with {Test} = {
|
||||
let md = "1. First\n2. Second\n3. Third"
|
||||
let expected = "<ol>\n<li>First</li>\n<li>Second</li>\n<li>Third</li>\n</ol>\n"
|
||||
Test.assertEqualMsg(expected, lib.toHtml(md), "snap: ordered list")
|
||||
}
|
||||
|
||||
// Snapshot: code block with language
|
||||
fn test_snapshot_code_block(): Unit with {Test} = {
|
||||
let md = "```rust\nlet x = 1;\nlet y = 2;\n```"
|
||||
let expected = "<pre><code class=\"language-rust\">let x = 1;\nlet y = 2;\n</code></pre>\n"
|
||||
Test.assertEqualMsg(expected, lib.toHtml(md), "snap: code block with rust lang")
|
||||
}
|
||||
|
||||
// Snapshot: code block without language
|
||||
fn test_snapshot_code_block_no_lang(): Unit with {Test} = {
|
||||
let md = "```\nplain code\n```"
|
||||
let expected = "<pre><code>plain code\n</code></pre>\n"
|
||||
Test.assertEqualMsg(expected, lib.toHtml(md), "snap: code block no lang")
|
||||
}
|
||||
|
||||
// Snapshot: blockquote
|
||||
fn test_snapshot_blockquote(): Unit with {Test} = {
|
||||
let md = "> This is quoted text"
|
||||
let expected = "<blockquote>\n<p>This is quoted text</p>\n</blockquote>\n"
|
||||
Test.assertEqualMsg(expected, lib.toHtml(md), "snap: blockquote")
|
||||
}
|
||||
|
||||
// Snapshot: horizontal rules
|
||||
fn test_snapshot_horizontal_rules(): Unit with {Test} = {
|
||||
Test.assertEqualMsg("<hr>\n", lib.toHtml("---"), "snap: hr dashes")
|
||||
Test.assertEqualMsg("<hr>\n", lib.toHtml("***"), "snap: hr asterisks")
|
||||
Test.assertEqualMsg("<hr>\n", lib.toHtml("___"), "snap: hr underscores")
|
||||
}
|
||||
|
||||
// Snapshot: HTML passthrough
|
||||
fn test_snapshot_html_passthrough(): Unit with {Test} = {
|
||||
Test.assertEqualMsg("<div class=\"box\">content</div>\n", lib.toHtml("<div class=\"box\">content</div>"), "snap: div passthrough")
|
||||
Test.assertEqualMsg("<video src=\"v.mp4\"></video>\n", lib.toHtml("<video src=\"v.mp4\"></video>"), "snap: video passthrough")
|
||||
}
|
||||
|
||||
// Snapshot: heading with inline formatting inside list
|
||||
fn test_snapshot_list_with_headings(): Unit with {Test} = {
|
||||
let md = "- ### Chapter 1\n- ### Chapter 2"
|
||||
let expected = "<ul>\n<li><h3>Chapter 1</h3></li>\n<li><h3>Chapter 2</h3></li>\n</ul>\n"
|
||||
Test.assertEqualMsg(expected, lib.toHtml(md), "snap: list with headings")
|
||||
}
|
||||
|
||||
// Snapshot: complete blog post conversion
|
||||
fn test_snapshot_blog_post(): Unit with {Test} = {
|
||||
let md = "# My Post\n\nIntro paragraph with **bold** and *italic*.\n\n## Code Example\n\n```js\nconsole.log(\"hi\");\n```\n\n- Item one\n- Item two\n\n> A quote\n\n---\n\nThe end."
|
||||
let html = lib.toHtml(md)
|
||||
Test.assert(String.startsWith(html, "<h1>My Post</h1>"), "snap: blog starts with h1")
|
||||
Test.assert(String.contains(html, "<strong>bold</strong>"), "snap: blog has bold")
|
||||
Test.assert(String.contains(html, "<em>italic</em>"), "snap: blog has italic")
|
||||
Test.assert(String.contains(html, "<h2>Code Example</h2>"), "snap: blog has h2")
|
||||
Test.assert(String.contains(html, "language-js"), "snap: blog has js code block")
|
||||
Test.assert(String.contains(html, "<li>Item one</li>"), "snap: blog has list item")
|
||||
Test.assert(String.contains(html, "<blockquote>"), "snap: blog has blockquote")
|
||||
Test.assert(String.contains(html, "<hr>"), "snap: blog has hr")
|
||||
Test.assert(String.contains(html, "The end."), "snap: blog has closing")
|
||||
}
|
||||
|
||||
// Snapshot: inline code with HTML entities
|
||||
fn test_snapshot_code_html_entities(): Unit with {Test} = {
|
||||
let result = lib.inlineToHtml("Use `<div>` and `&` in HTML")
|
||||
let expected = "Use <code><div></code> and <code>&amp;</code> in HTML"
|
||||
Test.assertEqualMsg(expected, result, "snap: code with HTML entities")
|
||||
}
|
||||
|
||||
// Snapshot: multiple paragraphs
|
||||
fn test_snapshot_multiple_paragraphs(): Unit with {Test} = {
|
||||
let md = "First.\n\nSecond.\n\nThird."
|
||||
let expected = "<p>First.</p>\n<p>Second.</p>\n<p>Third.</p>\n"
|
||||
Test.assertEqualMsg(expected, lib.toHtml(md), "snap: three paragraphs")
|
||||
}
|
||||
160
packages/markdown/test_unit.lux
Normal file
160
packages/markdown/test_unit.lux
Normal file
@@ -0,0 +1,160 @@
|
||||
import lib
|
||||
|
||||
// --- escapeHtml ---
|
||||
|
||||
fn test_escape_empty(): Unit with {Test} =
|
||||
Test.assertEqualMsg("", lib.escapeHtml(""), "escape empty string")
|
||||
|
||||
fn test_escape_no_special(): Unit with {Test} =
|
||||
Test.assertEqualMsg("hello world", lib.escapeHtml("hello world"), "no special chars unchanged")
|
||||
|
||||
fn test_escape_ampersand(): Unit with {Test} =
|
||||
Test.assertEqualMsg("a & b", lib.escapeHtml("a & b"), "escape ampersand")
|
||||
|
||||
fn test_escape_less_than(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<div>", lib.escapeHtml("<div>"), "escape angle brackets")
|
||||
|
||||
fn test_escape_all_three(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<a> & <b>", lib.escapeHtml("<a> & <b>"), "escape all three")
|
||||
|
||||
// --- inline: bold ---
|
||||
|
||||
fn test_bold(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<strong>bold</strong>", lib.inlineToHtml("**bold**"), "bold text")
|
||||
|
||||
fn test_bold_in_sentence(): Unit with {Test} =
|
||||
Test.assertEqualMsg("a <strong>bold</strong> b", lib.inlineToHtml("a **bold** b"), "bold in sentence")
|
||||
|
||||
fn test_bold_unclosed(): Unit with {Test} =
|
||||
Test.assertEqualMsg("**unclosed", lib.inlineToHtml("**unclosed"), "unclosed bold")
|
||||
|
||||
// --- inline: italic ---
|
||||
|
||||
fn test_italic_asterisk(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<em>italic</em>", lib.inlineToHtml("*italic*"), "italic with asterisk")
|
||||
|
||||
fn test_italic_underscore(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<em>italic</em>", lib.inlineToHtml("_italic_"), "italic with underscore")
|
||||
|
||||
fn test_italic_unclosed(): Unit with {Test} =
|
||||
Test.assertEqualMsg("*unclosed", lib.inlineToHtml("*unclosed"), "unclosed italic")
|
||||
|
||||
// --- inline: code ---
|
||||
|
||||
fn test_inline_code(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<code>code</code>", lib.inlineToHtml("`code`"), "inline code")
|
||||
|
||||
fn test_inline_code_with_html(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<code><div></code>", lib.inlineToHtml("`<div>`"), "code escapes HTML")
|
||||
|
||||
fn test_inline_code_unclosed(): Unit with {Test} =
|
||||
Test.assertEqualMsg("`unclosed", lib.inlineToHtml("`unclosed"), "unclosed backtick")
|
||||
|
||||
// --- inline: strikethrough ---
|
||||
|
||||
fn test_strikethrough(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<del>strike</del>", lib.inlineToHtml("~~strike~~"), "strikethrough")
|
||||
|
||||
fn test_strikethrough_unclosed(): Unit with {Test} =
|
||||
Test.assertEqualMsg("~~unclosed", lib.inlineToHtml("~~unclosed"), "unclosed strikethrough")
|
||||
|
||||
// --- inline: links ---
|
||||
|
||||
fn test_link(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<a href=\"/page\">text</a>", lib.inlineToHtml("[text](/page)"), "basic link")
|
||||
|
||||
fn test_link_with_formatting(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<strong><a href=\"/\">bold link</a></strong>", lib.inlineToHtml("**[bold link](/)**"), "bold link")
|
||||
|
||||
fn test_link_empty_text(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<a href=\"/page\"></a>", lib.inlineToHtml("[](/page)"), "link with empty text")
|
||||
|
||||
fn test_link_unclosed(): Unit with {Test} =
|
||||
Test.assertEqualMsg("[unclosed", lib.inlineToHtml("[unclosed"), "unclosed bracket")
|
||||
|
||||
// --- inline: images ---
|
||||
|
||||
fn test_image(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<img src=\"img.png\" alt=\"alt text\">", lib.inlineToHtml(""), "basic image")
|
||||
|
||||
fn test_image_empty_alt(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<img src=\"photo.jpg\" alt=\"\">", lib.inlineToHtml(""), "image with empty alt")
|
||||
|
||||
// --- inline: plain text ---
|
||||
|
||||
fn test_plain_text(): Unit with {Test} =
|
||||
Test.assertEqualMsg("hello world", lib.inlineToHtml("hello world"), "plain text unchanged")
|
||||
|
||||
fn test_empty_inline(): Unit with {Test} =
|
||||
Test.assertEqualMsg("", lib.inlineToHtml(""), "empty input")
|
||||
|
||||
// --- block: headings ---
|
||||
|
||||
fn test_h1(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<h1>Hello</h1>\n", lib.toHtml("# Hello"), "h1")
|
||||
|
||||
fn test_h2(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<h2>World</h2>\n", lib.toHtml("## World"), "h2")
|
||||
|
||||
fn test_h3(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<h3>Sub</h3>\n", lib.toHtml("### Sub"), "h3")
|
||||
|
||||
fn test_h4(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<h4>Deep</h4>\n", lib.toHtml("#### Deep"), "h4")
|
||||
|
||||
fn test_heading_with_inline(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<h1><strong>Bold</strong> heading</h1>\n", lib.toHtml("# **Bold** heading"), "heading with inline formatting")
|
||||
|
||||
// --- block: paragraphs ---
|
||||
|
||||
fn test_single_paragraph(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<p>hello world</p>\n", lib.toHtml("hello world"), "single paragraph")
|
||||
|
||||
fn test_empty_input(): Unit with {Test} =
|
||||
Test.assertEqualMsg("", lib.toHtml(""), "empty input produces empty output")
|
||||
|
||||
// --- block: code blocks ---
|
||||
|
||||
fn test_code_block(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<pre><code>hello\n</code></pre>\n", lib.toHtml("```\nhello\n```"), "code block")
|
||||
|
||||
fn test_code_block_with_lang(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<pre><code class=\"language-js\">let x = 1;\n</code></pre>\n", lib.toHtml("```js\nlet x = 1;\n```"), "code block with language")
|
||||
|
||||
fn test_code_block_escapes_html(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<pre><code><div>\n</code></pre>\n", lib.toHtml("```\n<div>\n```"), "code block escapes HTML")
|
||||
|
||||
// --- block: lists ---
|
||||
|
||||
fn test_unordered_list(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<ul>\n<li>one</li>\n<li>two</li>\n</ul>\n", lib.toHtml("- one\n- two"), "unordered list")
|
||||
|
||||
fn test_ordered_list(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<ol>\n<li>first</li>\n<li>second</li>\n</ol>\n", lib.toHtml("1. first\n2. second"), "ordered list")
|
||||
|
||||
fn test_single_item_list(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<ul>\n<li>only</li>\n</ul>\n", lib.toHtml("- only"), "single item list")
|
||||
|
||||
// --- block: blockquotes ---
|
||||
|
||||
fn test_blockquote(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<blockquote>\n<p>quoted</p>\n</blockquote>\n", lib.toHtml("> quoted"), "blockquote")
|
||||
|
||||
// --- block: horizontal rules ---
|
||||
|
||||
fn test_hr_dashes(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<hr>\n", lib.toHtml("---"), "hr with dashes")
|
||||
|
||||
fn test_hr_asterisks(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<hr>\n", lib.toHtml("***"), "hr with asterisks")
|
||||
|
||||
fn test_hr_underscores(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<hr>\n", lib.toHtml("___"), "hr with underscores")
|
||||
|
||||
// --- block: HTML passthrough ---
|
||||
|
||||
fn test_html_passthrough(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<div class=\"foo\">bar</div>\n", lib.toHtml("<div class=\"foo\">bar</div>"), "HTML passes through")
|
||||
|
||||
fn test_heading_in_list(): Unit with {Test} =
|
||||
Test.assertEqualMsg("<ul>\n<li><h3>Title</h3></li>\n</ul>\n", lib.toHtml("- ### Title"), "heading inside list item")
|
||||
Reference in New Issue
Block a user