perf: Rewriting convert_deflists for performance.

This commit is contained in:
Spencer Brower
2026-09-02 14:59:04 -04:00
parent 0392bfe587
commit 9f7db3a916
41 changed files with 688 additions and 117 deletions
+4
View File
@@ -169,6 +169,10 @@
bloaty # du for binaries?
# temp
hugo
pandoc
# IDE
unstable.helix
claude-code
+231 -117
View File
@@ -4,140 +4,275 @@ import cm "vendor:commonmark"
import "core:strings"
// DefList_Entry represents a single term-definition pair in a definition list.
DefList_Entry :: struct {
term: string,
definition: string,
}
// convert_deflists scans markdown text for definition list patterns and
// converts them to <dl><dt><dd> HTML blocks before cmark processing.
//
// A definition line starts with optional whitespace followed by a colon and
// a space. The term is the nearest preceding non-blank line (immediately or
// within one blank line). Consecutive term+definition pairs are grouped into
// a single <dl> block.
// A definition line starts with fewer than four columns of leading whitespace
// followed by a colon and a space (or end-of-line). The term is the nearest
// preceding non-blank line (immediately or within one blank line). Consecutive
// term+definition pairs are grouped into a single <dl> block, and consecutive
// definition lines under one term become multiple <dd> elements.
//
// Terms and definitions are rendered through cmark individually so that
// inline markdown (code, links, emphasis) is processed.
// The scanner walks the body one line at a time using byte offsets: it never
// splits the input, allocates nothing per line, and copies non-deflist text
// through verbatim. Matching is disabled inside fenced code blocks. Terms and
// definitions are rendered through cmark individually so that inline markdown
// (code, links, emphasis) is processed.
// Line_Kind classifies a line for convert_deflists's group-finding scan. Other
// covers fenced-code delimiters and their contents, where definition syntax is
// inert.
Line_Kind :: enum {
None,
Blank,
Term,
Def,
Other,
}
convert_deflists :: proc(body: string, allocator := context.allocator) -> string {
lines := strings.split(body, "\n", allocator = context.temp_allocator)
// Fast path: a body with no definition line at all cannot contain a
// definition list, so return it untouched — skipping the builder allocation
// and the classifying scan below. This is the common case (most pages have
// no definition lists), and the guard scan is leaner than the full loop.
if !has_def_line(body) {
return body
}
sb := strings.builder_make(context.temp_allocator)
first := true
need_blank := false
// emitted_up_to is the byte offset up to which `body` has been copied into
// `sb`. It stays at 0 until the first deflist is emitted; if it is still 0
// at the end, nothing matched and we can return the body unchanged.
emitted_up_to := 0
i := 0
for i < len(lines) {
entries, matched, next := try_match_deflist(lines, i)
if matched {
html := render_deflist(entries)
if !first {
strings.write_string(&sb, "\n\n")
in_fence := false
fence_marker: u8 = 0
// The scan is definition-driven: it walks line by line, remembering the two
// previous lines' kinds and offsets, and only does group-finding work when it
// reaches a (rare) definition line — whose term is the immediately preceding
// line, or the one before a single blank line. This avoids a per-line forward
// lookahead: each line is classified once, with no rescanning.
prev1_kind, prev2_kind := Line_Kind.None, Line_Kind.None
prev1_pos, prev2_pos := 0, 0
pos := 0
for {
line, next, ok := line_at(body, pos)
if !ok {
break
}
// Classify the line. Definition-list syntax is inert inside fenced code
// blocks, so fence delimiters and their contents are classified Other.
kind: Line_Kind
if in_fence {
if is_fence_line(line, fence_marker) {
in_fence = false
}
strings.write_string(&sb, html)
first = false
need_blank = true
i = next
continue
kind = .Other
} else if marker, opened := fence_open(line); opened {
in_fence = true
fence_marker = marker
kind = .Other
} else if is_def_line(line) {
kind = .Def
} else if is_blank_line(line) {
kind = .Blank
} else {
kind = .Term
}
if need_blank {
strings.write_string(&sb, "\n\n")
need_blank = false
} else if !first {
strings.write_string(&sb, "\n")
// A definition-list group begins at the term preceding this def line
// (immediately, or across a single blank line). Flush pending passthrough
// text verbatim, then render the whole group directly into the builder.
if kind == .Def {
term_pos := -1
if prev1_kind == .Term {
term_pos = prev1_pos
} else if prev1_kind == .Blank && prev2_kind == .Term {
term_pos = prev2_pos
}
if term_pos >= 0 {
strings.write_string(&sb, body[emitted_up_to:term_pos])
group_end := write_deflist_group(&sb, body, term_pos)
emitted_up_to = group_end
pos = group_end
prev1_kind, prev2_kind = .None, .None
in_fence = false
continue
}
}
strings.write_string(&sb, lines[i])
first = false
i += 1
prev2_kind, prev2_pos = prev1_kind, prev1_pos
prev1_kind, prev1_pos = kind, pos
pos = next
}
if emitted_up_to == 0 {
// No definition list anywhere; the body passes through unchanged.
return body
}
strings.write_string(&sb, body[emitted_up_to:])
return strings.clone(strings.to_string(sb), allocator)
}
// try_match_deflist attempts to match a definition list group starting at
// lines[start]. A group is one or more term+definition pairs. Returns the
// matched entries, whether a match was found, and the index past the group.
try_match_deflist :: proc(
lines: []string,
start: int,
) -> (
entries: [dynamic]DefList_Entry,
ok: bool,
end: int,
) {
entries = make([dynamic]DefList_Entry, 0, allocator = context.temp_allocator)
end = start
// line_at returns the line beginning at byte offset `pos` (excluding its
// trailing newline) along with the offset of the following line. `pos` must be
// 0 or one past a '\n'. ok is false once `pos` reaches the end of `body`.
line_at :: proc(body: string, pos: int) -> (line: string, next: int, ok: bool) {
if pos >= len(body) {
return "", pos, false
}
nl := strings.index_byte(body[pos:], '\n')
if nl < 0 {
return body[pos:], len(body), true
}
return body[pos:pos + nl], pos + nl + 1, true
}
i := start
for i < len(lines) {
// A term must be non-blank and not itself a def line
if is_blank_line(lines[i]) || is_def_line(lines[i]) {
// has_def_line reports whether any line in `body` is a definition line (see
// is_def_line). It is a cheap guard for convert_deflists: a body with no
// definition line cannot contain a definition list. There are no false
// negatives (every definition list requires a definition line); a false
// positive — e.g. a colon line that only appears inside a code fence — simply
// falls through to the full scan, which then finds no match and returns `body`.
has_def_line :: proc(body: string) -> bool {
pos := 0
for {
line, next := line_at(body, pos) or_break
if is_def_line(line) {
return true
}
pos = next
}
return false
}
// write_deflist_group renders the definition-list group beginning at `start`
// directly into `sb`, wrapped in <dl>...</dl>, and returns the byte offset one
// past the group (the start of the first line not consumed). The caller must
// have confirmed via term_has_def that a group starts at `start`, so the group
// always contains at least one term+definition pair. A trailing blank line that
// separates the group from following content is left unconsumed.
write_deflist_group :: proc(sb: ^strings.Builder, body: string, start: int) -> (end: int) {
strings.write_string(sb, "<dl>")
pos := start
for {
term_line, term_next, term_ok := line_at(body, pos)
if !term_ok || is_blank_line(term_line) || is_def_line(term_line) {
break
}
// Look for a def line: immediately after or with one blank line
def_idx := i + 1
if def_idx < len(lines) && is_blank_line(lines[def_idx]) {
def_idx += 1
// The definition follows immediately or across a single blank line.
def_pos := term_next
def_line, def_next, def_ok := line_at(body, def_pos)
if def_ok && is_blank_line(def_line) {
def_pos = def_next
def_line, def_next, def_ok = line_at(body, def_pos)
}
if def_idx >= len(lines) || !is_def_line(lines[def_idx]) {
if !def_ok || !is_def_line(def_line) {
break
}
// Found a term + def pair
append(
&entries,
DefList_Entry {
term = strings.trim_space(lines[i]),
definition = def_content(lines[def_idx]),
},
)
i = def_idx + 1
// Emit the term with its first definition, then any consecutive
// definition lines as additional <dd>s under the same term.
strings.write_string(sb, "<dt>")
strings.write_string(sb, render_inline_md(strings.trim_space(term_line)))
strings.write_string(sb, "</dt><dd>")
strings.write_string(sb, render_inline_md(def_content(def_line)))
strings.write_string(sb, "</dd>")
pos = def_next
for {
cont, cont_next, cont_ok := line_at(body, pos)
if !cont_ok || !is_def_line(cont) {
break
}
strings.write_string(sb, "<dd>")
strings.write_string(sb, render_inline_md(def_content(cont)))
strings.write_string(sb, "</dd>")
pos = cont_next
}
// After a pair, check if another pair follows (optionally
// separated by one blank line). If so, continue the group.
// If not, break without consuming the blank line.
if i < len(lines) && is_blank_line(lines[i]) {
after_blank := i + 1
if after_blank < len(lines) &&
!is_blank_line(lines[after_blank]) &&
!is_def_line(lines[after_blank]) {
// Check whether a def follows this potential term
check_def := after_blank + 1
if check_def < len(lines) && is_blank_line(lines[check_def]) {
check_def += 1
}
if check_def < len(lines) && is_def_line(lines[check_def]) {
i = after_blank
continue
}
// Continue the group if another term+definition pair follows,
// optionally separated by one blank line. Otherwise stop here without
// consuming a trailing blank line.
blank_line, after_blank, blank_ok := line_at(body, pos)
if blank_ok && is_blank_line(blank_line) {
if term_has_def(body, after_blank) {
pos = after_blank
continue
}
break
}
// No blank line: loop and let the top re-test this line as a term.
}
if len(entries) > 0 {
ok = true
end = i
}
return
strings.write_string(sb, "</dl>")
return pos
}
// is_def_line returns true if the line is a definition line:
// optional leading whitespace, a colon, then whitespace or end-of-line.
is_def_line :: proc(line: string) -> bool {
trimmed := strings.trim_left(line, " \t")
if len(trimmed) < 1 || trimmed[0] != ':' {
// term_has_def reports whether the line at `pos` is a term that is immediately
// (or across one blank line) followed by a definition line.
term_has_def :: proc(body: string, pos: int) -> bool {
line, next, ok := line_at(body, pos)
if !ok || is_blank_line(line) || is_def_line(line) {
return false
}
if len(trimmed) == 1 {
def_pos := next
def_line, def_next, def_ok := line_at(body, def_pos)
if def_ok && is_blank_line(def_line) {
def_pos = def_next
def_line, def_next, def_ok = line_at(body, def_pos)
}
return def_ok && is_def_line(def_line)
}
// fence_open reports whether `line` opens a fenced code block and, if so, which
// marker character it uses. Leading spaces are ignored.
fence_open :: proc(line: string) -> (marker: u8, ok: bool) {
trimmed := strings.trim_left(line, " ")
if strings.has_prefix(trimmed, "```") {
return '`', true
}
if strings.has_prefix(trimmed, "~~~") {
return '~', true
}
return 0, false
}
// is_fence_line reports whether `line` is a fence using the given marker
// character (used to detect the closing fence of an open block).
is_fence_line :: proc(line: string, marker: u8) -> bool {
trimmed := strings.trim_left(line, " ")
if marker == '~' {
return strings.has_prefix(trimmed, "~~~")
}
return strings.has_prefix(trimmed, "```")
}
// is_def_line returns true if the line is a definition line: fewer than four
// columns of leading whitespace, a colon, then whitespace or end-of-line. Four
// or more columns of indentation is a code block, not a definition.
is_def_line :: proc(line: string) -> bool {
indent := 0
n := 0
for n < len(line) && (line[n] == ' ' || line[n] == '\t') {
indent += line[n] == '\t' ? 4 : 1
n += 1
}
if indent >= 4 {
return false
}
rest := line[n:]
if len(rest) < 1 || rest[0] != ':' {
return false
}
if len(rest) == 1 {
return true
}
return trimmed[1] == ' ' || trimmed[1] == '\t'
return rest[1] == ' ' || rest[1] == '\t'
}
// def_content extracts the definition text from a definition line,
@@ -154,27 +289,6 @@ is_blank_line :: proc(line: string) -> bool {
return strings.trim_space(line) == ""
}
// render_deflist builds the <dl> HTML block from a list of entries.
// Each term and definition is rendered through cmark to process inline
// markdown. Result lives in context.temp_allocator.
render_deflist :: proc(entries: [dynamic]DefList_Entry) -> string {
sb := strings.builder_make(context.temp_allocator)
strings.write_string(&sb, "<dl>")
for entry in entries {
term_html := render_inline_md(entry.term)
def_html := render_inline_md(entry.definition)
strings.write_string(&sb, "<dt>")
strings.write_string(&sb, term_html)
strings.write_string(&sb, "</dt><dd>")
strings.write_string(&sb, def_html)
strings.write_string(&sb, "</dd>")
}
strings.write_string(&sb, "</dl>")
return strings.to_string(sb)
}
// render_inline_md renders a snippet of markdown through cmark and strips
// the surrounding <p> tags. Result lives in context.temp_allocator.
render_inline_md :: proc(text: string) -> string {
+92
View File
@@ -0,0 +1,92 @@
#+test
package markdown
import "core:testing"
// Fixture-driven tests for convert_deflists. Every fixture is exact-matched
// against its oracle in testdata/deflists/expected/. See
// testdata/deflists/reference/README.md for how each expected file was decided
// (cross-checked against Pandoc and Hugo/Goldmark).
//
// The fixture inputs are the definition-list examples from
// https://www.markdownlang.com/extended/definition-lists.html
//
// Provenance of each expected file:
// 01,02,08,09 Pandoc and Hugo agree; oracle = that agreed HTML.
// 06 Both agree it is NOT a deflist; convert_deflists passes the
// markdown through unchanged, so expected = the raw input.
// 07 CommonMark's <4-space marker rule: we accept the 2-space colon
// (Pandoc does; Hugo does not).
// 05 Block nesting is unsupported; our flatten happens to match
// Pandoc's output exactly.
// 03,04 Block content in definitions is unsupported; expected captures
// our current degraded behavior (a <dl> for the term/first line,
// the rest falling through as markdown) as a regression guard.
Deflist_Fixture :: struct {
name: string,
input: string,
expected: string,
}
@(test)
test_deflist_fixtures :: proc(t: ^testing.T) {
cases := []Deflist_Fixture {
{
"01-basic",
#load(#directory + "testdata/deflists/01-basic.md", string),
#load(#directory + "testdata/deflists/expected/01-basic.html", string),
},
{
"02-multiple-defs",
#load(#directory + "testdata/deflists/02-multiple-defs.md", string),
#load(#directory + "testdata/deflists/expected/02-multiple-defs.html", string),
},
{
"03-multiline-def",
#load(#directory + "testdata/deflists/03-multiline-def.md", string),
#load(#directory + "testdata/deflists/expected/03-multiline-def.html", string),
},
{
"04-markdown-in-def",
#load(#directory + "testdata/deflists/04-markdown-in-def.md", string),
#load(#directory + "testdata/deflists/expected/04-markdown-in-def.html", string),
},
{
"05-nested",
#load(#directory + "testdata/deflists/05-nested.md", string),
#load(#directory + "testdata/deflists/expected/05-nested.html", string),
},
{
"06-compact",
#load(#directory + "testdata/deflists/06-compact.md", string),
#load(#directory + "testdata/deflists/expected/06-compact.html", string),
},
{
"07-extra-space",
#load(#directory + "testdata/deflists/07-extra-space.md", string),
#load(#directory + "testdata/deflists/expected/07-extra-space.html", string),
},
{
"08-glossary",
#load(#directory + "testdata/deflists/08-glossary.md", string),
#load(#directory + "testdata/deflists/expected/08-glossary.html", string),
},
{
"09-api-params",
#load(#directory + "testdata/deflists/09-api-params.md", string),
#load(#directory + "testdata/deflists/expected/09-api-params.html", string),
},
}
for c in cases {
result := convert_deflists(c.input, context.temp_allocator)
testing.expectf(
t,
result == c.expected,
"[%s]\n expected: %q\n got: %q",
c.name,
c.expected,
result,
)
}
}
+2
View File
@@ -0,0 +1,2 @@
Term
: Definition content
+7
View File
@@ -0,0 +1,7 @@
Markdown
: A lightweight markup language
: Created by John Gruber in 2004
HTML
: A standard markup language used to create web pages
: Used to create web pages
+5
View File
@@ -0,0 +1,5 @@
Term
: This is the first line of definition content
This is the second line, needs at least one space indentation
This is a new paragraph, needs at least one space indentation and a preceding blank line
+7
View File
@@ -0,0 +1,7 @@
Markdown Syntax
: **Markdown** supports multiple text formatting:
- *Italic* and **Bold**
- [Link](https://www.markdownlang.com)
- `Inline code`
- > Block quote
- Lists and other elements
+6
View File
@@ -0,0 +1,6 @@
Outer Term
: Outer Definition
Inner Term
: Inner Definition
: Another Inner Definition
+1
View File
@@ -0,0 +1 @@
Term: Definition
+2
View File
@@ -0,0 +1,2 @@
Term
: Definition
+11
View File
@@ -0,0 +1,11 @@
API
: An application programming interface, a set of rules that allow different software applications to communicate with each other.
Framework
: A software library that provides a standardized structure for application development.
Git
: A distributed version control system used to track changes in a project development process.
IDE
: An integrated development environment, a software application that includes a code editor and various development tools.
+14
View File
@@ -0,0 +1,14 @@
user_id
: **Required** - The unique identifier for the user.
: Type: `integer`
name
: **Optional** - The display name for the user.
: Type: `string`
: Default: `null`
status
: **Optional** - The user status.
: Type: `string`
: Allowed values: `active`, `inactive`, `suspended`
: Default: `active`
+1
View File
@@ -0,0 +1 @@
<dl><dt>Term</dt><dd>Definition content</dd></dl>
@@ -0,0 +1 @@
<dl><dt>Markdown</dt><dd>A lightweight markup language</dd><dd>Created by John Gruber in 2004</dd><dt>HTML</dt><dd>A standard markup language used to create web pages</dd><dd>Used to create web pages</dd></dl>
@@ -0,0 +1,3 @@
<dl><dt>Term</dt><dd>This is the first line of definition content</dd></dl> This is the second line, needs at least one space indentation
This is a new paragraph, needs at least one space indentation and a preceding blank line
@@ -0,0 +1,5 @@
<dl><dt>Markdown Syntax</dt><dd><strong>Markdown</strong> supports multiple text formatting:</dd></dl> - *Italic* and **Bold**
- [Link](https://www.markdownlang.com)
- `Inline code`
- > Block quote
- Lists and other elements
+1
View File
@@ -0,0 +1 @@
<dl><dt>Outer Term</dt><dd>Outer Definition</dd><dt>Inner Term</dt><dd>Inner Definition</dd><dd>Another Inner Definition</dd></dl>
+1
View File
@@ -0,0 +1 @@
Term: Definition
@@ -0,0 +1 @@
<dl><dt>Term</dt><dd>Definition</dd></dl>
+1
View File
@@ -0,0 +1 @@
<dl><dt>API</dt><dd>An application programming interface, a set of rules that allow different software applications to communicate with each other.</dd><dt>Framework</dt><dd>A software library that provides a standardized structure for application development.</dd><dt>Git</dt><dd>A distributed version control system used to track changes in a project development process.</dd><dt>IDE</dt><dd>An integrated development environment, a software application that includes a code editor and various development tools.</dd></dl>
@@ -0,0 +1 @@
<dl><dt>user_id</dt><dd><strong>Required</strong> - The unique identifier for the user.</dd><dd>Type: <code>integer</code></dd><dt>name</dt><dd><strong>Optional</strong> - The display name for the user.</dd><dd>Type: <code>string</code></dd><dd>Default: <code>null</code></dd><dt>status</dt><dd><strong>Optional</strong> - The user status.</dd><dd>Type: <code>string</code></dd><dd>Allowed values: <code>active</code>, <code>inactive</code>, <code>suspended</code></dd><dd>Default: <code>active</code></dd></dl>
+60
View File
@@ -0,0 +1,60 @@
# 3-way behavior comparison: ours vs Pandoc vs Hugo
Captured for later review (e.g. if we ever pursue "Option B" — full CommonMark
block semantics inside definitions). Compares `convert_deflists` output (Option A,
inline/flat) against Pandoc (`-f markdown -t html5`) and Hugo/Goldmark for the
four fixtures where behavior is interesting. Raw renderings live in `pandoc/` and
`hugo/`; ours is in `../expected/`. Whitespace/formatting differences ignored
below — only structural differences are described.
## 03-multiline-def — definition with a continuation line + a second paragraph
*How much multi-line content belongs to the `<dd>`?*
- **Ours:** only the **first line** goes in the `<dd>`. The continuation line and
the second paragraph fall out of the `<dl>` as **raw, unrendered markdown**.
- **Pandoc:** folds the continuation line **into** the `<dd>` (lazy continuation),
then **ejects** the second paragraph as a sibling `<p>` after `</dl>`.
- **Hugo:** keeps **everything inside** the `<dd>` — continuation line as text,
second paragraph as a nested `<p>`.
- All three differ. We capture the least, Pandoc the middle, Hugo the most.
## 04-markdown-in-def — definition containing a bullet list (with a blockquote)
*What happens to the nested list?*
- **Ours:** intro line becomes the `<dd>`; the **entire bullet list falls out as
raw markdown** (unrendered `- …` lines after `</dl>`).
- **Pandoc:** bullets **flattened into the `<dd>`** as inline text — no `<ul>`, the
`-` become literal dashes.
- **Hugo:** bullets become a **real nested `<ul>`/`<li>`** (blockquote and all)
inside the `<dd>`.
- All three differ; same shape as 03. We drop it, Pandoc absorbs-but-flattens,
Hugo builds true structure.
## 05-nested — a definition list nested under another definition
*Nest or flatten?*
- **Ours:** **flatten** — one flat `<dl>`; "Inner Term" is just another `<dt>`.
- **Pandoc:** **flatten** — structurally **identical to ours**.
- **Hugo:** **nests** a real inner `<dl>` inside the outer `<dd>`.
- Ours == Pandoc; Hugo is the odd one out.
## 07-extra-space — term then a 2-space-indented `: Definition`
*Is a 2-space colon a definition marker?*
- **Ours:** **yes** → clean `<dl>`.
- **Pandoc:** **yes** → structurally **identical to ours**.
- **Hugo:** **no** → rejects it, emits a plain `<p>`.
- Ours == Pandoc; Hugo is the odd one out.
## Pattern
Two clean groups:
- **Marker / flattening decisions (05, 07):** we land **exactly on Pandoc**. These
are the calls our CommonMark-marker + flat-inline design makes cleanly; Pandoc
agrees, Hugo diverges (nests / stricter marker).
- **Block-content-in-a-definition (03, 04):** we diverge from **both** tools, doing
**less** — we emit the `<dl>` for the term/first line and let the rest fall
through as raw markdown, rather than absorbing it into the `<dd>` (flattened like
Pandoc, or structured like Hugo).
In short: our behavior is **"Pandoc, minus the ability to pull block content into
a `<dd>`."** Single inline-line definitions (01, 02, 05, 07, 08, 09) match a real
engine; genuinely multi-block definitions (03, 04) stop at the first line and pass
the remainder through. Closing that gap is what Option B would entail.
+44
View File
@@ -0,0 +1,44 @@
# Definition-list test fixtures
Inputs (`../NN-name.md`) are the definition-list examples from
<https://www.markdownlang.com/extended/definition-lists.html>.
`pandoc/` and `hugo/` hold each input rendered by Pandoc (`-f markdown -t html5`)
and Hugo (Goldmark with `markup.goldmark.extensions.definitionList = true`).
They are retained as reference for a possible future "Option B" (full CommonMark
block semantics inside definitions). They are NOT the test oracle for the current
implementation.
`../expected/NN.html` is the oracle for `convert_deflists`: our inline fragment
format (no inter-tag newlines, no trailing newline), derived from the trusted
tool per the decisions below.
## Decisions (Option A — simple / inline)
Marker whitespace follows CommonMark: `:` with 0–3 spaces of indent is a
definition; 4+ (tab = 4) is a code block. Definition bodies are inline/flat.
Every fixture has a concrete exact-match oracle in `../expected/NN.html`.
| Fixture | Pandoc | Hugo | Decision | expected/ source |
|---------|--------|------|----------|------------------|
| 01-basic | `<dl>` | `<dl>` | support | hugo (agreed) |
| 02-multiple-defs | multi `<dd>` | multi `<dd>` | support | hugo (agreed) |
| 06-compact (`Term: Definition`) | `<p>` | `<p>` | NOT a deflist → passthrough | raw input |
| 07-extra-space (2-space `:`) | `<dl>` | `<p>` | support (accept, CommonMark ≤3-space rule) | hand-written |
| 08-glossary | `<dl>` | `<dl>` | support | hugo (agreed) |
| 09-api-params | multi `<dd>` | multi `<dd>` | support | hugo (agreed) |
| 05-nested | flattens | nested `<dl>` | flatten (matches Pandoc) | snapshot (== pandoc) |
| 03-multiline-def | ejects 2nd para | nests 2nd para in `<dd>` | degrade | snapshot (current output) |
| 04-markdown-in-def | flattens list | nested `<ul>`/`<blockquote>` | degrade | snapshot (current output) |
A detailed 3-way behavior comparison (ours vs Pandoc vs Hugo) for the diverging
fixtures is in `COMPARISON.md`, kept for Option-B review.
03/04/05 require block-level content inside `<dd>` (multi-paragraph, nested
lists/blockquotes, nested `<dl>`), which the inline preprocessor does not
produce. 05 flattens exactly as Pandoc does, so its snapshot equals Pandoc's
output. 03/04 emit a `<dl>` for the term and first line, then let the remaining
block content fall through as markdown; their `expected/` files snapshot that
current degraded behavior as a regression guard, not an endorsed rendering.
Revisit via Option B (full CommonMark block semantics) if real content needs it.
@@ -0,0 +1,4 @@
<dl>
<dt>Term</dt>
<dd>Definition content</dd>
</dl>
@@ -0,0 +1,8 @@
<dl>
<dt>Markdown</dt>
<dd>A lightweight markup language</dd>
<dd>Created by John Gruber in 2004</dd>
<dt>HTML</dt>
<dd>A standard markup language used to create web pages</dd>
<dd>Used to create web pages</dd>
</dl>
@@ -0,0 +1,7 @@
<dl>
<dt>Term</dt>
<dd>This is the first line of definition content
This is the second line, needs at least one space indentation
<p>This is a new paragraph, needs at least one space indentation and a preceding blank line</p>
</dd>
</dl>
@@ -0,0 +1,16 @@
<dl>
<dt>Markdown Syntax</dt>
<dd><strong>Markdown</strong> supports multiple text formatting:
<ul>
<li><em>Italic</em> and <strong>Bold</strong></li>
<li><a href="https://www.markdownlang.com">Link</a></li>
<li><code>Inline code</code></li>
<li>
<blockquote>
<p>Block quote</p>
</blockquote>
</li>
<li>Lists and other elements</li>
</ul>
</dd>
</dl>
@@ -0,0 +1,10 @@
<dl>
<dt>Outer Term</dt>
<dd>Outer Definition
<dl>
<dt>Inner Term</dt>
<dd>Inner Definition</dd>
<dd>Another Inner Definition</dd>
</dl>
</dd>
</dl>
@@ -0,0 +1 @@
<p>Term: Definition</p>
@@ -0,0 +1,2 @@
<p>Term
: Definition</p>
@@ -0,0 +1,10 @@
<dl>
<dt>API</dt>
<dd>An application programming interface, a set of rules that allow different software applications to communicate with each other.</dd>
<dt>Framework</dt>
<dd>A software library that provides a standardized structure for application development.</dd>
<dt>Git</dt>
<dd>A distributed version control system used to track changes in a project development process.</dd>
<dt>IDE</dt>
<dd>An integrated development environment, a software application that includes a code editor and various development tools.</dd>
</dl>
@@ -0,0 +1,14 @@
<dl>
<dt>user_id</dt>
<dd><strong>Required</strong> - The unique identifier for the user.</dd>
<dd>Type: <code>integer</code></dd>
<dt>name</dt>
<dd><strong>Optional</strong> - The display name for the user.</dd>
<dd>Type: <code>string</code></dd>
<dd>Default: <code>null</code></dd>
<dt>status</dt>
<dd><strong>Optional</strong> - The user status.</dd>
<dd>Type: <code>string</code></dd>
<dd>Allowed values: <code>active</code>, <code>inactive</code>, <code>suspended</code></dd>
<dd>Default: <code>active</code></dd>
</dl>
@@ -0,0 +1,6 @@
<dl>
<dt>Term</dt>
<dd>
Definition content
</dd>
</dl>
@@ -0,0 +1,16 @@
<dl>
<dt>Markdown</dt>
<dd>
A lightweight markup language
</dd>
<dd>
Created by John Gruber in 2004
</dd>
<dt>HTML</dt>
<dd>
A standard markup language used to create web pages
</dd>
<dd>
Used to create web pages
</dd>
</dl>
@@ -0,0 +1,9 @@
<dl>
<dt>Term</dt>
<dd>
This is the first line of definition content This is the second line,
needs at least one space indentation
</dd>
</dl>
<p>This is a new paragraph, needs at least one space indentation and a
preceding blank line</p>
@@ -0,0 +1,9 @@
<dl>
<dt>Markdown Syntax</dt>
<dd>
<strong>Markdown</strong> supports multiple text formatting: -
<em>Italic</em> and <strong>Bold</strong> - <a
href="https://www.markdownlang.com">Link</a> - <code>Inline code</code>
- &gt; Block quote - Lists and other elements
</dd>
</dl>
@@ -0,0 +1,13 @@
<dl>
<dt>Outer Term</dt>
<dd>
Outer Definition
</dd>
<dt>Inner Term</dt>
<dd>
Inner Definition
</dd>
<dd>
Another Inner Definition
</dd>
</dl>
@@ -0,0 +1 @@
<p>Term: Definition</p>
@@ -0,0 +1,6 @@
<dl>
<dt>Term</dt>
<dd>
Definition
</dd>
</dl>
@@ -0,0 +1,22 @@
<dl>
<dt>API</dt>
<dd>
An application programming interface, a set of rules that allow
different software applications to communicate with each other.
</dd>
<dt>Framework</dt>
<dd>
A software library that provides a standardized structure for
application development.
</dd>
<dt>Git</dt>
<dd>
A distributed version control system used to track changes in a project
development process.
</dd>
<dt>IDE</dt>
<dd>
An integrated development environment, a software application that
includes a code editor and various development tools.
</dd>
</dl>
@@ -0,0 +1,33 @@
<dl>
<dt>user_id</dt>
<dd>
<strong>Required</strong> - The unique identifier for the user.
</dd>
<dd>
Type: <code>integer</code>
</dd>
<dt>name</dt>
<dd>
<strong>Optional</strong> - The display name for the user.
</dd>
<dd>
Type: <code>string</code>
</dd>
<dd>
Default: <code>null</code>
</dd>
<dt>status</dt>
<dd>
<strong>Optional</strong> - The user status.
</dd>
<dd>
Type: <code>string</code>
</dd>
<dd>
Allowed values: <code>active</code>, <code>inactive</code>,
<code>suspended</code>
</dd>
<dd>
Default: <code>active</code>
</dd>
</dl>