mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Answer architecture round 1: a token's width is true for every kind that carries one, the escape walk threads one record, the == step and the directive precondition say why, and item 96 files the inline HTML scan
This commit is contained in:
@@ -88,7 +88,7 @@ export function readDirectiveLine(line: string): Read<DirectiveLine> | undefined
|
||||
return line.charAt(directivePrefix.length) === '/' ? readCloserLine(line) : readOpenerLine(line)
|
||||
}
|
||||
|
||||
// Reads the directive an `!adf:` at `index` claims.
|
||||
// The caller has checked `claimsDirectivePrefix` at `index`.
|
||||
export function readInlineDirective(text: string, index: number): Read<DirectiveSpan> {
|
||||
return readClaimedDirective(text, index, 1)
|
||||
}
|
||||
|
||||
@@ -32,10 +32,21 @@ type EmittedDelimiter = { closes: boolean; offset: number; pair: number; width:
|
||||
|
||||
type EmittedRun = { canClose: boolean; canOpen: boolean; character: string; delimiters: EmittedDelimiter[]; length: number; start: number }
|
||||
|
||||
const runCharacters = '*_`~'
|
||||
// What stays fixed while one line's escapes are decided; `escaped` collects them.
|
||||
type EscapeWalk = {
|
||||
claims: Claims
|
||||
container: LineContainer
|
||||
escaped: Set<number>
|
||||
escapings: readonly InlineEscaping[]
|
||||
headingCloser: number | undefined
|
||||
linkClose: number
|
||||
scan: string
|
||||
}
|
||||
|
||||
const followsLinkText = /[([]/
|
||||
|
||||
const runCharacters = '*_`~'
|
||||
|
||||
export function assembleInlineLine(segments: readonly InlineSegment[], container: LineContainer, flavour: WrittenFlavour): AssembledLine {
|
||||
return escape(resolveEmphasis(segments), container, flavourClaims[flavour])
|
||||
}
|
||||
@@ -89,30 +100,38 @@ 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
|
||||
const walk: EscapeWalk = {
|
||||
claims,
|
||||
container,
|
||||
escaped: new Set(),
|
||||
escapings,
|
||||
headingCloser: container === 'heading' ? closingHashes(scan) : undefined,
|
||||
linkClose: lastLinkClose(scan, escapings),
|
||||
scan,
|
||||
}
|
||||
let line = scanLine(scan, 0)
|
||||
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)
|
||||
index = escapeRun(walk, index, line)
|
||||
continue
|
||||
}
|
||||
const escaping = escapings[index]
|
||||
const token = readInlineToken(scan, index, claims)
|
||||
const claimed = isEscapable(escaping)
|
||||
? index === headingCloser || claimsLineStart(line, index, container) || claimsCharacter(scan, escapings, linkClose, index, token, container)
|
||||
? index === walk.headingCloser || claimsLineStart(line, index, container) || claimsCharacter(walk, index, token)
|
||||
: escaping === 'bracketed-link-target' && token.kind === 'directive'
|
||||
if (claimed) escaped.add(index)
|
||||
if (claimed) walk.escaped.add(index)
|
||||
// The reader takes a `==` whole, so its second `=` starts nothing unless the first is escaped.
|
||||
index += !claimed && token.kind === 'highlight' ? token.width : 1
|
||||
}
|
||||
return escaped
|
||||
return walk.escaped
|
||||
}
|
||||
|
||||
// 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 {
|
||||
function escapeRun(walk: EscapeWalk, start: number, line: ScanLine): number {
|
||||
const { escaped, escapings, scan } = walk
|
||||
const end = start + runLength(scan, start)
|
||||
const run = escapings.slice(start, end)
|
||||
if (run.some(isSyntax) && run.some(isEscapable)) {
|
||||
@@ -121,13 +140,13 @@ function escapeRun(scan: string, escapings: readonly InlineEscaping[], start: nu
|
||||
}
|
||||
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
|
||||
if (!(index > start && scan.charAt(index) === '`') && !claimsRunRest(walk, index, end, line)) break
|
||||
escaped.add(index)
|
||||
}
|
||||
return end
|
||||
}
|
||||
|
||||
function claimsRunRest(scan: string, escapings: readonly InlineEscaping[], index: number, end: number, line: ScanLine, container: LineContainer, claims: Claims): boolean {
|
||||
function claimsRunRest({ claims, container, escapings, scan }: EscapeWalk, index: number, end: number, line: ScanLine): boolean {
|
||||
const escaping = escapings[index]
|
||||
if (escaping === 'bracketed-link-target') return readInlineToken(scan, index, claims).kind === 'code-span'
|
||||
if (!isEscapable(escaping)) return false
|
||||
@@ -136,7 +155,7 @@ function claimsRunRest(scan: string, escapings: readonly InlineEscaping[], index
|
||||
return token.kind !== 'text'
|
||||
}
|
||||
|
||||
function claimsCharacter(scan: string, escapings: readonly InlineEscaping[], linkClose: number, index: number, token: InlineToken, container: LineContainer): boolean {
|
||||
function claimsCharacter({ container, escapings, linkClose, scan }: EscapeWalk, index: number, token: InlineToken): boolean {
|
||||
const character = scan.charAt(index)
|
||||
const inBrackets = escapings[index] === 'bracketed'
|
||||
switch (token.kind) {
|
||||
|
||||
@@ -6,22 +6,23 @@ 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.
|
||||
// What the inline reader reads from a token start, `width` characters on; `text` reads literally, a directive reads by its own grammar, and HTML is refused.
|
||||
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 }
|
||||
| { kind: 'bracket-close' | 'entity' | 'escape' | 'hard-break' | 'line-ending' | 'text'; width: number }
|
||||
| { kind: 'code-span'; opener: number; width: number }
|
||||
| { canClose: boolean; canOpen: boolean; kind: 'delimiter-run'; width: number }
|
||||
| { kind: 'directive' }
|
||||
| { closes: boolean; kind: 'highlight'; opens: boolean; width: number }
|
||||
| { construct: string; kind: 'html' }
|
||||
|
||||
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 }
|
||||
if (claims.directives && claimsDirectivePrefix(source, index)) return { kind: 'directive' }
|
||||
return source.charAt(index + 1) === '[' ? { image: true, kind: 'bracket', width: 2 } : text(1)
|
||||
case '[':
|
||||
return { image: false, kind: 'bracket', width: 1 }
|
||||
@@ -63,7 +64,7 @@ function angleToken(source: string, index: number): InlineToken {
|
||||
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 }
|
||||
return construct === undefined ? text(1) : { construct, kind: 'html' }
|
||||
}
|
||||
|
||||
function backtickToken(source: string, index: number): InlineToken {
|
||||
|
||||
@@ -119,46 +119,49 @@ function scanInline(scan: Scan): Result<Scanned> {
|
||||
}
|
||||
|
||||
function readToken(scan: Scan, index: number, token: InlineToken): Result<number> {
|
||||
const end = index + token.width
|
||||
switch (token.kind) {
|
||||
case 'autolink':
|
||||
case 'autolink': {
|
||||
const end = index + token.width
|
||||
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)
|
||||
scan.pieces.push({ active: true, image: token.image, kind: 'open', start: index + token.width })
|
||||
return success(index + token.width)
|
||||
case 'bracket-close':
|
||||
return closeBracket(scan, index)
|
||||
case 'code-span':
|
||||
case 'code-span': {
|
||||
const end = index + token.width
|
||||
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)
|
||||
return success(index + token.width)
|
||||
case 'directive':
|
||||
return readDirective(scan, index)
|
||||
case 'entity':
|
||||
case 'escape':
|
||||
case 'text':
|
||||
scan.pending += scan.source.slice(index, index + token.width)
|
||||
return success(index + token.width)
|
||||
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)
|
||||
return success(index + token.width)
|
||||
case 'highlight':
|
||||
flush(scan, false)
|
||||
scan.pieces.push({ closes: token.closes, kind: 'highlight', opens: token.opens })
|
||||
return success(end)
|
||||
return success(index + token.width)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
`Bar = 9`
|
||||
|
||||
`Next ID = 96`
|
||||
`Next ID = 97`
|
||||
|
||||
| Goal | W |
|
||||
|---|---|
|
||||
@@ -34,6 +34,7 @@
|
||||
| 62 | 0.2.0 | decision | **Move the portable flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/portable/`, so `emit/` no longer imports `portable/`.** | 4 | 5 | 2 | 6 | 2 | 9.4 |
|
||||
| 92 | 0.2.0 | | **Lower the reading cost of the files a chunk reads most before its first commit.** | 2 | 4 | 2 | 4 | 1 | 9.0 |
|
||||
| 89 | 0.2.1 | | **Build a `date`, `emoji`, `inlineCard`, `mention` or `status` holding no mark Atlassian's schema withholds from it, in every reader.** | 4 | 4 | 5 | 6 | 4 | 13.0 |
|
||||
| 96 | 0.2.1 | defect | **Read and write a text holding many `<!--`, `<?`, `<![CDATA[` or `<!` openers without a terminator in time linear in the text.** | 2 | 3 | 5 | 9 | 9 | 9.2 |
|
||||
| 33 | 0.2.1 | defect | **Emit a line in time linear in its mark runs, in `adfToLosslessMarkdown` and `adfToPortableMarkdown`.** | 4 | 5 | 6 | 9 | 9 | 8.7 |
|
||||
| 7 | 0.3.0 | decision | **Ship HTML: `adfToHtml`, `htmlToAdf`, and markdown to and from HTML composed through ADF, each call named by its flavour.** | 6 | 9 | 9 | 9 | 2, 4 | 25.8 |
|
||||
| 45 | 0.3.0 | | **Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.** | 2 | 3 | 6 | 8 | 1 | 25.2 |
|
||||
@@ -134,6 +135,12 @@ them 68188. Read most over the last five, opens and searches: `README.md` 32, `d
|
||||
may reject the document. Replace `InlineNodeModel.takesMarks` (`src/adf/inline-nodes.ts`) with the
|
||||
mark types a node takes, and have the emitter carry a node holding another.
|
||||
|
||||
### 96. Read and write a text holding many `<!--`, `<?`, `<![CDATA[` or `<!` openers without a terminator in time linear in the text.
|
||||
|
||||
`inlineHtmlConstruct` (`src/markdown/commonmark/grammar.ts`) searches the rest of the text for the
|
||||
opener's terminator at every opener, so `<!--` repeated with no `-->` is quadratic in both
|
||||
directions: the reader and the emitter's escape walk both ask it through `readInlineToken`.
|
||||
|
||||
### 33. Emit a line in time linear in its mark runs, in `adfToLosslessMarkdown` and `adfToPortableMarkdown`.
|
||||
|
||||
`adfToLosslessMarkdown` spends 23 s on one paragraph of 2000 × `un` plus `**-r**`: each run its
|
||||
@@ -279,7 +286,8 @@ gains its row.
|
||||
|
||||
`src/markdown/emit/line-escaping.ts` asks the reader's tokens where text would read as syntax, but
|
||||
`escapeClosedRuns` still works out which backtick strings an escape leaves, and `unspellableRuns`
|
||||
rebuilds the reader's emphasis and highlight pairing to pick the run to carry. Reading the line
|
||||
rebuilds the reader's emphasis and highlight pairing to pick the run to carry, and `lastLinkClose`
|
||||
guesses which text `[` opens link text. Reading the line
|
||||
back costs a parse per line attempt, and the run it picks may differ, changing output.
|
||||
|
||||
### 91. Spell an emphasis or link once across the hard breaks between two nodes it marks, in both writers.
|
||||
|
||||
Reference in New Issue
Block a user