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

Escape a line against the inline reader's own tokens, in time linear in a run

This commit is contained in:
2026-10-09 16:39:13 +02:00
parent f5a296797e
commit dba94dd8fd
9 changed files with 212 additions and 268 deletions
+1 -1
View File
@@ -53,7 +53,7 @@ In `docs/decisions.md`:
- Firefox reads the build
- The coverage floors
- The size ratchet
- The project ships under the comprehension floor until items 59, 60, 61 and 62 land
- The project ships under the comprehension floor until items 59, 60 and 62 land
- Properties on a fixed seed
- The CommonMark suite checks three ways
- The flavour spec is read as a source
+2 -2
View File
@@ -570,10 +570,10 @@ config gone missing fails the leg instead of falling back to oxlint's own defaul
`--deny-warnings`, since a rule from a category this config never names arrives as a warning it
exits 0 on.
## The project ships under the comprehension floor until items 59, 60, 61 and 62 land
## The project ships under the comprehension floor until items 59, 60 and 62 land
2026-10-03, the maintainer. KISS, a technical principle, and its comprehension floor of 7. Valid
while `todo.md` items 59, 60, 61 and 62 are open.
while `todo.md` items 59, 60 and 62 are open.
A four-seat comprehension panel scored the project under the floor of 7. Its scoring run of
2026-10-08 is the baseline. Since 2026-10-04, a chunk whose panel scores any
-8
View File
@@ -235,14 +235,6 @@ export function openingHtmlBlock(line: string, interrupting: boolean): OpenHtmlB
return undefined
}
export function opensBracketedAutolink(text: string, index: number): boolean {
return readBracketedAutolink(text, index) !== undefined
}
export function opensEmailAutolink(text: string, index: number): boolean {
return readEmailAutolink(text, index) !== undefined
}
export function readBracketedAutolink(text: string, index: number): number | undefined {
bracketedAutolink.lastIndex = index
return bracketedAutolink.exec(text)?.[0].length
+2 -2
View File
@@ -3,7 +3,7 @@ import test from 'node:test'
import type { DirectiveAttributes, DirectiveLine } from './directive-syntax.ts'
import { largestNesting } from '../nesting.ts'
import { readDirectiveLine, readInlineDirective } from './directive-syntax.ts'
import { claimsDirectivePrefix, readDirectiveLine, readInlineDirective } from './directive-syntax.ts'
// A pair the input spells bare decodes to itself; a quoted one names its spelling beside the decoding.
type Pair = [string, string, string?]
@@ -22,8 +22,8 @@ function fault(line: string): string {
}
function inline(text: string): unknown {
if (!claimsDirectivePrefix(text, 0)) return 'unclaimed'
const read = readInlineDirective(text, 0)
if (read === undefined) return 'unclaimed'
if (read.fault !== undefined) return read.fault.message
return { attributes: read.value.attributes, content: read.value.content, length: read.value.length, name: read.value.name }
}
+7 -3
View File
@@ -88,8 +88,9 @@ export function readDirectiveLine(line: string): Read<DirectiveLine> | undefined
return line.charAt(directivePrefix.length) === '/' ? readCloserLine(line) : readOpenerLine(line)
}
export function readInlineDirective(text: string, index: number): Read<DirectiveSpan> | undefined {
return readNestedDirective(text, index, 1)
// Reads the directive an `!adf:` at `index` claims.
export function readInlineDirective(text: string, index: number): Read<DirectiveSpan> {
return readClaimedDirective(text, index, 1)
}
export function readSoleStringAttribute(span: DirectiveSpan, key: string): Read<string> {
@@ -211,7 +212,10 @@ function readOpenerAttributes(rest: string, index: number): Read<Attributes> {
}
function readNestedDirective(text: string, index: number, depth: number): Read<DirectiveSpan> | undefined {
if (!claimsDirectivePrefix(text, index)) return undefined
return claimsDirectivePrefix(text, index) ? readClaimedDirective(text, index, depth) : undefined
}
function readClaimedDirective(text: string, index: number, depth: number): Read<DirectiveSpan> {
const name = inlineDirectiveName(text, index)
if (name === undefined) return { fault: malformedDirective(prefixFault) }
if (depth > largestNesting) {
+70 -101
View File
@@ -1,13 +1,12 @@
import type { Claims, WrittenFlavour } from '../portable/conventions.ts'
import type { InlineToken } from '../inline-tokens.ts'
import type { LineContainer } from '../line-container.ts'
import { backslashEscape, escapesLineClaim, inlineHtmlConstruct, opensBracketedAutolink, opensEmailAutolink, type LinePosition } from '../commonmark/grammar.ts'
import { backtickRun, closingBacktickRun } from '../commonmark/backtick-runs.ts'
import { claimsDirectivePrefix } from '../directive-syntax.ts'
import { delimiterFlags, isWordCharacter, matchEmphasis, runLength } from '../commonmark/emphasis-matching.ts'
import { flavourClaims, highlightDelimiter, highlightFlanking } from '../portable/conventions.ts'
import { delimiterRunToken, readInlineToken } from '../inline-tokens.ts'
import { escapesLineClaim, type LinePosition } from '../commonmark/grammar.ts'
import { flavourClaims, highlightFlanking } from '../portable/conventions.ts'
import { isBareDelimiterRow } from '../pipe-table-syntax.ts'
import { opensLinkDefinition } from '../commonmark/link-reference-definitions.ts'
import { readEntityReference } from '../commonmark/entity-references.ts'
export type DelimiterRole = 'close' | 'open'
@@ -33,7 +32,7 @@ type EmittedDelimiter = { closes: boolean; offset: number; pair: number; width:
type EmittedRun = { canClose: boolean; canOpen: boolean; character: string; delimiters: EmittedDelimiter[]; length: number; start: number }
const delimiters = ['*', '_', '`', '~']
const runCharacters = '*_`~'
const followsLinkText = /[([]/
@@ -92,36 +91,68 @@ function escape(segments: readonly InlineSegment[], container: LineContainer, cl
function escapeClaims(scan: string, escapings: readonly InlineEscaping[], container: LineContainer, claims: Claims): ReadonlySet<number> {
const escaped = new Set<number>()
const linkClose = lastLinkClose(scan, escapings)
const headingCloser = container === 'heading' ? closingHashes(scan) : undefined
let line = scanLine(scan, 0)
let afterEscape = false
// Whether the `=` before opens a `==` the reader takes whole, so this one starts nothing.
let pairsEquals = false
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)
let index = 0
while (index < scan.length) {
while (index > line.start + line.text.length) line = scanLine(scan, line.start + line.text.length + 1)
if (runCharacters.includes(scan.charAt(index))) {
index = escapeRun(scan, escapings, index, line, container, claims, escaped)
continue
}
const escaping = escapings[index]
const escapable = escaping === 'backslash' || escaping === 'bracketed'
const opensEquals: boolean = claims.highlights && !pairsEquals && scan.startsWith(highlightDelimiter, index)
const opensDirective = claims.directives && claimsDirectivePrefix(scan, index)
const claimed: boolean =
(escapable &&
((opensEquals && claimsHighlight(scan, index)) ||
claimsLineStart(line, index, container) ||
mergesWithSyntax(scan, escapings, index) ||
opensDirective ||
opensConstruct(scan, linkClose, index, escaping === 'bracketed', container, afterEscape))) ||
(escaping === 'bracketed-link-target' &&
((scan.charAt(index) === '`' && opensCodeSpan(scan, index, afterEscape)) || opensDirective))
const token = readInlineToken(scan, index, claims)
const claimed = isEscapable(escaping)
? index === headingCloser || claimsLineStart(line, index, container) || claimsCharacter(scan, escapings, linkClose, index, token, container)
: escaping === 'bracketed-link-target' && token.kind === 'directive'
if (claimed) escaped.add(index)
afterEscape = claimed
pairsEquals = opensEquals && !claimed
index += !claimed && token.kind === 'highlight' ? token.width : 1
}
return escaped
}
// Like an emphasis run, a `==` in text escapes where the reader can open or close with it.
function claimsHighlight(scan: string, index: number): boolean {
const flanking = highlightFlanking(scan, index)
return flanking.opens || flanking.closes
// A run joining the emitter's own delimiter escapes its text; a run of text escapes from its start while the reader claims what the escapes leave of it.
function escapeRun(scan: string, escapings: readonly InlineEscaping[], start: number, line: ScanLine, container: LineContainer, claims: Claims, escaped: Set<number>): number {
const end = start + runLength(scan, start)
const run = escapings.slice(start, end)
if (run.some(isSyntax) && run.some(isEscapable)) {
for (let index = start; index < end; index += 1) if (isEscapable(escapings[index])) escaped.add(index)
return end
}
for (let index = start; index < end; index += 1) {
// A backtick run escapes whole: a rest left bare would be a raw run of another length for a closer.
if (!(index > start && scan.charAt(index) === '`') && !claimsRunRest(scan, escapings, index, end, line, container, claims)) break
escaped.add(index)
}
return end
}
function claimsRunRest(scan: string, escapings: readonly InlineEscaping[], index: number, end: number, line: ScanLine, container: LineContainer, claims: Claims): boolean {
const escaping = escapings[index]
if (escaping === 'bracketed-link-target') return readInlineToken(scan, index, claims).kind === 'code-span'
if (!isEscapable(escaping)) return false
if (claimsLineStart(line, index, container)) return true
const token = scan.charAt(index) === '`' ? readInlineToken(scan, index, claims) : delimiterRunToken(scan, index, end - index, claims)
return token.kind !== 'text'
}
function claimsCharacter(scan: string, escapings: readonly InlineEscaping[], linkClose: number, index: number, token: InlineToken, container: LineContainer): boolean {
const character = scan.charAt(index)
const inBrackets = escapings[index] === 'bracketed'
switch (token.kind) {
case 'bracket':
if (token.image) return isSyntax(escapings[index + 1])
return inBrackets || index < linkClose
case 'bracket-close':
return inBrackets
case 'highlight':
return token.opens || token.closes
case 'text':
if (character === '|') return container === 'table-cell'
return character === '{' && scan.charAt(index - 1) === ']' && isSyntax(escapings[index - 1])
default:
return true
}
}
// CommonMark reads no escape inside a code span, so a backtick string left beside an escape still closes a span that an earlier bare run of its length opens.
@@ -246,37 +277,14 @@ function emittedRuns(segments: readonly InlineSegment[], placements: readonly nu
return { nodes, runs }
}
function mergesWithSyntax(scan: string, escapings: readonly (InlineEscaping | undefined)[], index: number): boolean {
const character = scan.charAt(index)
if (character === '!') return scan.charAt(index + 1) === '[' && isSyntax(escapings[index + 1])
if (character === '{') return scan.charAt(index - 1) === ']' && isSyntax(escapings[index - 1])
if (!delimiters.includes(character)) return false
return touchesSyntax(scan, escapings, index, -1) || touchesSyntax(scan, escapings, index, 1)
}
function touchesSyntax(scan: string, escapings: readonly (InlineEscaping | undefined)[], index: number, step: number): boolean {
const character = scan.charAt(index)
let cursor = index + step
while (scan.charAt(cursor) === character && !isSyntax(escapings[cursor])) cursor += step
return scan.charAt(cursor) === character && isSyntax(escapings[cursor])
function isEscapable(escaping: InlineEscaping | undefined): boolean {
return escaping === 'backslash' || escaping === 'bracketed'
}
export function isSyntax(escaping: InlineEscaping | undefined): boolean {
return escaping === 'none' || escaping === 'bracketed-link-target'
}
function opensConstruct(
scan: string,
linkClose: number,
index: number,
inBrackets: boolean,
container: LineContainer,
afterEscape: boolean,
): boolean {
if (container === 'heading' && closesHeading(scan, index)) return true
return claimsCharacter(scan, linkClose, index, inBrackets, container, afterEscape)
}
// A hard break is the one spelling that puts a delimiter row under a row of its own, so only a later line claims.
function claimsLineStart(line: ScanLine, index: number, container: LineContainer): boolean {
if (container !== 'paragraph') return false
@@ -284,36 +292,19 @@ function claimsLineStart(line: ScanLine, index: number, container: LineContainer
return escapesLineClaim(line.text, index - line.start, line.position)
}
// Where the closing sequence an ATX heading would read in the text starts, `undefined` where it reads none.
function closingHashes(scan: string): number | undefined {
let start = scan.length
while (scan.charAt(start - 1) === '#') start -= 1
if (start === scan.length) return undefined
return start === 0 || /[ \t]/.test(scan.charAt(start - 1)) ? start : undefined
}
function scanLine(scan: string, start: number): ScanLine {
const end = scan.indexOf('\n', start)
return { position: start === 0 ? 'first' : 'later', start, text: scan.slice(start, end === -1 ? undefined : end) }
}
function closesHeading(scan: string, index: number): boolean {
if (scan.charAt(index) !== '#' || !/^#+$/.test(scan.slice(index))) return false
return index === 0 || /[ \t]/.test(scan.charAt(index - 1))
}
function claimsCharacter(
scan: string,
linkClose: number,
index: number,
inBrackets: boolean,
container: LineContainer,
afterEscape: boolean,
): boolean {
const character = scan.charAt(index)
if (inBrackets && (character === '[' || character === ']')) return true
if (character === '|') return container === 'table-cell'
if (character === '\\') return backslashEscape(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 index < linkClose
if (character === '`') return opensCodeSpan(scan, index, afterEscape)
if (character === '*' || character === '_' || character === '~') return claimsEmphasis(scan, index, afterEscape)
return false
}
// A `]` the emitter spelled sits inside a construct that binds before link text does.
function lastLinkClose(scan: string, escapings: readonly (InlineEscaping | undefined)[]): number {
for (let cursor = scan.length - 1; cursor >= 0; cursor -= 1) {
@@ -323,28 +314,6 @@ function lastLinkClose(scan: string, escapings: readonly (InlineEscaping | undef
return -1
}
function opensCodeSpan(scan: string, index: number, afterEscape: boolean): boolean {
// A run escapes whole: a rest left bare would be a raw run of another length for a closer.
if (afterEscape && scan.charAt(index - 1) === '`') return true
if (!startsRun(scan, index, afterEscape)) return false
const opener = backtickRun(scan, index)
return closingBacktickRun(scan, index + opener, opener) !== undefined
}
function claimsEmphasis(scan: string, index: number, afterEscape: boolean): boolean {
if (!startsRun(scan, index, afterEscape)) return false
const character = scan.charAt(index)
const length = runLength(scan, index)
if (character === '~' && length !== 2) return false
const flags = delimiterFlags(character, index === 0 ? '' : scan.charAt(index - 1), scan.charAt(index + length))
return flags.canClose || flags.canOpen
}
function startsRun(scan: string, index: number, afterEscape: boolean): boolean {
if (index === 0 || afterEscape) return true
return scan.charAt(index - 1) !== scan.charAt(index)
}
function charAt(text: string, index: number): string {
return index < 0 ? '' : text.charAt(index)
}
+77
View File
@@ -0,0 +1,77 @@
import type { Claims } from './portable/conventions.ts'
import { backslashEscape, inlineHtmlConstruct, readBracketedAutolink, readEmailAutolink } from './commonmark/grammar.ts'
import { backtickRun, closingBacktickRun } from './commonmark/backtick-runs.ts'
import { claimsDirectivePrefix } from './directive-syntax.ts'
import { delimiterFlags, runLength } from './commonmark/emphasis-matching.ts'
import { highlightDelimiter, highlightFlanking } from './portable/conventions.ts'
import { readEntityReference } from './commonmark/entity-references.ts'
// What the inline reader reads from a token start, `width` characters on; `text` reads literally.
export type InlineToken =
| { kind: 'code-span'; opener: number; width: number }
| { closes: boolean; kind: 'highlight'; opens: boolean; width: number }
| { canClose: boolean; canOpen: boolean; kind: 'delimiter-run'; width: number }
| { construct: string; kind: 'html'; width: 1 }
| { email: boolean; kind: 'autolink'; width: number }
| { image: boolean; kind: 'bracket'; width: number }
| { kind: 'bracket-close' | 'directive' | 'escape' | 'hard-break' | 'line-ending' | 'entity' | 'text'; width: number }
export function readInlineToken(source: string, index: number, claims: Claims): InlineToken {
switch (source.charAt(index)) {
case '<':
return angleToken(source, index)
case '!':
if (claims.directives && claimsDirectivePrefix(source, index)) return { kind: 'directive', width: 1 }
return source.charAt(index + 1) === '[' ? { image: true, kind: 'bracket', width: 2 } : text(1)
case '[':
return { image: false, kind: 'bracket', width: 1 }
case ']':
return { kind: 'bracket-close', width: 1 }
case '\\':
if (source.charAt(index + 1) === '\n') return { kind: 'hard-break', width: 2 }
return backslashEscape(source, index) === undefined ? text(1) : { kind: 'escape', width: 2 }
case '\n':
return { kind: 'line-ending', width: 1 }
case '&': {
const reference = readEntityReference(source, index)
return reference === undefined ? text(1) : { kind: 'entity', width: reference.length }
}
case '`':
return backtickToken(source, index)
case '*':
case '_':
case '~':
return delimiterRunToken(source, index, runLength(source, index), claims)
case '=':
return claims.highlights && source.startsWith(highlightDelimiter, index) ? { ...highlightFlanking(source, index), kind: 'highlight', width: highlightDelimiter.length } : text(1)
default:
return text(1)
}
}
// The run of `length` delimiters from `index`, which a caller knowing the run's end reads without rescanning it.
export function delimiterRunToken(source: string, index: number, length: number, claims: Claims): InlineToken {
const character = source.charAt(index)
if (character === '~' && (!claims.strikethrough || length !== 2)) return text(length)
const flags = delimiterFlags(character, source.charAt(index - 1), source.charAt(index + length))
return flags.canOpen || flags.canClose ? { ...flags, kind: 'delimiter-run', width: length } : text(length)
}
function angleToken(source: string, index: number): InlineToken {
const bracketed = readBracketedAutolink(source, index)
if (bracketed !== undefined) return { email: false, kind: 'autolink', width: bracketed }
const email = readEmailAutolink(source, index)
if (email !== undefined) return { email: true, kind: 'autolink', width: email }
const construct = inlineHtmlConstruct(source, index)
return construct === undefined ? text(1) : { construct, kind: 'html', width: 1 }
}
function backtickToken(source: string, index: number): InlineToken {
const opener = backtickRun(source, index)
const closer = closingBacktickRun(source, index + opener, opener)
return closer === undefined ? text(opener) : { kind: 'code-span', opener, width: closer + opener - index }
}
function text(width: number): InlineToken {
return { kind: 'text', width }
}
+51 -140
View File
@@ -2,15 +2,15 @@ import type { AdfMark, AdfNode } from '../../adf/document.ts'
import type { Claims } from '../portable/conventions.ts'
import type { DirectiveSpan, NestedSpans } from '../directive-syntax.ts'
import type { EmphasisPairing } from '../commonmark/emphasis-matching.ts'
import type { InlineToken } from '../inline-tokens.ts'
import type { LineContainer } from '../line-container.ts'
import type { LinkDefinition } from '../commonmark/link-syntax.ts'
import { backslashEscape, decodeTextEscapes, inlineHtmlConstruct, readBracketedAutolink, readEmailAutolink, trimTrailingSpace } from '../commonmark/grammar.ts'
import { backtickRun, closingBacktickRun } from '../commonmark/backtick-runs.ts'
import { decodeTextEscapes, trimTrailingSpace } from '../commonmark/grammar.ts'
import { centeredImage, externalMedia } from '../external-image.ts'
import { commonMarkLink, linkHref } from '../mark-spellings.ts'
import { delimiterFlags, matchEmphasis, runLength } from '../commonmark/emphasis-matching.ts'
import { matchEmphasis } from '../commonmark/emphasis-matching.ts'
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { highlightDelimiter, highlightFlanking } from '../portable/conventions.ts'
import { highlightDelimiter } from '../portable/conventions.ts'
import { identicalMarks, mergeAdjacentText, nodeAttrs, nodeMarks } from '../../adf/document.ts'
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
import { joinsWhenRead, textBreakName, textBreakSpelling } from '../adjacent-text.ts'
@@ -20,6 +20,7 @@ import { openingLinkTakesDirective } from '../emit/inline-line.ts'
import { readCarriedInline } from '../opaque-carry.ts'
import { readDirectiveMark } from './directive-marks.ts'
import { readInlineDirectiveNode } from './directive-nodes.ts'
import { readInlineToken } from '../inline-tokens.ts'
import { readTextDirective } from '../text-directive.ts'
// Only a `paragraph` block takes `image`, the `mediaSingle` a lone image builds; every other container takes `nodes`, where the image is its linked alt text.
@@ -107,75 +108,60 @@ function freshScan(source: string, shared: SharedScan, own: Pick<Scan, 'claims'
}
function scanInline(scan: Scan): Result<Scanned> {
const { source } = scan
let index = 0
while (index < source.length) {
switch (source.charAt(index)) {
case '<': {
const angle = readAngle(scan, index)
if (!angle.ok) return angle
index = angle.value
break
}
case '!': {
const directive = scan.claims.directives ? readDirective(scan, index) : undefined
if (directive === undefined) {
index = openBracket(scan, index)
break
}
if (!directive.ok) return directive
index = directive.value
break
}
case '[':
index = openBracket(scan, index)
break
case ']': {
const closed = closeBracket(scan, index)
if (!closed.ok) return closed
index = closed.value
break
}
default:
index = readCharacter(scan, index)
}
while (index < scan.source.length) {
const read = readToken(scan, index, readInlineToken(scan.source, index, scan.claims))
if (!read.ok) return read
index = read.value
}
flush(scan, scan.container !== undefined)
return assemble(scan)
}
function readCharacter(scan: Scan, index: number): number {
switch (scan.source.charAt(index)) {
case '\\':
return readBackslash(scan, index)
case '\n':
return readLineEnding(scan, index)
case '`':
return readBackticks(scan, index)
case '*':
case '_':
case '~':
return readDelimiterRun(scan, index)
case '=':
return readEquals(scan, index)
default:
scan.pending += scan.source.charAt(index)
return index + 1
function readToken(scan: Scan, index: number, token: InlineToken): Result<number> {
const end = index + token.width
switch (token.kind) {
case 'autolink':
flush(scan, false)
pushNode(scan, linkedText(scan.source.slice(index + 1, end - 1), token.email ? 'mailto:' : ''))
return success(end)
case 'bracket':
flush(scan, false)
scan.pieces.push({ active: true, image: token.image, kind: 'open', start: end })
return success(end)
case 'bracket-close':
return closeBracket(scan, index)
case 'code-span':
flush(scan, false)
pushNode(scan, { marks: [{ type: 'code' }], text: codeSpanText(scan.source.slice(index + token.opener, end - token.opener)), type: 'text' })
return success(end)
case 'delimiter-run':
flush(scan, false)
scan.pieces.push({ canClose: token.canClose, canOpen: token.canOpen, character: scan.source.charAt(index), kind: 'run', length: token.width })
return success(end)
case 'directive':
return readDirective(scan, index)
case 'hard-break':
// CommonMark strips the spaces the two-space break is spelled with, and keeps those before a backslash.
flush(scan, false)
pushNode(scan, { type: 'hardBreak' })
return success(end)
case 'highlight':
flush(scan, false)
scan.pieces.push({ closes: token.closes, kind: 'highlight', opens: token.opens })
return success(end)
case 'html':
return failure('unmappable-html', `no raw HTML converts at this version: ${token.construct}`, scan.path)
case 'line-ending':
return success(readLineEnding(scan, index))
case 'entity':
case 'escape':
case 'text':
scan.pending += scan.source.slice(index, end)
return success(end)
}
}
function readBackslash(scan: Scan, index: number): number {
if (scan.source.charAt(index + 1) === '\n') {
// CommonMark strips the spaces the two-space break is spelled with, and keeps those before a backslash.
flush(scan, false)
pushNode(scan, { type: 'hardBreak' })
return index + 2
}
const width = backslashEscape(scan.source, index) === undefined ? 1 : 2
scan.pending += scan.source.slice(index, index + width)
return index + width
}
function readLineEnding(scan: Scan, index: number): number {
const hard = scan.pending.endsWith(' ')
flush(scan, true)
@@ -184,48 +170,10 @@ function readLineEnding(scan: Scan, index: number): number {
return index + 1
}
function readBackticks(scan: Scan, index: number): number {
const span = readCodeSpan(scan.source, index)
if (span === undefined) {
const run = backtickRun(scan.source, index)
scan.pending += scan.source.slice(index, index + run)
return index + run
}
flush(scan, false)
pushNode(scan, { marks: [{ type: 'code' }], text: span.text, type: 'text' })
return span.end
}
function readAngle(scan: Scan, index: number): Result<number> {
const autolink = readAutolink(scan.source, index)
if (autolink !== undefined) {
flush(scan, false)
pushNode(scan, autolink.node)
return success(index + autolink.length)
}
const construct = inlineHtmlConstruct(scan.source, index)
if (construct !== undefined) return failure('unmappable-html', `no raw HTML converts at this version: ${construct}`, scan.path)
scan.pending += '<'
return success(index + 1)
}
function openBracket(scan: Scan, index: number): number {
const image = scan.source.charAt(index) === '!'
if (image && scan.source.charAt(index + 1) !== '[') {
scan.pending += '!'
return index + 1
}
const width = image ? 2 : 1
flush(scan, false)
scan.pieces.push({ active: true, image, kind: 'open', start: index + width })
return index + width
}
function readDirective(scan: Scan, index: number): Result<number> | undefined {
function readDirective(scan: Scan, index: number): Result<number> {
const held = scan.spans.get(index)
if (held !== undefined) return pushDirective(scan, held, index)
const directive = readInlineDirective(scan.source, index)
if (directive === undefined) return undefined
if (directive.fault !== undefined) return faulted(directive.fault, scan.path)
return pushDirective(scan, directive.value, index)
}
@@ -382,36 +330,6 @@ function marksLink(nodes: readonly Inline[]): boolean {
return adfNodes(nodes).some((node) => nodeMarks(node).some((mark) => mark.type === 'link'))
}
function readDelimiterRun(scan: Scan, index: number): number {
const character = scan.source.charAt(index)
const length = runLength(scan.source, index)
const flags = delimiterFlags(character, scan.source.charAt(index - 1), scan.source.charAt(index + length))
if ((character === '~' && (!scan.claims.strikethrough || length !== 2)) || (!flags.canOpen && !flags.canClose)) scan.pending += scan.source.slice(index, index + length)
else {
flush(scan, false)
scan.pieces.push({ canClose: flags.canClose, canOpen: flags.canOpen, character, kind: 'run', length })
}
return index + length
}
function readEquals(scan: Scan, index: number): number {
if (!scan.claims.highlights || !scan.source.startsWith(highlightDelimiter, index)) {
scan.pending += '='
return index + 1
}
flush(scan, false)
scan.pieces.push({ ...highlightFlanking(scan.source, index), kind: 'highlight' })
return index + highlightDelimiter.length
}
function readAutolink(source: string, index: number): { length: number; node: AdfNode } | undefined {
const bracketed = readBracketedAutolink(source, index)
if (bracketed !== undefined) return { length: bracketed, node: linkedText(source.slice(index + 1, index + bracketed - 1), '') }
const email = readEmailAutolink(source, index)
if (email === undefined) return undefined
return { length: email, node: linkedText(source.slice(index + 1, index + email - 1), 'mailto:') }
}
function linkedText(text: string, scheme: string): AdfNode {
return { marks: [{ attrs: { href: `${scheme}${text}` }, type: 'link' }], text, type: 'text' }
}
@@ -685,13 +603,6 @@ function applyMark(nodes: readonly Inline[], mark: AdfMark): Inline[] {
})
}
function readCodeSpan(source: string, index: number): { end: number; text: string } | undefined {
const opener = backtickRun(source, index)
const closer = closingBacktickRun(source, index + opener, opener)
if (closer === undefined) return undefined
return { end: closer + opener, text: codeSpanText(source.slice(index + opener, closer)) }
}
function codeSpanText(content: string): string {
const text = content.replaceAll('\n', ' ')
const padded = text.startsWith(' ') && text.endsWith(' ') && /[^ ]/.test(text)
+2 -11
View File
@@ -25,7 +25,6 @@
| ID | Release | Exempt | Item | R | S | A | G | Goals | Score |
|---|---|---|---|---|---|---|---|---|---|
| 61 | 0.2.0 | decision | **Have the emitter ask the inline reader how a line reads back, in place of `line-escaping.ts` predicting it.** | 6 | 8 | 3 | 8 | 1 | 14.0 |
| 94 | 0.2.0 | defect | **Read two lists a link reference definition parts as two lists, in every reader.** | 3 | 3 | 4 | 7 | 4, 5 | 13.6 |
| 93 | 0.2.0 | | **Report a refusal's `path` where the node would stand after `portableMarkdownToAdf` moves it out of a task item.** | 2 | 3 | 3 | 5 | 1 | 13.2 |
| 86 | 0.2.0 | | **Write an image whose `media` only a link marks, and one captioned with plain text, in the CommonMark spelling a writer panel picks, in `adfToLosslessMarkdown`.** | 4 | 4 | 5 | 6 | 4, 6 | 13.0 |
@@ -69,14 +68,6 @@
## Details
### 61. Have the emitter ask the inline reader how a line reads back, in place of `line-escaping.ts` predicting it.
The comprehension panel's worst place: `escapeClaims` and `escapeClosedRuns` re-implement the
reader's view — flanking, code-span closers, link-definition openings, highlight flanking — and
only the property tests catch drift. It caps the panel's Locality score. The replacement runs in
time linear in the line: today `mergesWithSyntax`, `touchesSyntax` and `closesHeading` rescan a run
of one character from each of its characters.
### 94. Read two lists a link reference definition parts as two lists, in every reader.
`openedList` in `src/markdown/parse/blocks.ts` joins a fresh list to the list just before it when
@@ -153,7 +144,7 @@ three linear.
### 7. Ship HTML: `adfToHtml`, `htmlToAdf`, and markdown to and from HTML composed through ADF, each call named by its flavour.
Lands after items 6, 59, 60, 61 and 62. Which flavours get a composed call is this item's to settle;
Lands after items 6, 59, 60 and 62. Which flavours get a composed call is this item's to settle;
the CommonMark spec suite also runs against the one reading CommonMark. The README documents HTML as
it documents markdown, and its tagline and `package.json`'s description and keywords name HTML
again. `htmlToAdf` refuses a value that is not a string as `not-a-string`
@@ -286,7 +277,7 @@ gains its row.
### 91. Spell an emphasis or link once across the hard breaks between two nodes it marks, in both writers.
Lands after item 61. No reader marks a hard break, so `*One\` then `Two*` writes back as `_One_\` then
No reader marks a hard break, so `*One\` then `Two*` writes back as `_One_\` then
`_Two_`, and a link typed across a line as two links. Joining the run across the break in
`inline-line.ts` ruled worse with two comprehension panels (PR #22): a joined run that cannot be
spelled needs its own fallback, first as a set of parted breaks, then as a flag the first fallback