3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 19:58:28 +01:00

Answer stability round 1: 0 🔴, 6 🟡; share the label limit with readLabel, pin it at 1000 characters, move item 129 to the changelog

This commit is contained in:
2026-10-23 11:23:23 +02:00
committed by Lillem4n
parent ddcfa6ee1a
commit 94f4df4dcf
4 changed files with 17 additions and 9 deletions
+5 -2
View File
@@ -37,8 +37,11 @@
20 lines of 40000 tabs under 80000 nested items took 1988 ms.
- `losslessMarkdownToAdf` takes time linear in the markdown when an inline directive's `[content]`
holds many unclosed backtick runs: 640000 characters of them took one second, now 100 ms.
- Every reader leaves `[text]` and `[text][]` literal when the text runs over 999 characters, where
it read them as a link to the matching definition: CommonMark caps a link label at 999 characters.
- Every reader takes time linear in the inline markdown when emphasis spans many links, or many
closing brackets follow a link: 8000 links inside 8000 `*` took 4.4 seconds, now 170 ms.
- Every reader leaves a link label over 999 characters literal, counting an escape as two, where it
read a link to the matching definition: CommonMark caps a link label at 999 characters. This holds
for `[text]`, `[text][]`, `[text][label]` and the definition `[label]: url` alike.
- The README compares the package with seven other ADF converters, each checked against its
published version.
+4 -2
View File
@@ -9,14 +9,16 @@ export type LinkPart = { length: number; value: string }
export type TitleEscapes = 'lossless' | 'portable' | 'portable-table-cell'
const bracketedDestination = /<((?:[^\n<>\\]|\\[^\n])*)>/y
const linkLabel = /\[((?:[^[\]\\]|\\[\s\S]){0,999})\]/y
// CommonMark: a link label holds at most this many characters between its brackets, an escape counting as two.
export const labelLimit = 999
const linkLabel = new RegExp(String.raw`\[((?:[^[\]\\]|\\[\s\S]){0,${labelLimit}})\]`, 'y')
const titleClosers: Readonly<Record<string, string>> = { '"': '"', "'": "'", '(': ')' }
// The label is matched by its normalized raw text, escapes and all.
export function readLabel(text: string, offset: number): LinkPart | undefined {
linkLabel.lastIndex = offset
const value = linkLabel.exec(text)?.[1]
return value === undefined ? undefined : { length: value.length + 2, value }
return value === undefined || value.length > labelLimit ? undefined : { length: value.length + 2, value }
}
export function normalizeLabel(raw: string): string {
+3 -4
View File
@@ -15,7 +15,7 @@ import { identicalMarks, mergeAdjacentText, nodeAttrs, nodeMarks } from '../../a
import { inlineNodeModel, takesMark } from '../../adf/inline-nodes.ts'
import { joinsWhenRead, textBreakName, textBreakSpelling } from '../adjacent-text.ts'
import { matchEmphasis } from '../commonmark/emphasis-matching.ts'
import { normalizeLabel, readInlineTarget, readLabel } from '../commonmark/link-syntax.ts'
import { labelLimit, normalizeLabel, readInlineTarget, readLabel } from '../commonmark/link-syntax.ts'
import { noSpans, readInlineDirective } from '../directive/syntax.ts'
import { openingLinkTakesDirective } from '../emit/spelling-questions.ts'
import { readCarriedInline } from '../opaque-carry.ts'
@@ -431,9 +431,8 @@ function resolveTarget(scan: Scan, start: number, index: number): { definition:
if (inline !== undefined) return { definition: inline.definition, length: inline.length }
}
const label = scan.source.charAt(after) === '[' ? readLabel(scan.source, after) : undefined
// CommonMark: a label holds at most 999 characters, so longer link text names no definition.
const labelIsText = label === undefined || label.value === ''
if (labelIsText && index - start > 999) return undefined
if (labelIsText && index - start > labelLimit) return undefined
const name = labelIsText ? scan.source.slice(start, index) : label.value
const definition = scan.definitions.get(normalizeLabel(name))
if (definition === undefined) return undefined
@@ -566,7 +565,7 @@ function markPairings(pieces: readonly Piece[], nodes: Inline[][], pairings: rea
}
for (const pairing of opening.get(index) ?? []) {
const type = markType(pairing.opener.character, pairing.used)
// A type takes its place from the innermost pairing of it.
// Each type once, so a piece's marks stay at most three however deep the pairings nest.
const outer = open.at(-1)?.marks.filter((mark) => mark.type !== type) ?? []
open.push({ closer: pairing.closer.index, marks: [...outer, { type }] })
}
+5 -1
View File
@@ -1078,8 +1078,12 @@ test('reads the reference links a definition resolves, and leaves the rest liter
test('reads link text past 999 characters as no label, in every reader', () => {
const longest = `foo${' '.repeat(993)}bar`
const tooLong = `foo${' '.repeat(1000)}bar`
const tooLong = `foo${' '.repeat(994)}bar`
const escaped = '\\*'.repeat(499)
const escapedTooLong = '\\*'.repeat(500)
for (const read of [commonMarkToAdf, losslessMarkdownToAdf, portableMarkdownToAdf]) {
assert.deepEqual(content(read(`[a][${escaped}]\n\n[${escaped}]: /u\n`)), [{ content: [marked('a', link('/u'))], type: 'paragraph' }])
assert.deepEqual(content(read(`[a][${escapedTooLong}]\n\n[${escapedTooLong}]: /u\n`)), [paragraph(`[a][${'*'.repeat(500)}]`), paragraph(`[${'*'.repeat(500)}]: /u`)])
assert.deepEqual(content(read(`[${longest}]\n\n[foo bar]: /u\n`)), [{ content: [marked(longest, link('/u'))], type: 'paragraph' }])
assert.deepEqual(content(read(`[${longest}][]\n\n[foo bar]: /u\n`)), [{ content: [marked(longest, link('/u'))], type: 'paragraph' }])
assert.deepEqual(content(read(`[${tooLong}]\n\n[foo bar]: /u\n`)), [paragraph(`[${tooLong}]`)])