mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-13 04:08:29 +01:00
Read a link destination holding an unbalanced unescaped parenthesis as no destination
This commit is contained in:
@@ -54,6 +54,10 @@
|
||||
- Every reader leaves `[text]`, `[text][]`, `[text][label]` and a definition `[label]: url` literal
|
||||
when the label runs over 999 characters, where it read them as a link or a definition. CommonMark
|
||||
caps a label at 999 characters, an escape counting as two.
|
||||
- Every reader leaves a link or a definition literal when its unbracketed destination holds a `(`
|
||||
that is neither escaped nor closed by a `)` before the space ending the destination, where it
|
||||
read it as a link: `[a](b(c "t")` is now text, as in CommonMark. Markdown the writers wrote reads
|
||||
as before, since they escape such a parenthesis.
|
||||
- The README compares the package with seven other ADF converters, each checked against its
|
||||
published version.
|
||||
|
||||
|
||||
@@ -494,3 +494,7 @@ Read most (opens and searches):
|
||||
## Reading cost, chunk 6c9ac781b7a6, finished 2026-10-28T10:49:16Z
|
||||
|
||||
306528 new input tokens before the first commit.
|
||||
|
||||
## Reading cost, chunk ccd1a10b3e06, finished 2026-10-28T12:01:54Z
|
||||
|
||||
120318 new input tokens before the first commit.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { test } from 'node:test'
|
||||
|
||||
import { readInlineTarget, targetMemo } from './link-syntax.ts'
|
||||
import { readDestination, readInlineTarget, targetMemo } from './link-syntax.ts'
|
||||
|
||||
// Every `](` of the text, read in order through one memo, as a reader closes its brackets.
|
||||
function targets(text: string): unknown[] {
|
||||
@@ -28,6 +28,16 @@ test('an unbracketed destination ends at its first unmatched `)`', () => {
|
||||
assert.deepEqual(targets('[a](b\\)c)'), [{ destination: 'b)c', length: 6 }])
|
||||
})
|
||||
|
||||
test('a destination holding an unbalanced unescaped `(` at a space or the end reads as none', () => {
|
||||
assert.deepEqual(targets('[a](b(c "t")'), [undefined])
|
||||
assert.deepEqual(targets('[a](b(c "t") [d](e)'), [undefined, { destination: 'e', length: 3 }])
|
||||
assert.deepEqual(targets('[a](b(c) "t")'), [{ destination: 'b(c)', length: 10, title: 't' }])
|
||||
assert.deepEqual(targets('[a](b((c) "t")'), [undefined])
|
||||
assert.equal(readDestination('b(c', 0), undefined)
|
||||
assert.equal(readDestination('b(c d', 0), undefined)
|
||||
assert.deepEqual(readDestination('b(c) d', 0), { length: 4, value: 'b(c)' })
|
||||
})
|
||||
|
||||
test('a target reads whitespace and a title around its destination, bracketed, unbracketed or empty', () => {
|
||||
assert.deepEqual(targets('[a]( b\n "t" ) [c](<d e> \'f\') [g]()'), [
|
||||
{ destination: 'b', length: 10, title: 't' },
|
||||
|
||||
@@ -72,6 +72,13 @@ export function targetMemo(text: string): TargetMemo {
|
||||
return { destinationEnds: undefined, tails: new Map(), text }
|
||||
}
|
||||
|
||||
// Of the starts still open at a stop, only the innermost has its parentheses balanced; the others hold a `(` nothing closed and keep no end.
|
||||
function stopAt(ends: Int32Array, open: number[], index: number): void {
|
||||
const innermost = open.pop()
|
||||
if (innermost !== undefined) ends[innermost] = index
|
||||
open.length = 0
|
||||
}
|
||||
|
||||
// `ends[start]` is where an unbracketed destination from `start` stops, filled in one pass for every start a target can have: just after a `(`, a space or a control character, and 0, where `readDestination`'s run begins.
|
||||
function destinationEnds(text: string): Int32Array {
|
||||
const ends = new Int32Array(text.length + 1)
|
||||
@@ -82,10 +89,10 @@ function destinationEnds(text: string): Int32Array {
|
||||
const start = character === ')' ? open.pop() : undefined
|
||||
if (start !== undefined) ends[start] = index
|
||||
if (character !== ' ' && !holdsControlCharacter(character)) continue
|
||||
for (const stopped of open.splice(0)) ends[stopped] = index
|
||||
stopAt(ends, open, index)
|
||||
open.push(index + 1)
|
||||
}
|
||||
for (const stopped of open) ends[stopped] = text.length
|
||||
stopAt(ends, open, text.length)
|
||||
return ends
|
||||
}
|
||||
|
||||
|
||||
@@ -1031,6 +1031,8 @@ test('leaves the bracket pair no link parses as the text it holds', () => {
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('a ] b\n')), [paragraph('a ] b')])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('[a](/u\n')), [paragraph('[a](/u')])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('[a](/u x)\n')), [paragraph('[a](/u x)')])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('[a](b(c "t")\n')), [paragraph('[a](b(c "t")')])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('[a][r]\n\n[r]: b(c "t"\n')), [paragraph('[a][r]'), paragraph('[r]: b(c "t"')])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('[a](<u\n')), [paragraph('[a](<u')])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('![a\n')), [paragraph('![a')])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('[a [b](/u) c](/v)\n')), [
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 151 | 0.3.0 | | defect | **Read a link destination holding an unbalanced unescaped parenthesis as no destination, in every markdown reader.** | 2 | 2 | 2 | 4 | 2 | 5 by 2 | 17.4 |
|
||||
| 95 | 0.3.0 | | decision | **Find the code-span closers an escape forms, the mark runs a line cannot spell and the text `[` that opens link text by reading the line back, in place of `escapeClosedRuns`, `unspellableRuns` and `lastLinkClose` predicting them.** | 4 | 1 | 3 | 6 | 6 | code health by 7 | 10.2 |
|
||||
| 146 | 0.3.0 | | | **Open the README with what an app developer choosing an ADF converter needs: what the package converts, which call to take, and that it runs without React, Atlaskit or any runtime dependency, in Node, Forge, Deno, Bun and browsers.** | 2 | 2 | 1 | 3 | 3 | 3 by 6 | 9.5 |
|
||||
| 33 | 0.3.0 | | defect | **Emit a line in time linear in its mark runs, in `adfToLosslessMarkdown` and `adfToPortableMarkdown`.** | 3 | 1 | 2 | 4 | 5 | 9 by 3 | 7.8 |
|
||||
@@ -69,15 +68,6 @@
|
||||
|
||||
## Details
|
||||
|
||||
### 151. Read a link destination holding an unbalanced unescaped parenthesis as no destination, in every markdown reader.
|
||||
|
||||
CommonMark lets an unbracketed destination hold a parenthesis only escaped or in a balanced pair,
|
||||
but `[a](b(c "t")` reads as a link to `b(c` titled `t`; cmark and commonmark.js read it as text.
|
||||
`targetMemo` (`src/markdown/commonmark/link-syntax.ts`), which `readDestination` reads through,
|
||||
stops at a space with parentheses still open and keeps the destination. The writers escape an
|
||||
unbalanced parenthesis, so markdown they wrote reads the same after the fix. Found while shipping
|
||||
item 140, 2026-10-28.
|
||||
|
||||
### 95. Find the code-span closers an escape forms, the mark runs a line cannot spell and the text `[` that opens link text by reading the line back, in place of `escapeClosedRuns`, `unspellableRuns` and `lastLinkClose` predicting them.
|
||||
|
||||
`escapeClosedRuns` works out which backtick strings an escape leaves, `unspellableRuns` rebuilds the
|
||||
|
||||
Reference in New Issue
Block a user