|
#!/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 "-->" 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" '-->' |
|
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" '&&' |
|
assert_contains "$code" '<pre><code class="language-bash">' |
|
|
|
it "a bare fence is still escaped" |
|
assert_contains "$(md_render '``` |
|
x > y & z |
|
```')" '>' |
|
|
|
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 |
|
---')" '—' |
|
|
|
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 |
|
|