Summary
Link reference definitions that are never referenced leak into the HTML output as paragraphs, instead of being stripped. This affects every flavor that uses the legacy parsing path (default/vanilla, original, ghost). The commonmark and gfm flavors are not affected — the CommonMark definition scanner handles all cases correctly.
Repro
const conv = new showdown.Converter(); // any non-cmSpec flavor
conv.makeHtml('[unused]: http://example.com/');
// actual: "<p>[unused]: http://example.com/</p>"
// expected: ""
Not limited to document boundaries — any unreferenced definition leaks:
conv.makeHtml('one\n\n[unused]: /url\n\ntwo');
// actual: "<p>one</p>\n<p>[unused]: /url</p>\n<p>two</p>"
// expected: "<p>one</p>\n<p>two</p>"
Expected behavior
Link reference definitions are metadata and must produce no output, referenced or not:
- Markdown.pl 1.0.1 strips definitions unconditionally (verified against the actual script:
[unused]: http://example.com/ as the sole document content yields empty output).
- CommonMark is explicit about it; spec example 207 is exactly this case (
[foo]: /url → empty output), and our commonmark flavor already passes it.
- specs/original.md covers it under Link reference definitions ("A definition that is never used still produces no output"); the corresponding case is currently skipped in
testsuite.original.js pending this fix.
Root cause
The legacy branch of src/subParsers/makehtml/stripLinkDefinitions.js deliberately backs out of stripping when the link id occurs fewer than two times in the document:
// if there aren't two instances of linkId it must not be a reference link so back out
linkId = showdown.helper.caseFold(linkId);
if (showdown.helper.caseFold(text).split(linkId).length - 1 < 2) {
return wholeMatch;
}
The heuristic itself contradicts both original Markdown and CommonMark, and its implementation makes the outcome depend on coincidental substrings, because it counts raw substring occurrences anywhere in the text rather than actual references:
| input |
output |
why |
[unused]: /url |
leaks as <p> |
id occurs once |
this feature is unused + blank line + [unused]: /url |
stripped |
the word "unused" in prose counts as a second instance |
[a]: /a |
stripped |
the a in its own URL counts |
[zqx]: /url |
leaks |
no coincidental substring anywhere |
Proposed fix
Remove the back-out check and strip definitions unconditionally, matching Markdown.pl, the CommonMark spec, and the behavior of the cmSpec scanner (parseCmLinkDefinitions) in the same file.
Summary
Link reference definitions that are never referenced leak into the HTML output as paragraphs, instead of being stripped. This affects every flavor that uses the legacy parsing path (default/vanilla, original, ghost). The
commonmarkandgfmflavors are not affected — the CommonMark definition scanner handles all cases correctly.Repro
Not limited to document boundaries — any unreferenced definition leaks:
Expected behavior
Link reference definitions are metadata and must produce no output, referenced or not:
[unused]: http://example.com/as the sole document content yields empty output).[foo]: /url→ empty output), and ourcommonmarkflavor already passes it.testsuite.original.jspending this fix.Root cause
The legacy branch of
src/subParsers/makehtml/stripLinkDefinitions.jsdeliberately backs out of stripping when the link id occurs fewer than two times in the document:The heuristic itself contradicts both original Markdown and CommonMark, and its implementation makes the outcome depend on coincidental substrings, because it counts raw substring occurrences anywhere in the text rather than actual references:
[unused]: /url<p>this feature is unused+ blank line +[unused]: /url[a]: /aain its own URL counts[zqx]: /urlProposed fix
Remove the back-out check and strip definitions unconditionally, matching Markdown.pl, the CommonMark spec, and the behavior of the
cmSpecscanner (parseCmLinkDefinitions) in the same file.