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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user