Read the inline text, and decode the escapes and references CommonMark spells #33
@@ -138,9 +138,10 @@ live Atlassian APIs; property-generated ADF trees; the CommonMark spec suite aga
|
|||||||
- Nothing recurses unbounded: the guards walk iteratively, and blocks, marks and JSON values — an
|
- Nothing recurses unbounded: the guards walk iteratively, and blocks, marks and JSON values — an
|
||||||
attribute's and a carried node's alike — are all held to 500 levels, so a deep document is a
|
attribute's and a carried node's alike — are all held to 500 levels, so a deep document is a
|
||||||
`Result` rather than the stack overflow that waits near 2000.
|
`Result` rather than the stack overflow that waits near 2000.
|
||||||
- A reader takes the text and an index — a sticky regex, `indexOf` — never a fresh slice per
|
- A reader takes the text and an index — a sticky regex whose `lastIndex` the caller sets on the
|
||||||
character, and a per-character walk hoists what does not vary with the character. The pipeline
|
line before it reads, `indexOf` — never a fresh slice per character, and a per-character walk
|
||||||
persona feeds documents nobody typed, so an ordinary megabyte stays milliseconds.
|
hoists the scan that does not vary with the character. The pipeline persona feeds documents
|
||||||
|
nobody typed, and a megabyte through a quadratic walk is a minute rather than a millisecond.
|
||||||
- No casts: `as`, `as unknown as`, non-null `!`. A boundary owes a type guard validating the
|
- No casts: `as`, `as unknown as`, non-null `!`. A boundary owes a type guard validating the
|
||||||
fields it claims (`isAdfDocument`); past it everything is typed. Make invalid states
|
fields it claims (`isAdfDocument`); past it everything is typed. Make invalid states
|
||||||
unrepresentable.
|
unrepresentable.
|
||||||
@@ -180,7 +181,7 @@ One-line commit messages and PR titles; short PR summaries. No AI-attribution ma
|
|||||||
No wiki markup (§1), no network or filesystem I/O, no name→id resolution (§3), no ADF schema
|
No wiki markup (§1), no network or filesystem I/O, no name→id resolution (§3), no ADF schema
|
||||||
validation or exported validator — a refusal that keeps the round-trip is not schema validation,
|
validation or exported validator — a refusal that keeps the round-trip is not schema validation,
|
||||||
so the one a spelled node carrying the same mark type twice earns stays, no shipped CSS (§4), no
|
so the one a spelled node carrying the same mark type twice earns stays, no shipped CSS (§4), no
|
||||||
streaming APIs, no performance budget past §11's linear scan — nothing here is tuned, and no
|
streaming APIs, no performance budget past §11's scanning rule — nothing here is tuned, and no
|
||||||
figure is promised. A CLI is a later goal (`todo.md`), not a non-goal.
|
figure is promised. A CLI is a later goal (`todo.md`), not a non-goal.
|
||||||
|
|
||||||
## 15. The working loop
|
## 15. The working loop
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ function escape(segments: readonly InlineSegment[], container: LineContainer): A
|
|||||||
const escaped = new Set<number>()
|
const escaped = new Set<number>()
|
||||||
const placements: number[] = []
|
const placements: number[] = []
|
||||||
let output = ''
|
let output = ''
|
||||||
|
const linkClose = lastLinkClose(scan, escapings)
|
||||||
let line = scanLine(scan, 0)
|
let line = scanLine(scan, 0)
|
||||||
for (let index = 0; index < scan.length; index += 1) {
|
for (let index = 0; index < scan.length; index += 1) {
|
||||||
if (index > line.start + line.text.length) line = scanLine(scan, line.start + line.text.length + 1)
|
if (index > line.start + line.text.length) line = scanLine(scan, line.start + line.text.length + 1)
|
||||||
@@ -78,7 +79,7 @@ function escape(segments: readonly InlineSegment[], container: LineContainer): A
|
|||||||
escapable &&
|
escapable &&
|
||||||
(claimsLineStart(line, index, container) ||
|
(claimsLineStart(line, index, container) ||
|
||||||
mergesWithSyntax(scan, escapings, index) ||
|
mergesWithSyntax(scan, escapings, index) ||
|
||||||
opensConstruct(scan, escapings, index, escaping === 'bracketed', container, escaped))
|
opensConstruct(scan, linkClose, index, escaping === 'bracketed', container, escaped))
|
||||||
) {
|
) {
|
||||||
output += '\\'
|
output += '\\'
|
||||||
escaped.add(index)
|
escaped.add(index)
|
||||||
@@ -185,14 +186,14 @@ function isSyntax(escaping: InlineEscaping | undefined): boolean {
|
|||||||
|
|
||||||
function opensConstruct(
|
function opensConstruct(
|
||||||
scan: string,
|
scan: string,
|
||||||
escapings: readonly (InlineEscaping | undefined)[],
|
linkClose: number,
|
||||||
index: number,
|
index: number,
|
||||||
inBrackets: boolean,
|
inBrackets: boolean,
|
||||||
container: LineContainer,
|
container: LineContainer,
|
||||||
escaped: ReadonlySet<number>,
|
escaped: ReadonlySet<number>,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (container === 'heading' && closesHeading(scan, index)) return true
|
if (container === 'heading' && closesHeading(scan, index)) return true
|
||||||
return claimsCharacter(scan, escapings, index, inBrackets, container, escaped)
|
return claimsCharacter(scan, linkClose, index, inBrackets, container, escaped)
|
||||||
}
|
}
|
||||||
|
|
||||||
function claimsLineStart(line: ScanLine, index: number, container: LineContainer): boolean {
|
function claimsLineStart(line: ScanLine, index: number, container: LineContainer): boolean {
|
||||||
@@ -211,7 +212,7 @@ function closesHeading(scan: string, index: number): boolean {
|
|||||||
|
|
||||||
function claimsCharacter(
|
function claimsCharacter(
|
||||||
scan: string,
|
scan: string,
|
||||||
escapings: readonly (InlineEscaping | undefined)[],
|
linkClose: number,
|
||||||
index: number,
|
index: number,
|
||||||
inBrackets: boolean,
|
inBrackets: boolean,
|
||||||
container: LineContainer,
|
container: LineContainer,
|
||||||
@@ -224,19 +225,20 @@ function claimsCharacter(
|
|||||||
if (character === '&') return readEntityReference(scan, index) !== undefined
|
if (character === '&') return readEntityReference(scan, index) !== undefined
|
||||||
if (character === '<') return opensBracketedAutolink(scan, index) || opensEmailAutolink(scan, index) || inlineHtmlConstruct(scan, index) !== undefined
|
if (character === '<') return opensBracketedAutolink(scan, index) || opensEmailAutolink(scan, index) || inlineHtmlConstruct(scan, index) !== undefined
|
||||||
if (character === ':') return opensInlineDirective(scan, index)
|
if (character === ':') return opensInlineDirective(scan, index)
|
||||||
if (character === '[') return opensLink(scan, escapings, index)
|
if (character === '[') return index < linkClose
|
||||||
if (character === '`') return opensCodeSpan(scan, index, escaped)
|
if (character === '`') return opensCodeSpan(scan, index, escaped)
|
||||||
if (character === '*' || character === '_' || character === '~') return claimsEmphasis(scan, index, escaped)
|
if (character === '*' || character === '_' || character === '~') return claimsEmphasis(scan, index, escaped)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The last `]` link text could close on, so a `[` ahead of it opens one and a `[` past it cannot.
|
||||||
// A `]` the emitter spelled sits inside a construct that binds before link text does.
|
// A `]` the emitter spelled sits inside a construct that binds before link text does.
|
||||||
function opensLink(scan: string, escapings: readonly (InlineEscaping | undefined)[], index: number): boolean {
|
function lastLinkClose(scan: string, escapings: readonly (InlineEscaping | undefined)[]): number {
|
||||||
for (let cursor = index + 1; cursor < scan.length; cursor += 1) {
|
for (let cursor = scan.length - 1; cursor >= 0; cursor -= 1) {
|
||||||
if (scan.charAt(cursor) !== ']' || isSyntax(escapings[cursor])) continue
|
if (scan.charAt(cursor) !== ']' || isSyntax(escapings[cursor])) continue
|
||||||
if (followsLinkText.test(scan.charAt(cursor + 1))) return true
|
if (followsLinkText.test(scan.charAt(cursor + 1))) return cursor
|
||||||
}
|
}
|
||||||
return false
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
function opensCodeSpan(scan: string, index: number, escaped: ReadonlySet<number>): boolean {
|
function opensCodeSpan(scan: string, index: number, escaped: ReadonlySet<number>): boolean {
|
||||||
|
|||||||
@@ -4,14 +4,18 @@ import { dirname, join } from 'node:path'
|
|||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import { readFileSync } from 'node:fs'
|
import { readFileSync } from 'node:fs'
|
||||||
|
|
||||||
|
import { readEntityReference } from './entity-references.ts'
|
||||||
|
|
||||||
const source = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'entity-references.ts'), 'utf8')
|
const source = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'entity-references.ts'), 'utf8')
|
||||||
|
|
||||||
// The packing the module rests on, read off the rows rather than trusted: a value holding a space or a
|
// A value holding a space or a tilde mis-splits into a wrong name and a lost one, silently, at load.
|
||||||
// tilde mis-splits into a wrong name and a lost one, at load, with nothing failing.
|
test('every packed reference parts into one name and one value the table decodes', () => {
|
||||||
test('every packed reference parts into one name and one value', () => {
|
|
||||||
const packed = /const packedReferences =\n {2}'([\s\S]*?)'\n/.exec(source)?.[1]
|
const packed = /const packedReferences =\n {2}'([\s\S]*?)'\n/.exec(source)?.[1]
|
||||||
assert.ok(packed !== undefined)
|
assert.ok(packed !== undefined)
|
||||||
const references = packed.split(/(?:\\\n)? +/)
|
const references = packed.split(/(?:\\\n)? +/)
|
||||||
assert.equal(references.length, 2125)
|
assert.equal(references.length, 2125)
|
||||||
for (const reference of references) assert.match(reference, /^[A-Za-z][A-Za-z0-9]*~[^ ~]+$/)
|
for (const reference of references) {
|
||||||
|
assert.match(reference, /^[A-Za-z][A-Za-z0-9]*~[^ ~]+$/)
|
||||||
|
assert.ok(readEntityReference(`&${reference.slice(0, reference.indexOf('~'))};`, 0) !== undefined)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user