Compare commits

..

3 Commits

7 changed files with 119 additions and 16 deletions
@@ -0,0 +1,73 @@
{
"content": [
{
"content": [
{
"text": "`` ``",
"type": "text"
},
{
"marks": [
{
"type": "code"
}
],
"text": "a",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"marks": [
{
"type": "underline"
}
],
"text": "``",
"type": "text"
},
{
"text": "``",
"type": "text"
},
{
"marks": [
{
"type": "code"
}
],
"text": "a",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"text": "``\\`",
"type": "text"
},
{
"marks": [
{
"type": "code"
}
],
"text": "`",
"type": "text"
},
{
"text": "`",
"type": "text"
}
],
"type": "paragraph"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,5 @@
\`\` \`\``a`
:underline[\`\`]\`\``a`
\`\`\\\``` ` ``\`
+4 -2
View File
@@ -54,8 +54,10 @@ normalizes to it through the round-trip.
- Entity references in input decode to their characters; output backslash-escapes only where text - Entity references in input decode to their characters; output backslash-escapes only where text
would otherwise parse as syntax, scanning the assembled line rather than each text node: escape would otherwise parse as syntax, scanning the assembled line rather than each text node: escape
the leading delimiter of a construct that would otherwise open, re-scan from there, and repeat. the leading delimiter of a construct that would otherwise open, re-scan from there, and repeat.
A backtick run escapes whole, and a lone backtick escapes wherever an escaped one follows it in A backtick run escapes whole, and a bare one escapes wherever a later backtick string of its
the same inline content: CommonMark reads no escape inside a code span, so `` \` `` closes one. length forms around an escape in the same inline content — an escaped backtick alone, one joined
to the bare run after it, or a bare run an escape splits off: CommonMark reads no escape inside a
code span, so such a string still closes one.
Where a paragraph opens with what reads as a link reference definition, which resolves before Where a paragraph opens with what reads as a link reference definition, which resolves before
any inline construct binds (a `]` inside a code span or `{attrs}` counts), an opening text `[` any inline construct binds (a `]` inside a code span or `{attrs}` counts), an opening text `[`
escapes and an opening link's nodes ride the carry. escapes and an opening link's nodes ride the carry.
+1 -1
View File
@@ -427,6 +427,6 @@ test('generated markdown refuses, or what it parses to refuses to emit, or its s
) )
if (!parameters.gate) return if (!parameters.gate) return
assert.ok(fixpoints >= fixpointFloor, `${fixpoints} of ${gateRuns} runs reached the fixpoint, under the floor of ${fixpointFloor}`) assert.ok(fixpoints >= fixpointFloor, `${fixpoints} of ${gateRuns} runs reached the fixpoint, under the floor of ${fixpointFloor}`)
assert.ok(directiveShaped >= directiveShapedFloor, `${directiveShaped} runs reaching the fixpoint held a node or mark only a directive or the carry spells, under the floor of ${directiveShapedFloor}`) assert.ok(directiveShaped >= directiveShapedFloor, `${directiveShaped} runs reaching the fixpoint held a node or mark outside CommonMark's own types, under the floor of ${directiveShapedFloor}`)
}) })
+24 -8
View File
@@ -103,17 +103,33 @@ function escapedIndexes(scan: string, escapings: readonly InlineEscaping[], cont
escaped.add(index) escaped.add(index)
} }
} }
escapeLoneBackticks(scan, escapings, escaped) escapeClosedRuns(scan, escapings, escaped)
return escaped return escaped
} }
// CommonMark reads no escape inside a code span, so an escaped backtick still closes one a bare lone backtick before it opens. // CommonMark reads no escape inside a code span, so a backtick string an escape forms or splits off still closes one an earlier bare run opens.
function escapeLoneBackticks(scan: string, escapings: readonly InlineEscaping[], escaped: Set<number>): void { function escapeClosedRuns(scan: string, escapings: readonly InlineEscaping[], escaped: Set<number>): void {
let escapedAfter = false const formed = new Set<number>()
for (let index = scan.length - 1; index >= 0; index -= 1) { let end = scan.length - 1
if (scan.charAt(index) !== '`') continue while (end >= 0) {
if (escapedAfter && escapings[index] !== 'none' && scan.charAt(index - 1) !== '`' && scan.charAt(index + 1) !== '`') escaped.add(index) if (scan.charAt(end) !== '`') {
if (escaped.has(index)) escapedAfter = true end -= 1
continue
}
let start = end
while (scan.charAt(start - 1) === '`') start -= 1
let segmentEnd = end
for (let index = end; index > start; index -= 1) {
if (!escaped.has(index)) continue
formed.add(segmentEnd - index + 1)
segmentEnd = index - 1
}
if (segmentEnd !== end || escaped.has(start)) formed.add(segmentEnd - start + 1)
else if (escapings[start] !== 'none' && formed.has(end - start + 1)) {
for (let index = start; index <= end; index += 1) escaped.add(index)
formed.add(1)
}
end = start - 1
} }
} }
+6 -1
View File
@@ -85,6 +85,10 @@ const marks = fc.uniqueArray(mark, { maxLength: 3, selector: (held) => held.type
const textNode = fc.record({ marks, text }).map((held): AdfNode => ({ ...held, type: 'text' })) const textNode = fc.record({ marks, text }).map((held): AdfNode => ({ ...held, type: 'text' }))
const backtickRunNode = fc
.record({ marks: fc.oneof(fc.constant<AdfMark[]>([]), fc.constant<AdfMark[]>([{ type: 'code' }]), marks), text: fc.string({ maxLength: 6, minLength: 1, unit: fc.constantFrom('`', '``', ' ', 'a') }) })
.map((held): AdfNode => ({ ...held, type: 'text' }))
const autolinkTextNode = fc const autolinkTextNode = fc
.record({ href: fc.tuple(fc.constantFrom('ab:', 'http://'), textOf(0)).map(([scheme, rest]) => `${scheme}${rest}`), marks }) .record({ href: fc.tuple(fc.constantFrom('ab:', 'http://'), textOf(0)).map(([scheme, rest]) => `${scheme}${rest}`), marks })
.map(({ href, marks: held }): AdfNode => ({ marks: [...held.filter((outer) => outer.type !== 'link'), { attrs: { href }, type: 'link' }], text: href, type: 'text' })) .map(({ href, marks: held }): AdfNode => ({ marks: [...held.filter((outer) => outer.type !== 'link'), { attrs: { href }, type: 'link' }], text: href, type: 'text' }))
@@ -119,7 +123,7 @@ const positions = fc.letrec<Positions>((tie) => {
const leafBlocks = blockNodes.filter((entry) => entry.leaf).map((entry) => entry.node) const leafBlocks = blockNodes.filter((entry) => entry.leaf).map((entry) => entry.node)
const containerBlocks = blockNodes.filter((entry) => !entry.leaf).map((entry) => entry.node) const containerBlocks = blockNodes.filter((entry) => !entry.leaf).map((entry) => entry.node)
const misplacedWeight = 7 const misplacedWeight = 7
const paragraph = inlineContent.map((content): AdfNode => ({ content, type: 'paragraph' })) const paragraph = fc.oneof({ arbitrary: inlineContent, weight: 3 }, { arbitrary: fc.array(backtickRunNode, { maxLength: 4, minLength: 2 }), weight: 1 }).map((content): AdfNode => ({ content, type: 'paragraph' }))
const cell = (type: string) => paragraph.map((held): AdfNode => ({ content: [held], type })) const cell = (type: string) => paragraph.map((held): AdfNode => ({ content: [held], type }))
const listItems = fc.array( const listItems = fc.array(
blockContent.map((content): AdfNode => ({ content, type: 'listItem' })), blockContent.map((content): AdfNode => ({ content, type: 'listItem' })),
@@ -150,6 +154,7 @@ const positions = fc.letrec<Positions>((tie) => {
{ depthIdentifier, depthSize: 'small', maxDepth: 4 }, { depthIdentifier, depthSize: 'small', maxDepth: 4 },
{ arbitrary: textNode, weight: 12 }, { arbitrary: textNode, weight: 12 },
{ arbitrary: autolinkTextNode, weight: 2 }, { arbitrary: autolinkTextNode, weight: 2 },
{ arbitrary: backtickRunNode, weight: 3 },
{ arbitrary: fc.oneof(...inlineNodes), weight: 7 }, { arbitrary: fc.oneof(...inlineNodes), weight: 7 },
{ arbitrary: fc.oneof(...blockNodes.map((entry) => entry.node), unknownNode), weight: 2 }, { arbitrary: fc.oneof(...blockNodes.map((entry) => entry.node), unknownNode), weight: 2 },
), ),
+6 -4
View File
@@ -64,10 +64,12 @@ proves 12, 13 spells 11's gaps in 12's grammar, and 12 rewrites code 4b and 4c c
10c's properties as its third user. Under the gate seed the property asserts floors on the 10c's properties as its third user. Under the gate seed the property asserts floors on the
runs reaching the fixpoint and on the directive-shaped ones. It lands when hunts of several runs reaching the fixpoint and on the directive-shaped ones. It lands when hunts of several
hundred thousand runs per engine pass clean, since the breaks hit once per ~150,000 runs, hundred thousand runs per engine pass clean, since the breaks hit once per ~150,000 runs,
past a 10,000-run bar. Two breaks, both shipped in `0.1.0`, are fixed inside it. An escaped past a 10,000-run bar. Two breaks, both shipped in `0.1.0`, are fixed inside it. A backtick
backtick closed an earlier lone backtick's code span, since CommonMark reads no escape string an escape formed closed an earlier bare run's code span, since CommonMark reads no
inside one: a backtick run now escapes whole, and a lone backtick escapes wherever an escape inside one: an escaped backtick alone or, as the review found, one joined to the bare
escaped one follows it in the same inline content. A paragraph's opening read as a link run after it. A backtick run now escapes whole, and a bare run escapes wherever a later
string of its length forms around an escape in the same inline content: the generalized pass
the maintainer chose (2026-09-15). A paragraph's opening read as a link
reference definition across a `]` the emitter spelled: the emitter now escapes the opening reference definition across a `]` the emitter spelled: the emitter now escapes the opening
`[` exactly when the parser's own definition reader accepts the paragraph, and a link `[` exactly when the parser's own definition reader accepts the paragraph, and a link
opening it rides the carry until 13b. opening it rides the carry until 13b.