mirror of
https://github.com/sbrow/thor.git
synced 2026-10-25 02:01:56 -04:00
perf: Rewriting convert_deflists for performance.
This commit is contained in:
@@ -169,6 +169,10 @@
|
|||||||
|
|
||||||
bloaty # du for binaries?
|
bloaty # du for binaries?
|
||||||
|
|
||||||
|
# temp
|
||||||
|
hugo
|
||||||
|
pandoc
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
unstable.helix
|
unstable.helix
|
||||||
claude-code
|
claude-code
|
||||||
|
|||||||
+248
-134
@@ -4,140 +4,275 @@ import cm "vendor:commonmark"
|
|||||||
|
|
||||||
import "core:strings"
|
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
|
// convert_deflists scans markdown text for definition list patterns and
|
||||||
// converts them to <dl><dt><dd> HTML blocks before cmark processing.
|
// converts them to <dl><dt><dd> HTML blocks before cmark processing.
|
||||||
//
|
//
|
||||||
// A definition line starts with optional whitespace followed by a colon and
|
// A definition line starts with fewer than four columns of leading whitespace
|
||||||
// a space. The term is the nearest preceding non-blank line (immediately or
|
// followed by a colon and a space (or end-of-line). The term is the nearest
|
||||||
// within one blank line). Consecutive term+definition pairs are grouped into
|
// preceding non-blank line (immediately or within one blank line). Consecutive
|
||||||
// a single <dl> block.
|
// 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
|
// The scanner walks the body one line at a time using byte offsets: it never
|
||||||
// inline markdown (code, links, emphasis) is processed.
|
// 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 {
|
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)
|
sb := strings.builder_make(context.temp_allocator)
|
||||||
|
|
||||||
first := true
|
// emitted_up_to is the byte offset up to which `body` has been copied into
|
||||||
need_blank := false
|
// `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
|
in_fence := false
|
||||||
for i < len(lines) {
|
fence_marker: u8 = 0
|
||||||
entries, matched, next := try_match_deflist(lines, i)
|
|
||||||
if matched {
|
// The scan is definition-driven: it walks line by line, remembering the two
|
||||||
html := render_deflist(entries)
|
// previous lines' kinds and offsets, and only does group-finding work when it
|
||||||
if !first {
|
// reaches a (rare) definition line — whose term is the immediately preceding
|
||||||
strings.write_string(&sb, "\n\n")
|
// 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
|
||||||
}
|
}
|
||||||
strings.write_string(&sb, html)
|
|
||||||
first = false
|
// Classify the line. Definition-list syntax is inert inside fenced code
|
||||||
need_blank = true
|
// blocks, so fence delimiters and their contents are classified Other.
|
||||||
i = next
|
kind: Line_Kind
|
||||||
|
if in_fence {
|
||||||
|
if is_fence_line(line, fence_marker) {
|
||||||
|
in_fence = false
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if need_blank {
|
|
||||||
strings.write_string(&sb, "\n\n")
|
|
||||||
need_blank = false
|
|
||||||
} else if !first {
|
|
||||||
strings.write_string(&sb, "\n")
|
|
||||||
}
|
|
||||||
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)
|
return strings.clone(strings.to_string(sb), allocator)
|
||||||
}
|
}
|
||||||
|
|
||||||
// try_match_deflist attempts to match a definition list group starting at
|
// line_at returns the line beginning at byte offset `pos` (excluding its
|
||||||
// lines[start]. A group is one or more term+definition pairs. Returns the
|
// trailing newline) along with the offset of the following line. `pos` must be
|
||||||
// matched entries, whether a match was found, and the index past the group.
|
// 0 or one past a '\n'. ok is false once `pos` reaches the end of `body`.
|
||||||
try_match_deflist :: proc(
|
line_at :: proc(body: string, pos: int) -> (line: string, next: int, ok: bool) {
|
||||||
lines: []string,
|
if pos >= len(body) {
|
||||||
start: int,
|
return "", pos, false
|
||||||
) -> (
|
}
|
||||||
entries: [dynamic]DefList_Entry,
|
nl := strings.index_byte(body[pos:], '\n')
|
||||||
ok: bool,
|
if nl < 0 {
|
||||||
end: int,
|
return body[pos:], len(body), true
|
||||||
) {
|
}
|
||||||
entries = make([dynamic]DefList_Entry, 0, allocator = context.temp_allocator)
|
return body[pos:pos + nl], pos + nl + 1, true
|
||||||
end = start
|
|
||||||
|
|
||||||
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]) {
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look for a def line: immediately after or with one blank line
|
// has_def_line reports whether any line in `body` is a definition line (see
|
||||||
def_idx := i + 1
|
// is_def_line). It is a cheap guard for convert_deflists: a body with no
|
||||||
if def_idx < len(lines) && is_blank_line(lines[def_idx]) {
|
// definition line cannot contain a definition list. There are no false
|
||||||
def_idx += 1
|
// 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`.
|
||||||
if def_idx >= len(lines) || !is_def_line(lines[def_idx]) {
|
has_def_line :: proc(body: string) -> bool {
|
||||||
break
|
pos := 0
|
||||||
}
|
for {
|
||||||
|
line, next := line_at(body, pos) or_break
|
||||||
// Found a term + def pair
|
if is_def_line(line) {
|
||||||
append(
|
|
||||||
&entries,
|
|
||||||
DefList_Entry {
|
|
||||||
term = strings.trim_space(lines[i]),
|
|
||||||
definition = def_content(lines[def_idx]),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
i = def_idx + 1
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(entries) > 0 {
|
|
||||||
ok = true
|
|
||||||
end = i
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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] != ':' {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
if len(trimmed) == 1 {
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return trimmed[1] == ' ' || trimmed[1] == '\t'
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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_ok || !is_def_line(def_line) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
}
|
||||||
|
|
||||||
|
strings.write_string(sb, "</dl>")
|
||||||
|
return pos
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
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 rest[1] == ' ' || rest[1] == '\t'
|
||||||
}
|
}
|
||||||
|
|
||||||
// def_content extracts the definition text from a definition line,
|
// 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) == ""
|
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
|
// render_inline_md renders a snippet of markdown through cmark and strips
|
||||||
// the surrounding <p> tags. Result lives in context.temp_allocator.
|
// the surrounding <p> tags. Result lives in context.temp_allocator.
|
||||||
render_inline_md :: proc(text: string) -> string {
|
render_inline_md :: proc(text: string) -> string {
|
||||||
|
|||||||
@@ -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
@@ -0,0 +1,2 @@
|
|||||||
|
Term
|
||||||
|
: Definition content
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
@@ -0,0 +1,6 @@
|
|||||||
|
Outer Term
|
||||||
|
: Outer Definition
|
||||||
|
|
||||||
|
Inner Term
|
||||||
|
: Inner Definition
|
||||||
|
: Another Inner Definition
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
Term: Definition
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
Term
|
||||||
|
: Definition
|
||||||
+11
@@ -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
@@ -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`
|
||||||
@@ -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
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Term: Definition
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<dl><dt>Term</dt><dd>Definition</dd></dl>
|
||||||
@@ -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>
|
||||||
@@ -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
@@ -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>
|
||||||
|
- > 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>
|
||||||
Reference in New Issue
Block a user