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": { "attrs": {
"url": "b](c" "url": "b](c)"
}, },
"type": "inlineCard" "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\"}"} ::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 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 with JSON string escaping (`\"` `\\` `\n` `\t` `\uXXXX`, …) — total over
Unicode, and raw newlines never appear inside quotes. A quoted value also escapes `` ` ``, `&`, Unicode, and raw newlines never appear inside quotes. A quoted value also escapes `` ` ``, `&`,
`<` and `|` as `\u0060`, `\u0026`, `\u003c` and `\u007c`; `*`, `_`, `~`, `[` and `(` resolve after a `<` and `|` as `\u0060`, `\u0026`, `\u003c` and `\u007c`; `*`, `_`, `~`, `[`, `]` and `(` resolve
directive binds and stay raw. The closing `}` is the first one outside quotes, since a quoted value after a directive binds and stay raw. The closing `}` is the first one outside quotes, since a
holds `}` unescaped. All values are strings at the grammar level; each node's section assigns types. 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 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 quotes in the shortest form each escape has, and omits empty `{attrs}` except where the `{` itself
claims the directive (`:hardBreak{}`). 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 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, `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 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 inline layer spells as syntax: a code span, an autolink, a link destination or title. A `|` there
that pipe and the table takes the directive form. A pipe table parses back to exactly that shape. takes the directive form instead. A pipe table parses back to exactly that shape.
``` ```
| Part | Qty | | 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(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({ text: '`', type: 'text' }, marked('x', { type: 'code' })), '\\``x`\n')
assert.equal(emitted(marked('x', { type: 'code' }), { text: '`', type: 'text' }), '`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: '!', 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', 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') assert.equal(emitted({ text: '[a](b)', type: 'text' }), '\\[a](b)\n')
+3 -3
View File
@@ -19,7 +19,7 @@ const delimiters = ['*', '_', '`', '~']
const asciiPunctuation = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/ const asciiPunctuation = /[!"#$%&'()*+,\-./:;<=>?@[\\\]^_`{|}~]/
const htmlConstructs = [/^<[!?]/, /^<\/?[A-Za-z][A-Za-z0-9-]*(?:[\s/>]|$)/, /^<[^\s<>@]+@[^\s<>@]+>/] const htmlConstructs = [/^<[!?]/, /^<\/?[A-Za-z][A-Za-z0-9-]*(?:[\s/>]|$)/, /^<[^\s<>@]+@[^\s<>@]+>/]
const inlineDirectiveOpener = /^:[a-z][A-Za-z0-9]*[[{]/ const inlineDirectiveOpener = /^:[a-z][A-Za-z0-9]*[[{]/
const linkTextCloser = /[([:]/ const followsLinkText = /[([:]/
const unicodePunctuation = /[\p{P}\p{S}]/u const unicodePunctuation = /[\p{P}\p{S}]/u
export function assembleInlineLine(segments: readonly InlineSegment[], container: LineContainer): AssembledLine { export function assembleInlineLine(segments: readonly InlineSegment[], container: LineContainer): AssembledLine {
@@ -179,11 +179,11 @@ function claimsCharacter(
return false 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 { function opensLink(scan: string, escapings: readonly (InlineEscaping | undefined)[], index: number): boolean {
for (let cursor = index + 1; cursor < scan.length; cursor += 1) { for (let cursor = index + 1; cursor < scan.length; cursor += 1) {
if (scan.charAt(cursor) !== ']' || isSyntax(escapings[cursor])) continue 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 return false
} }