Keep the link-opener scan out of emitted syntax, which binds first
CI / gate (push) Successful in 5s
CI / gate (push) Successful in 5s
This commit is contained in:
@@ -34,6 +34,21 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "extension"
|
"type": "extension"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "[a",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"url": "b](c"
|
||||||
|
},
|
||||||
|
"type": "inlineCard"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "doc",
|
"type": "doc",
|
||||||
|
|||||||
@@ -3,3 +3,5 @@ a `b :inlineCard{url="c\u0060d"}
|
|||||||
:inlineCard{url="https://example.com/s?q=a\u0026amp;b\u003cc"}
|
:inlineCard{url="https://example.com/s?q=a\u0026amp;b\u003cc"}
|
||||||
|
|
||||||
::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"}
|
||||||
|
|||||||
@@ -256,6 +256,8 @@ test('escapes a literal delimiter that would merge with an emitted one', () => {
|
|||||||
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: '!', type: 'text' }, marked('x', { attrs: { href: 'https://example.com/' }, type: 'link' })), '\\\n')
|
assert.equal(emitted({ text: '!', type: 'text' }, marked('x', { attrs: { href: 'https://example.com/' }, type: 'link' })), '\\\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(marked('x', { type: 'underline' }), { text: '{}', type: 'text' }), ':underline[x]\\{}\n')
|
assert.equal(emitted(marked('x', { type: 'underline' }), { text: '{}', type: 'text' }), ':underline[x]\\{}\n')
|
||||||
assert.equal(emitted({ attrs: { text: '' }, type: 'status' }, { text: '{color=red}', type: 'text' }), ':status[]\\{color=red}\n')
|
assert.equal(emitted({ attrs: { text: '' }, type: 'status' }, { text: '{color=red}', type: 'text' }), ':status[]\\{color=red}\n')
|
||||||
assert.equal(emitted(marked('x', { attrs: { href: 'https://example.com/' }, type: 'link' }), { text: '{}', type: 'text' }), '[x](https://example.com/){}\n')
|
assert.equal(emitted(marked('x', { attrs: { href: 'https://example.com/' }, type: 'link' }), { text: '{}', type: 'text' }), '[x](https://example.com/){}\n')
|
||||||
|
|||||||
@@ -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 linkOpener = /\](?=[([:])/
|
const linkTextCloser = /[([:]/
|
||||||
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 {
|
||||||
@@ -65,7 +65,7 @@ function escape(segments: readonly InlineSegment[], container: LineContainer): A
|
|||||||
for (let index = 0; index < scan.length; index += 1) {
|
for (let index = 0; index < scan.length; index += 1) {
|
||||||
const escaping = escapings[index]
|
const escaping = escapings[index]
|
||||||
const escapable = escaping === 'backslash' || escaping === 'bracketed'
|
const escapable = escaping === 'backslash' || escaping === 'bracketed'
|
||||||
if (escapable && (mergesWithSyntax(scan, escapings, index) || opensConstruct(scan, index, escaping === 'bracketed', container, escaped))) {
|
if (escapable && (mergesWithSyntax(scan, escapings, index) || opensConstruct(scan, escapings, index, escaping === 'bracketed', container, escaped))) {
|
||||||
output += '\\'
|
output += '\\'
|
||||||
escaped.add(index)
|
escaped.add(index)
|
||||||
}
|
}
|
||||||
@@ -131,10 +131,17 @@ function isSyntax(escaping: InlineEscaping | undefined): boolean {
|
|||||||
return escaping === 'none'
|
return escaping === 'none'
|
||||||
}
|
}
|
||||||
|
|
||||||
function opensConstruct(scan: string, index: number, inBrackets: boolean, container: LineContainer, escaped: ReadonlySet<number>): boolean {
|
function opensConstruct(
|
||||||
|
scan: string,
|
||||||
|
escapings: readonly (InlineEscaping | undefined)[],
|
||||||
|
index: number,
|
||||||
|
inBrackets: boolean,
|
||||||
|
container: LineContainer,
|
||||||
|
escaped: ReadonlySet<number>,
|
||||||
|
): boolean {
|
||||||
if (container === 'heading' && closesHeading(scan, index)) return true
|
if (container === 'heading' && closesHeading(scan, index)) return true
|
||||||
if (container === 'paragraph' && claimsLineStart(scan, index)) return true
|
if (container === 'paragraph' && claimsLineStart(scan, index)) return true
|
||||||
return claimsCharacter(scan, index, inBrackets, container, escaped)
|
return claimsCharacter(scan, escapings, index, inBrackets, container, escaped)
|
||||||
}
|
}
|
||||||
|
|
||||||
function claimsLineStart(scan: string, index: number): boolean {
|
function claimsLineStart(scan: string, index: number): boolean {
|
||||||
@@ -150,7 +157,14 @@ function closesHeading(scan: string, index: number): boolean {
|
|||||||
return index === 0 || /[ \t]/.test(scan.charAt(index - 1))
|
return index === 0 || /[ \t]/.test(scan.charAt(index - 1))
|
||||||
}
|
}
|
||||||
|
|
||||||
function claimsCharacter(scan: string, index: number, inBrackets: boolean, container: LineContainer, escaped: ReadonlySet<number>): boolean {
|
function claimsCharacter(
|
||||||
|
scan: string,
|
||||||
|
escapings: readonly (InlineEscaping | undefined)[],
|
||||||
|
index: number,
|
||||||
|
inBrackets: boolean,
|
||||||
|
container: LineContainer,
|
||||||
|
escaped: ReadonlySet<number>,
|
||||||
|
): boolean {
|
||||||
const character = scan.charAt(index)
|
const character = scan.charAt(index)
|
||||||
const rest = scan.slice(index)
|
const rest = scan.slice(index)
|
||||||
if (inBrackets && (character === '[' || character === ']')) return true
|
if (inBrackets && (character === '[' || character === ']')) return true
|
||||||
@@ -159,12 +173,21 @@ function claimsCharacter(scan: string, index: number, inBrackets: boolean, conta
|
|||||||
if (character === '&') return startsEntityReference(rest)
|
if (character === '&') return startsEntityReference(rest)
|
||||||
if (character === '<') return opensBracketedAutolink(rest) || htmlConstructs.some((construct) => construct.test(rest))
|
if (character === '<') return opensBracketedAutolink(rest) || htmlConstructs.some((construct) => construct.test(rest))
|
||||||
if (character === ':') return inlineDirectiveOpener.test(rest)
|
if (character === ':') return inlineDirectiveOpener.test(rest)
|
||||||
if (character === '[') return linkOpener.test(rest)
|
if (character === '[') return opensLink(scan, escapings, index)
|
||||||
if (character === '`') return opensCodeSpan(scan, index, escaped)
|
if (character === '`') return opensCodeSpan(scan, index, escaped)
|
||||||
if (character === '*' || character === '_' || character === '~') return opensEmphasis(scan, index, escaped)
|
if (character === '*' || character === '_' || character === '~') return opensEmphasis(scan, index, escaped)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A `]` the emitter spelled closes the construct it belongs to, which binds first — never link text.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
function opensCodeSpan(scan: string, index: number, escaped: ReadonlySet<number>): boolean {
|
function opensCodeSpan(scan: string, index: number, escaped: ReadonlySet<number>): boolean {
|
||||||
if (!startsRun(scan, index, escaped)) return false
|
if (!startsRun(scan, index, escaped)) return false
|
||||||
const length = runLength(scan, index)
|
const length = runLength(scan, index)
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ detail is settled at its own milestone.
|
|||||||
inline alike — the constructs those four open all bind at or before a directive does, and
|
inline alike — the constructs those four open all bind at or before a directive does, and
|
||||||
nothing else reaches into `{attrs}`. Emitted attributes being inert leaves 3 free to keep
|
nothing else reaches into `{attrs}`. Emitted attributes being inert leaves 3 free to keep
|
||||||
CommonMark's own precedence between a directive and a code span, and collapsed
|
CommonMark's own precedence between a directive and a code span, and collapsed
|
||||||
`escaping: 'attribute'` into `none`.
|
`escaping: 'attribute'` into `none`. The escaper's link-opener scan skips emitted syntax
|
||||||
|
to match: a `](` inside a directive escapes no text `[`.
|
||||||
- [ ] **2e4 — The carry's fallback triggers.** `spec/flavour.md` carries a node its section
|
- [ ] **2e4 — The carry's fallback triggers.** `spec/flavour.md` carries a node its section
|
||||||
cannot spell — an attrs key no section lists, a value that is not the section's type, an
|
cannot spell — an attrs key no section lists, a value that is not the section's type, an
|
||||||
arg slot holding no bare token, marks no nesting spells — where the emitter still refuses,
|
arg slot holding no bare token, marks no nesting spells — where the emitter still refuses,
|
||||||
|
|||||||
Reference in New Issue
Block a user