ssh git@grit.deadkey.org man

mailmanpat/markdown.awk · 2026-09-05 17:01:23 +08002026-9-5 +8git clone …

FILES / test / regressions.sh

NameLast CommitTimestampSize
..
assert.bash 6b66487 h Extract the markdown parser from jenny 26-09-05 09:57 3 KB
regressions.sh e8441ae h Let a thematic break end a quoted paragraph 26-09-05 17:01 10 KB
run.sh 6b66487 h Extract the markdown parser from jenny 26-09-05 09:57 383 B
units.sh ba20446 h Fold the probe after each decode, not only before the first 26-09-05 10:46 20 KB
#!/usr/bin/env bash
# Regression tests: one case per bug the parser has had.
# Run: test/regressions.sh (or test/run.sh for everything)
set -uo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/assert.bash"
REPO_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
AWK="${AWK:-awk}"
GREP="${GREP:-grep}"
md_render() {
printf '%s\n' "$1" | "$AWK" -f "$REPO_ROOT/markdown.awk"
}
md_unsafe() {
printf '%s\n' "$1" | "$AWK" -v UNSAFE_HTML=1 -f "$REPO_ROOT/markdown.awk"
}
suite "nested lists"
# The nesting machinery existed but the indent unit was a tab or four spaces,
# while a child is conventionally indented to its parent's content column --
# two spaces for "- ". Two-space nesting, the common form, came out flat.
it "a two-space child nests inside its parent"
out=$(md_render '- a
- b
- c')
assert_contains "$out" '<li>
a
<ul>'
it "a sibling after a nested list rejoins the outer list"
# The closer walked past the target depth because it compared against pblock,
# which is assigned nowhere and so was always empty. It closed the outer <ul>
# too, and the sibling opened a second one.
assert_eq "$(printf '%s' "$out" | "$GREP" -o '<ul>' | wc -l | tr -d ' ')" "2"
assert_eq "$(printf '%s' "$out" | "$GREP" -o '</ul>' | wc -l | tr -d ' ')" "2"
it "three levels of two-space nesting all nest"
deep=$(md_render '- a
- b
- c
- d')
assert_eq "$(printf '%s' "$deep" | "$GREP" -o '<ul>' | wc -l | tr -d ' ')" "3"
it "four-space nesting still works"
# Two spaces is the step, not an alternative to four: written as an
# alternation with four first, a four-space indent matched the wide branch and
# peeled only one level, collapsing three levels into two.
four=$(md_render '- a
- b
- c')
assert_eq "$(printf '%s' "$four" | "$GREP" -o '<ul>' | wc -l | tr -d ' ')" "2"
it "a flat list is unaffected"
flat=$(md_render '- a
- b
- c')
assert_eq "$(printf '%s' "$flat" | "$GREP" -o '<ul>' | wc -l | tr -d ' ')" "1"
assert_eq "$(printf '%s' "$flat" | "$GREP" -o '<li>' | wc -l | tr -d ' ')" "3"
it "ordered lists nest too"
onest=$(md_render '1. a
1. b
2. c')
assert_eq "$(printf '%s' "$onest" | "$GREP" -o '<ol>' | wc -l | tr -d ' ')" "2"
suite "strikethrough"
it "doubled tildes become del"
assert_contains "$(md_render 'Plain ~~struck~~ here.')" '<del>struck</del>'
it "text after a strike containing emphasis is not lost"
# nextil shares t1/tag/t2 globally, so parsing the struck text clobbered the
# tail still waiting in the outer frame -- the rest of the line vanished.
assert_contains "$(md_render '~~*it*~~ TAIL')" '<del><em>it</em></del> TAIL'
it "two strikes on one line both close"
two=$(md_render '~~one~~ mid ~~*two*~~ end')
assert_eq "$(printf '%s' "$two" | "$GREP" -o '<del>' | wc -l | tr -d ' ')" "2"
assert_contains "$two" 'end'
it "a single tilde stays literal"
# "~/path" and "approx ~5" must survive untouched.
assert_contains "$(md_render 'Path ~/x and approx ~5 items.')" '~/x and approx ~5'
it "an unpaired double tilde stays literal"
# Rather than opening a <del> that runs to the end of the document.
assert_contains "$(md_render 'Unpaired ~~ stays.')" 'Unpaired ~~ stays.'
it "tildes inside code are untouched"
assert_contains "$(md_render 'A `~~code~~` span.')" '<code>~~code~~</code>'
# These exercise how underscores interact with real tags, which is only
# observable while tags survive -- so they run in the opt-in passthrough mode.
# The escaping default has its own tests; what is checked here is the parsing
# rule, not which mode is the default.
suite "underscores in inline HTML"
it "an underscore that is a tag's whole content is literal"
# <span class="us">_</span> writes a visible underscore. It opened an <em>
# that swallowed the markup after it.
assert_contains "$(md_unsafe 'x <span class="us">_</span> y')" '<span class="us">_</span>'
it "two such underscores do not pair across their tags"
# They emitted mis-nested "<b><em></b> ... <b></em></b>".
pair=$(md_unsafe 'A <b>_</b> and <b>_</b> here.')
assert_contains "$pair" '<b>_</b> and <b>_</b>'
assert_not_contains "$pair" '<em>'
it "real emphasis still works"
assert_contains "$(md_render 'Real _emphasis_ here.')" '<em>emphasis</em>'
it "bold still works"
assert_contains "$(md_render 'Real __bold__ here.')" '<strong>bold</strong>'
it "emphasis spanning a tag still works"
assert_contains "$(md_unsafe 'A _em <b>x</b> more_ end.')" '<em>em <b>x</b> more</em>'
it "intraword underscores are still literal"
assert_contains "$(md_render 'Use node_modules and snake_case_name.')" 'node_modules and snake_case_name'
suite "headings are addressable"
# A document long enough to link within needs its headings to be targets. Any
# "#section" link -- from its own contents list, from a sibling page, from
# somewhere else entirely -- resolves to nothing without them. Publishing a
# real thesis found 23 such links pointing at nothing.
it "a heading carries an id made from its words"
assert_contains "$(md_render '## Vision')" '<h2 id="vision">'
it "punctuation and spacing collapse to single dashes"
assert_contains "$(md_render '## Scope (what MDD is not)')" 'id="scope-what-mdd-is-not"'
it "inline markup is stripped before slugging"
# A heading may be emphasised or hold code; "<code>Vision</code>" must still
# slug to "vision".
assert_contains "$(md_render '## The `Blueprint` Artifact')" 'id="the-blueprint-artifact"'
assert_contains "$(md_render '## Scope (what MDD is *not*)')" 'id="scope-what-mdd-is-not"'
it "every level is addressable"
assert_contains "$(md_render '# One')" '<h1 id="one">'
assert_contains "$(md_render '###### Six')" '<h6 id="six">'
it "a heading with no sluggable characters gets no id"
# An empty id is worse than none: it is not a valid target and would collide
# with every other unsluggable heading.
out=$(md_render '## ---')
assert_not_contains "$out" 'id=""'
it "a paragraph carries a deep-link id"
# Headings were addressable and paragraphs were not, so a reader could link to
# a section but not to the sentence they meant.
assert_contains "$(md_render 'Just a paragraph.')" 'id="p-'
it "a paragraph's id is derived from its own text, not its position"
# The point of hashing the text: inserting a paragraph above must not move the
# anchor of the one below it. A positional id would keep resolving after the
# insertion and quietly point at the wrong paragraph, which is worse than
# breaking, because the link still looks like it works.
before=$(md_render 'Alpha text.
Beta text.' | "$GREP" -o 'id="p-[a-f0-9]*"' | tail -1)
after=$(md_render 'Inserted first.
Alpha text.
Beta text.' | "$GREP" -o 'id="p-[a-f0-9]*"' | tail -1)
assert_eq "$before" "$after"
it "an id changes when the paragraph's own words change"
# The other half of the contract, and the honest failure: the text someone
# linked to is not there any more, so the link should not silently land on
# something else.
one=$(md_render 'Alpha text.' | "$GREP" -o 'id="p-[a-f0-9]*"')
two=$(md_render 'Alpha text edited.' | "$GREP" -o 'id="p-[a-f0-9]*"')
if [ "$one" != "$two" ]; then pass; else fail "id did not change with the text: $one"; fi
it "repeated text still gets unique ids"
# Two paragraphs with the same words hash the same; ids on one page must not
# collide or the second is unreachable.
dup=$(md_render 'Same words.
Same words.' | "$GREP" -c 'id="p-')
assert_eq "$dup" "2"
assert_contains "$(md_render 'Same words.
Same words.')" '-2"'
suite "diagrams"
# A diagram fence is held for a client-side renderer instead of being shown as
# source. jenny ships no renderer and pulls nothing from a CDN -- these test
# the fence rule and the opt-in hook only; the bundle, if a site wants one, is
# the site's own file.
it "a mermaid fence becomes a diagram div"
assert_contains "$(md_render '```mermaid
flowchart LR
A --> B
```')" '<div class="diagram diagram-mermaid">'
it "diagram source is not HTML-escaped"
# The renderer reads the element's text content and parses it, so escaping
# "-->" to "--&gt;" would break every arrow in the diagram. This is the one
# place a fence's content is left raw.
dia=$(md_render '```mermaid
flowchart LR
A["x<br/>y"] -->|"label"| B
```')
assert_contains "$dia" '-->'
assert_not_contains "$dia" '--&gt;'
assert_contains "$dia" '<br/>'
it "the diagram fence closes with a div, not a code block"
assert_contains "$dia" '</div>'
assert_not_contains "$dia" '</code></pre>'
it "the legend variant keeps its own class"
assert_contains "$(md_render '```mermaid-legend
flowchart LR
A --> B
```')" 'diagram-mermaid-legend'
it "an ordinary code fence is still escaped"
# Only the fixed set of diagram languages is raw; everything else escapes as
# before, so a document cannot introduce a new raw language.
code=$(md_render '```bash
echo "a" && b > c
```')
assert_contains "$code" '&amp;&amp;'
assert_contains "$code" '<pre><code class="language-bash">'
it "a bare fence is still escaped"
assert_contains "$(md_render '```
x > y & z
```')" '&gt;'
it "a language whose name merely starts with mermaid is not raw"
notmer=$(md_render '```mermaidish
a > b
```')
assert_contains "$notmer" '<pre><code'
assert_not_contains "$notmer" '<div class="diagram'
suite "a break after a quote closes it"
# CommonMark is explicit that
#
# > quote
# ---
#
# is a quoted paragraph followed by a thematic break. Two rules were wrong
# here at once: the setext underline reached into the quote and made the text
# an <h2> still inside it, and once that was stopped the break rule would not
# fire either -- it required no pending text -- so the dashes were swallowed
# into the paragraph as an em-dash.
it "a break after a quoted line closes the quote"
out=$(md_render '> quote
---')
assert_contains "$out" '<p id="p-48bc76c8">quote</p>'
assert_contains "$out" '</blockquote>'
it "the break lands outside the quote"
# The order is what matters: the hr must follow the closing tag, not sit
# inside it.
out=$(md_render '> quote
---' | tr '\n' ' ')
assert_contains "$out" '</blockquote> <hr>'
it "the dashes are not absorbed into the paragraph"
assert_not_contains "$(md_render '> quote
---')" '&#8212;'
it "asterisks and underscores behave the same"
assert_contains "$(md_render '> q
***' | tr '\n' ' ')" '</blockquote> <hr>'
assert_contains "$(md_render '> q
___' | tr '\n' ' ')" '</blockquote> <hr>'
it "a setext underline outside a quote still makes a heading"
assert_contains "$(md_render 'Title
---')" '<h2 id="title">Title</h2>'
assert_contains "$(md_render 'Title
===')" '<h1 id="title">Title</h1>'
it "a quote at the end of the document is closed"
assert_contains "$(md_render '> quote')" '<p id="p-48bc76c8">quote</p>'
summary