Files
pad/testdata/wiki_grammar_corpus.json
T
xarmian b6afbb8fca fix(links,web): align the JS wiki-link grammar with Go's . (BUG-2834)
The Go and JS wiki-link patterns were byte-identical source text and did
not mean the same thing. Both spelled the escape alternative `\\.`, but
Go's RE2 `.` excludes only LF while ECMAScript's also excludes CR, U+2028
and U+2029. A body with a backslash immediately before one of those three
was INDEXED by the server and NOT RENDERED by the client: the backlink
panel claimed a link the document refused to draw. CRLF line endings make
the CR case the plausible one.

Measured on both sides before deciding anything, over nine code points.
Exactly three diverge; VT, FF and U+0085 agree, which bounds the
divergence at precisely ECMAScript's LineTerminator set minus LF rather
than leaving it open at "some whitespace controls".

Aligns JS UP to Go (`\\[^\n]`) rather than narrowing Go, for three
independent reasons: the grammar's other alternative already admits RAW
CR/LS/PS in both languages, so narrowing Go would make `[[A<CR>B]]` legal
and `[[A\<CR>B]]` illegal; narrowing Go would stop ExtractWikiLinks
returning rows it currently returns, and the next reconcile would DELETE
them, which is BUG-2805's damage shape; and markdown.ts's own
splitWikiBody already treats `\<CR>` as an escape pair, so only its regex
disagreed. LF stays excluded on both sides — scanBracketBody depends on
that and is untouched.

Also collapses the two duplicate regex literals in markdown.ts into one
exported WIKI_LINK_PATTERN_SOURCE. Exported as source text, not a RegExp
object, because a `/g` regex carries lastIndex and sharing one between a
replace() and a test's exec() would couple them through it.

The harness is the part meant to outlive the fix.
testdata/wiki_grammar_corpus.json is read by BOTH languages, and carries
expectations derived from the grammar spec rather than from either
implementation — comparing the two implementations to each other would
have reproduced the exact blind spot that hid this, since looking
identical is what they already did. Pure ASCII with every control
character as a \uXXXX escape: an early probe typed U+2028/U+2029 into a
shell heredoc, silently lost them, and would have "confirmed" the bug on
two cases that were actually spaces.

JS assertions are split across the node and jsdom projects because
renderMarkdown finishes through DOMPurify and returns '' without a DOM,
where it would fail for a reason unrelated to the grammar. Same split,
same reason, as the existing markdown.shareAttachments pair; the jsdom
file carries a leg proving the DOM path is live, since '' satisfies every
not.toContain assertion.

Negative-controlled both ways: reverting the JS pattern fails exactly 9
assertions (3 corpus + 3 per call site) and nothing else; narrowing the
GO pattern to JS semantics fails the same 3 cases from the other side, so
both halves are live instruments rather than tests that cannot fail.
2026-08-31 23:23:56 +00:00

251 lines
8.3 KiB
JSON

{
"_comment": [
"SHARED cross-language corpus for the wiki-link bracket grammar (BUG-2834).",
"",
"GENERATED FILE -- pure ASCII by construction. Every control character and",
"non-ASCII code point below is a \\uXXXX escape, never a raw byte, so this",
"file survives editors, diffs, terminals and copy-paste intact. An early",
"probe for this very bug typed U+2028/U+2029 into a shell heredoc, silently",
"lost them, and would have 'confirmed' the divergence on two cases that were",
"actually spaces. Do not hand-edit a raw control character into this file.",
"",
"Consumed by BOTH implementations of the grammar, which are written as the",
"same pattern text in two languages that do not agree on what it means:",
"",
" Go: internal/links/extract.go wikiLinkPattern",
" asserted by internal/links/grammar_parity_test.go",
" JS: web/src/lib/utils/markdown.ts renderMarkdown + wikiLinksToMarkdown",
" asserted by web/src/lib/utils/markdown.grammarParity.test.ts",
"",
"THE POINT: neither implementation defines truth. The `expect` fields are",
"derived from the grammar SPEC and both languages are asserted against them,",
"so a divergence introduced on EITHER side fails on that side. This file",
"exists because the two patterns are BYTE-IDENTICAL SOURCE TEXT -- a reviewer",
"comparing them side by side concludes they agree, and BUG-2834 lived in the",
"gap between that reading and the host languages' definition of `.`.",
"",
"THE SPEC. The body production is `(?:\\\\.|[^\\]\\\\])+` where `.` means ANY",
"CHARACTER EXCEPT LINE FEED (U+000A) -- Go's RE2 definition. JavaScript's `.`",
"additionally excludes CR, U+2028 and U+2029, which IS the divergence; the JS",
"patterns therefore spell it `[^\\n]` explicitly rather than `.`.",
"",
"`content` is the FULL input string. `expect.match` is whether the grammar",
"matches anywhere in it; `expect.body` is capture group 1 of the FIRST match",
"and is absent when match is false."
],
"cases": [
{
"name": "plain body",
"why": "baseline: the ordinary form must match in both languages",
"content": "[[Title]]",
"expect": {
"match": true,
"body": "Title"
}
},
{
"name": "pipe display segment",
"why": "the `|` display separator is an ordinary body byte to the grammar",
"content": "[[Title|Display]]",
"expect": {
"match": true,
"body": "Title|Display"
}
},
{
"name": "escaped close bracket",
"why": "the `\\\\.` alternative's primary job: `\\]` must not terminate the body",
"content": "[[A\\]B]]",
"expect": {
"match": true,
"body": "A\\]B"
}
},
{
"name": "escaped pipe",
"why": "escapeWikiBody emits `\\|` for a literal pipe in a title",
"content": "[[A\\|B]]",
"expect": {
"match": true,
"body": "A\\|B"
}
},
{
"name": "escaped backslash",
"why": "escapeWikiBody doubles backslashes first; the pair must be one unit",
"content": "[[A\\\\B]]",
"expect": {
"match": true,
"body": "A\\\\B"
}
},
{
"name": "empty body",
"why": "the production is `+`, so an empty body is not a link",
"content": "[[]]",
"expect": {
"match": false
}
},
{
"name": "escaped close consumes the first of a bare pair",
"why": "`[[A\\]]` has nothing left to close with -- the naive strings.Index scan got this wrong (BUG-2805)",
"content": "[[A\\]]",
"expect": {
"match": false
}
},
{
"name": "escaped close then real close",
"why": "`[[A\\]]]` closes on the LAST two brackets, body keeps the escape",
"content": "[[A\\]]]",
"expect": {
"match": true,
"body": "A\\]"
}
},
{
"name": "trailing backslash with nothing to consume",
"why": "`\\\\.` needs a byte after the backslash; there is none",
"content": "[[A\\",
"expect": {
"match": false
}
},
{
"name": "surplus closing brackets",
"why": "the match ends at the first legal close; the extra `]]` is outside it",
"content": "[[A]]]]",
"expect": {
"match": true,
"body": "A"
}
},
{
"name": "inner open brackets are ordinary body bytes",
"why": "`[` is not excluded by `[^\\]\\\\]`, so the body swallows it -- checks greediness agrees across RE2 and backtracking",
"content": "[[A[[B]]]]",
"expect": {
"match": true,
"body": "A[[B"
}
},
{
"name": "raw LF inside body",
"why": "a RAW line terminator is matched by the `[^\\]\\\\]` alternative in BOTH languages -- this is why `.` excluding them was never a deliberate rule about titles",
"content": "[[A\nB]]",
"expect": {
"match": true,
"body": "A\nB"
}
},
{
"name": "raw CR inside body",
"why": "same as raw LF: the second alternative admits it, so only the ESCAPED form ever diverged",
"content": "[[A\rB]]",
"expect": {
"match": true,
"body": "A\rB"
}
},
{
"name": "backslash before LF",
"why": "BUG-2834 control: Go's `.` excludes LF, so this is NOT an escape pair and the bracket fails -- both languages already agreed here, and the fix must not change it (scanBracketBody in links.go depends on it)",
"content": "[[A\\\nB]]",
"expect": {
"match": false
}
},
{
"name": "backslash before CR",
"why": "BUG-2834 divergence 1 of 3: Go matched, JS did not. CRLF line endings make this the plausible one -- a body ending in a backslash immediately before a CRLF break",
"content": "[[A\\\rB]]",
"expect": {
"match": true,
"body": "A\\\rB"
}
},
{
"name": "backslash before U+2028 LINE SEPARATOR",
"why": "BUG-2834 divergence 2 of 3: excluded by ECMAScript's LineTerminator, not by RE2's",
"content": "[[A\\\u2028B]]",
"expect": {
"match": true,
"body": "A\\\u2028B"
}
},
{
"name": "backslash before U+2029 PARAGRAPH SEPARATOR",
"why": "BUG-2834 divergence 3 of 3",
"content": "[[A\\\u2029B]]",
"expect": {
"match": true,
"body": "A\\\u2029B"
}
},
{
"name": "backslash before U+000B VERTICAL TAB",
"why": "BOUNDS the divergence: a vertical whitespace control that is NOT an ECMAScript LineTerminator. Agreement here is what makes the population exactly three rather than 'the whitespace controls'",
"content": "[[A\\\u000bB]]",
"expect": {
"match": true,
"body": "A\\\u000bB"
}
},
{
"name": "backslash before U+000C FORM FEED",
"why": "BOUNDS the divergence: same role as VT",
"content": "[[A\\\fB]]",
"expect": {
"match": true,
"body": "A\\\fB"
}
},
{
"name": "backslash before U+0085 NEXT LINE",
"why": "BOUNDS the divergence: a Unicode line break that ECMAScript does NOT count as a LineTerminator. The 'it is all Unicode line breaks' hypothesis dies here",
"content": "[[A\\\u0085B]]",
"expect": {
"match": true,
"body": "A\\\u0085B"
}
},
{
"name": "backslash before space",
"why": "ordinary escape of a non-special byte: legal in both, and the negative control for the divergence cases above",
"content": "[[A\\ B]]",
"expect": {
"match": true,
"body": "A\\ B"
}
},
{
"name": "backslash before tab",
"why": "ordinary escape, horizontal whitespace",
"content": "[[A\\\tB]]",
"expect": {
"match": true,
"body": "A\\\tB"
}
},
{
"name": "collection-qualified body",
"why": "the `<slug>/<title>` form is ordinary body text to the GRAMMAR -- the ambiguity it creates is BUG-2830's, decided above the grammar, not inside it",
"content": "[[tasks/Setup]]",
"expect": {
"match": true,
"body": "tasks/Setup"
}
},
{
"name": "cross-workspace form",
"why": "`::` is likewise ordinary body text to the grammar",
"content": "[[team::TASK-5]]",
"expect": {
"match": true,
"body": "team::TASK-5"
}
}
]
}