Pin the two scans the narrowing left alone, and name what stays raw
CI / gate (push) Successful in 5s

This commit is contained in:
2026-08-27 00:07:44 +02:00
parent 6cbd96748b
commit d4ffdf3cc5
5 changed files with 16 additions and 10 deletions
@@ -43,7 +43,7 @@
},
{
"attrs": {
"url": "b](c"
"url": "b](c)"
},
"type": "inlineCard"
}
@@ -4,4 +4,4 @@ a `b :inlineCard{url="c\u0060d"}
::extension {extensionKey=jira parameters="{\"jql\":\"a\u007cb \u0060c\u0060 \u0026 d \u003c e\"}"}
[a:inlineCard{url="b](c"}
[a:inlineCard{url="b](c)"}
+6 -5
View File
@@ -94,9 +94,10 @@ container is open, a named error otherwise.
valid — no attributes. A bare value matches `[A-Za-z0-9_-]+`; any other value is double-quoted
with JSON string escaping (`\"` `\\` `\n` `\t` `\uXXXX`, …) — total over
Unicode, and raw newlines never appear inside quotes. A quoted value also escapes `` ` ``, `&`,
`<` and `|` as `\u0060`, `\u0026`, `\u003c` and `\u007c`; `*`, `_`, `~`, `[` and `(` resolve after a
directive binds and stay raw. The closing `}` is the first one outside quotes, since a quoted value
holds `}` unescaped. All values are strings at the grammar level; each node's section assigns types.
`<` and `|` as `\u0060`, `\u0026`, `\u003c` and `\u007c`; `*`, `_`, `~`, `[`, `]` and `(` resolve
after a directive binds and stay raw. The closing `}` is the first one outside quotes, since a
quoted value holds `}` unescaped. All values are strings at the grammar level; each node's section
assigns types.
Canonical form orders keys alphabetically, spells values bare wherever allowed, escapes inside
quotes in the shortest form each escape has, and omits empty `{attrs}` except where the `{` itself
claims the directive (`:hardBreak{}`).
@@ -220,8 +221,8 @@ One header row plus plain inline cells is a pipe table; anything richer is the d
and every cell carry no attrs and no marks, the first row is all `tableHeader` and the rest all
`tableCell`, every row has the header's cell count, and every cell holds exactly one attr-less,
mark-less paragraph — an empty cell holds one empty paragraph — with no `|` anywhere the
inline layer spells as syntax a code span, a link destination or title: pipe form cannot spell
that pipe and the table takes the directive form. A pipe table parses back to exactly that shape.
inline layer spells as syntax: a code span, an autolink, a link destination or title. A `|` there
takes the directive form instead. A pipe table parses back to exactly that shape.
```
| Part | Qty |
+5
View File
@@ -255,6 +255,11 @@ test('escapes a literal delimiter that would merge with an emitted one', () => {
assert.equal(code(adfToMarkdown(document(paragraph({ text: 'x', type: 'text' }, marked('~a', { type: 'strike' }))))), 'unspellable-mark')
assert.equal(emitted({ text: '`', type: 'text' }, marked('x', { type: 'code' })), '\\``x`\n')
assert.equal(emitted(marked('x', { type: 'code' }), { text: '`', type: 'text' }), '`x`\\`\n')
assert.equal(emitted({ text: '`a', type: 'text' }, marked('b', { type: 'code' })), '\\`a`b`\n')
assert.equal(
emitted({ text: '<https://example.com/', type: 'text' }, { attrs: { url: 'x>' }, type: 'inlineCard' }),
'\\<https://example.com/:inlineCard{url="x>"}\n',
)
assert.equal(emitted({ text: '!', type: 'text' }, marked('x', { attrs: { href: 'https://example.com/' }, type: 'link' })), '\\![x](https://example.com/)\n')
assert.equal(emitted({ text: '[a', type: 'text' }, marked('x', { attrs: { href: 'https://example.com/' }, type: 'link' })), '[a[x](https://example.com/)\n')
assert.equal(emitted({ text: '[a](b)', type: 'text' }), '\\[a](b)\n')
+3 -3
View File
@@ -19,7 +19,7 @@ const delimiters = ['*', '_', '`', '~']
const asciiPunctuation = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/
const htmlConstructs = [/^<[!?]/, /^<\/?[A-Za-z][A-Za-z0-9-]*(?:[\s/>]|$)/, /^<[^\s<>@]+@[^\s<>@]+>/]
const inlineDirectiveOpener = /^:[a-z][A-Za-z0-9]*[[{]/
const linkTextCloser = /[([:]/
const followsLinkText = /[([:]/
const unicodePunctuation = /[\p{P}\p{S}]/u
export function assembleInlineLine(segments: readonly InlineSegment[], container: LineContainer): AssembledLine {
@@ -179,11 +179,11 @@ function claimsCharacter(
return false
}
// A `]` the emitter spelled closes the construct it belongs to, which binds first — never link text.
// A `]` the emitter spelled sits inside a construct that binds before link text does.
function opensLink(scan: string, escapings: readonly (InlineEscaping | undefined)[], index: number): boolean {
for (let cursor = index + 1; cursor < scan.length; cursor += 1) {
if (scan.charAt(cursor) !== ']' || isSyntax(escapings[cursor])) continue
if (linkTextCloser.test(scan.charAt(cursor + 1))) return true
if (followsLinkText.test(scan.charAt(cursor + 1))) return true
}
return false
}