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:
+5
-2
@@ -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.
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }] })
|
||||
}
|
||||
|
||||
@@ -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}]`)])
|
||||
|
||||
Reference in New Issue
Block a user