26 - the escape phases and the container stack hold their invariants in types #125
@@ -67,7 +67,7 @@ function escape(segments: readonly InlineSegment[], container: LineContainer): A
|
|||||||
const scan = segments.map((segment) => segment.text).join('')
|
const scan = segments.map((segment) => segment.text).join('')
|
||||||
const escapings: InlineEscaping[] = []
|
const escapings: InlineEscaping[] = []
|
||||||
for (const segment of segments) for (let index = 0; index < segment.text.length; index += 1) escapings.push(segment.escaping)
|
for (const segment of segments) for (let index = 0; index < segment.text.length; index += 1) escapings.push(segment.escaping)
|
||||||
const escaped = escapedIndexes(scan, escapings, container)
|
const escaped = escapeClosedRuns(scan, escapings, escapeClaims(scan, escapings, container))
|
||||||
const placements: number[] = []
|
const placements: number[] = []
|
||||||
let output = ''
|
let output = ''
|
||||||
for (let index = 0; index < scan.length; index += 1) {
|
for (let index = 0; index < scan.length; index += 1) {
|
||||||
@@ -82,31 +82,30 @@ function escape(segments: readonly InlineSegment[], container: LineContainer): A
|
|||||||
return { line: output, unspellableRun: unspellableRun(segments, output, placements) }
|
return { line: output, unspellableRun: unspellableRun(segments, output, placements) }
|
||||||
}
|
}
|
||||||
|
|
||||||
function escapedIndexes(scan: string, escapings: readonly InlineEscaping[], container: LineContainer): Set<number> {
|
function escapeClaims(scan: string, escapings: readonly InlineEscaping[], container: LineContainer): ReadonlySet<number> {
|
||||||
const escaped = new Set<number>()
|
const escaped = new Set<number>()
|
||||||
const linkClose = lastLinkClose(scan, escapings)
|
const linkClose = lastLinkClose(scan, escapings)
|
||||||
let line = scanLine(scan, 0)
|
let line = scanLine(scan, 0)
|
||||||
|
let afterEscape = false
|
||||||
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)
|
||||||
const escaping = escapings[index]
|
const escaping = escapings[index]
|
||||||
const escapable = escaping === 'backslash' || escaping === 'bracketed'
|
const escapable = escaping === 'backslash' || escaping === 'bracketed'
|
||||||
if (
|
afterEscape =
|
||||||
(escapable &&
|
(escapable &&
|
||||||
(claimsLineStart(line, index, container) ||
|
(claimsLineStart(line, index, container) ||
|
||||||
mergesWithSyntax(scan, escapings, index) ||
|
mergesWithSyntax(scan, escapings, index) ||
|
||||||
opensConstruct(scan, linkClose, index, escaping === 'bracketed', container, escaped))) ||
|
opensConstruct(scan, linkClose, index, escaping === 'bracketed', container, afterEscape))) ||
|
||||||
(escaping === 'bracketed-link-target' &&
|
(escaping === 'bracketed-link-target' &&
|
||||||
((scan.charAt(index) === '`' && opensCodeSpan(scan, index, escaped)) || claimsDirectivePrefix(scan, index)))
|
((scan.charAt(index) === '`' && opensCodeSpan(scan, index, afterEscape)) || claimsDirectivePrefix(scan, index)))
|
||||||
) {
|
if (afterEscape) escaped.add(index)
|
||||||
escaped.add(index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
escapeClosedRuns(scan, escapings, escaped)
|
|
||||||
return escaped
|
return escaped
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommonMark reads no escape inside a code span, so a backtick string an escape forms or splits off still closes one an earlier bare run opens.
|
// CommonMark reads no escape inside a code span, so a backtick string an escape forms or splits off still closes one an earlier bare run opens.
|
||||||
function escapeClosedRuns(scan: string, escapings: readonly InlineEscaping[], escaped: Set<number>): void {
|
function escapeClosedRuns(scan: string, escapings: readonly InlineEscaping[], claimed: ReadonlySet<number>): ReadonlySet<number> {
|
||||||
|
const escaped = new Set(claimed)
|
||||||
const formed = new Set<number>()
|
const formed = new Set<number>()
|
||||||
let end = scan.length - 1
|
let end = scan.length - 1
|
||||||
while (end >= 0) {
|
while (end >= 0) {
|
||||||
@@ -118,17 +117,18 @@ function escapeClosedRuns(scan: string, escapings: readonly InlineEscaping[], es
|
|||||||
while (scan.charAt(start - 1) === '`') start -= 1
|
while (scan.charAt(start - 1) === '`') start -= 1
|
||||||
let segmentEnd = end
|
let segmentEnd = end
|
||||||
for (let index = end; index > start; index -= 1) {
|
for (let index = end; index > start; index -= 1) {
|
||||||
if (!escaped.has(index)) continue
|
if (!claimed.has(index)) continue
|
||||||
formed.add(segmentEnd - index + 1)
|
formed.add(segmentEnd - index + 1)
|
||||||
segmentEnd = index - 1
|
segmentEnd = index - 1
|
||||||
}
|
}
|
||||||
if (segmentEnd !== end || escaped.has(start)) formed.add(segmentEnd - start + 1)
|
if (segmentEnd !== end || claimed.has(start)) formed.add(segmentEnd - start + 1)
|
||||||
else if (escapings[start] !== 'none' && formed.has(end - start + 1)) {
|
else if (escapings[start] !== 'none' && formed.has(end - start + 1)) {
|
||||||
for (let index = start; index <= end; index += 1) escaped.add(index)
|
for (let index = start; index <= end; index += 1) escaped.add(index)
|
||||||
formed.add(1)
|
formed.add(1)
|
||||||
}
|
}
|
||||||
end = start - 1
|
end = start - 1
|
||||||
}
|
}
|
||||||
|
return escaped
|
||||||
}
|
}
|
||||||
|
|
||||||
function unspellableRun(segments: readonly InlineSegment[], output: string, placements: readonly number[]): NodeRange | undefined {
|
function unspellableRun(segments: readonly InlineSegment[], output: string, placements: readonly number[]): NodeRange | undefined {
|
||||||
@@ -231,10 +231,10 @@ function opensConstruct(
|
|||||||
index: number,
|
index: number,
|
||||||
inBrackets: boolean,
|
inBrackets: boolean,
|
||||||
container: LineContainer,
|
container: LineContainer,
|
||||||
escaped: ReadonlySet<number>,
|
afterEscape: boolean,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (container === 'heading' && closesHeading(scan, index)) return true
|
if (container === 'heading' && closesHeading(scan, index)) return true
|
||||||
return claimsCharacter(scan, linkClose, index, inBrackets, container, escaped)
|
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.
|
// A hard break is the one spelling that puts a delimiter row under a row of its own, so only a later line claims.
|
||||||
@@ -260,7 +260,7 @@ function claimsCharacter(
|
|||||||
index: number,
|
index: number,
|
||||||
inBrackets: boolean,
|
inBrackets: boolean,
|
||||||
container: LineContainer,
|
container: LineContainer,
|
||||||
escaped: ReadonlySet<number>,
|
afterEscape: boolean,
|
||||||
): boolean {
|
): boolean {
|
||||||
const character = scan.charAt(index)
|
const character = scan.charAt(index)
|
||||||
if (inBrackets && (character === '[' || character === ']')) return true
|
if (inBrackets && (character === '[' || character === ']')) return true
|
||||||
@@ -270,8 +270,8 @@ function claimsCharacter(
|
|||||||
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 claimsDirectivePrefix(scan, index)
|
if (character === '!') return claimsDirectivePrefix(scan, index)
|
||||||
if (character === '[') return index < linkClose
|
if (character === '[') return index < linkClose
|
||||||
if (character === '`') return opensCodeSpan(scan, index, escaped)
|
if (character === '`') return opensCodeSpan(scan, index, afterEscape)
|
||||||
if (character === '*' || character === '_' || character === '~') return claimsEmphasis(scan, index, escaped)
|
if (character === '*' || character === '_' || character === '~') return claimsEmphasis(scan, index, afterEscape)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,16 +284,16 @@ function lastLinkClose(scan: string, escapings: readonly (InlineEscaping | undef
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
function opensCodeSpan(scan: string, index: number, escaped: ReadonlySet<number>): boolean {
|
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.
|
// A run escapes whole: a rest left bare would be a raw run of another length for a closer.
|
||||||
if (scan.charAt(index - 1) === '`' && escaped.has(index - 1)) return true
|
if (afterEscape && scan.charAt(index - 1) === '`') return true
|
||||||
if (!startsRun(scan, index, escaped)) return false
|
if (!startsRun(scan, index, afterEscape)) return false
|
||||||
const opener = backtickRun(scan, index)
|
const opener = backtickRun(scan, index)
|
||||||
return closingBacktickRun(scan, index + opener, opener) !== undefined
|
return closingBacktickRun(scan, index + opener, opener) !== undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function claimsEmphasis(scan: string, index: number, escaped: ReadonlySet<number>): boolean {
|
function claimsEmphasis(scan: string, index: number, afterEscape: boolean): boolean {
|
||||||
if (!startsRun(scan, index, escaped)) return false
|
if (!startsRun(scan, index, afterEscape)) return false
|
||||||
const character = scan.charAt(index)
|
const character = scan.charAt(index)
|
||||||
const length = runLength(scan, index)
|
const length = runLength(scan, index)
|
||||||
if (character === '~' && length !== 2) return false
|
if (character === '~' && length !== 2) return false
|
||||||
@@ -301,8 +301,8 @@ function claimsEmphasis(scan: string, index: number, escaped: ReadonlySet<number
|
|||||||
return flags.canClose || flags.canOpen
|
return flags.canClose || flags.canOpen
|
||||||
}
|
}
|
||||||
|
|
||||||
function startsRun(scan: string, index: number, escaped: ReadonlySet<number>): boolean {
|
function startsRun(scan: string, index: number, afterEscape: boolean): boolean {
|
||||||
if (index === 0 || escaped.has(index - 1)) return true
|
if (index === 0 || afterEscape) return true
|
||||||
return scan.charAt(index - 1) !== scan.charAt(index)
|
return scan.charAt(index - 1) !== scan.charAt(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,13 +64,19 @@ type Line = { column: number; text: string }
|
|||||||
|
|
||||||
type LeafOpener = { index: number; position: SourcePosition }
|
type LeafOpener = { index: number; position: SourcePosition }
|
||||||
|
|
||||||
|
type ContainerStack = {
|
||||||
|
drop: (depth: number) => OpenContainer[]
|
||||||
|
edges: readonly { container: EdgeContainer; depth: number }[]
|
||||||
|
open: readonly OpenContainer[]
|
||||||
|
push: (container: OpenContainer) => void
|
||||||
|
}
|
||||||
|
|
||||||
type Walk = ParsedBlocks & {
|
type Walk = ParsedBlocks & {
|
||||||
directiveDepths: Map<string, number[]>
|
directiveDepths: Map<string, number[]>
|
||||||
edges: { container: EdgeContainer; depth: number }[]
|
|
||||||
leaf: OpenLeaf | undefined
|
leaf: OpenLeaf | undefined
|
||||||
leafOpeners: Map<Block[], Map<string, LeafOpener>>
|
leafOpeners: Map<Block[], Map<string, LeafOpener>>
|
||||||
position: SourcePosition
|
position: SourcePosition
|
||||||
stack: OpenContainer[]
|
stack: ContainerStack
|
||||||
}
|
}
|
||||||
|
|
||||||
const indentedCodeColumns = 4
|
const indentedCodeColumns = 4
|
||||||
@@ -82,11 +88,10 @@ export function parseBlocks(markdown: string): ParsedBlocks {
|
|||||||
blocks: [],
|
blocks: [],
|
||||||
definitions: new Map(),
|
definitions: new Map(),
|
||||||
directiveDepths: new Map(),
|
directiveDepths: new Map(),
|
||||||
edges: [],
|
|
||||||
leaf: undefined,
|
leaf: undefined,
|
||||||
leafOpeners: new Map(),
|
leafOpeners: new Map(),
|
||||||
position: { line: 1, offset: 0 },
|
position: { line: 1, offset: 0 },
|
||||||
stack: [],
|
stack: containerStack(),
|
||||||
}
|
}
|
||||||
for (const line of sourceLines(markdown)) {
|
for (const line of sourceLines(markdown)) {
|
||||||
walk.position = line.position
|
walk.position = line.position
|
||||||
@@ -96,16 +101,38 @@ export function parseBlocks(markdown: string): ParsedBlocks {
|
|||||||
return { blocks: walk.blocks, definitions: walk.definitions }
|
return { blocks: walk.blocks, definitions: walk.definitions }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function containerStack(): ContainerStack {
|
||||||
|
const edges: { container: EdgeContainer; depth: number }[] = []
|
||||||
|
const open: OpenContainer[] = []
|
||||||
|
return {
|
||||||
|
drop: (depth) => {
|
||||||
|
const dropped = open.splice(depth)
|
||||||
|
for (const container of dropped) {
|
||||||
|
if (container.kind === 'directive') container.depths.pop()
|
||||||
|
else edges.pop()
|
||||||
|
}
|
||||||
|
return dropped
|
||||||
|
},
|
||||||
|
edges,
|
||||||
|
open,
|
||||||
|
push: (container) => {
|
||||||
|
const depth = open.push(container) - 1
|
||||||
|
if (container.kind === 'directive') container.depths.push(depth)
|
||||||
|
else edges.push({ container, depth })
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function readLine(walk: Walk, line: Line): void {
|
function readLine(walk: Walk, line: Line): void {
|
||||||
const matched = matchContainers(walk, line)
|
const matched = matchContainers(walk, line)
|
||||||
// CommonMark: no container opens inside an open code or HTML block.
|
// CommonMark: no container opens inside an open code or HTML block.
|
||||||
if (matched.depth === walk.stack.length && swallowsLines(walk.leaf)) {
|
if (matched.depth === walk.stack.open.length && swallowsLines(walk.leaf)) {
|
||||||
readBlockLine(walk, matched.rest)
|
readBlockLine(walk, matched.rest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const paragraphOpen = matched.depth === walk.stack.length && walk.leaf?.kind === 'paragraph'
|
const paragraphOpen = matched.depth === walk.stack.open.length && walk.leaf?.kind === 'paragraph'
|
||||||
const opened = openContainers(walk, matched.rest, paragraphOpen, matched.depth)
|
const opened = openContainers(walk, matched.rest, paragraphOpen, matched.depth)
|
||||||
if (!opened.opened && matched.depth < walk.stack.length) {
|
if (!opened.opened && matched.depth < walk.stack.open.length) {
|
||||||
if (continuesLazily(walk, opened.rest)) {
|
if (continuesLazily(walk, opened.rest)) {
|
||||||
appendParagraph(walk, opened.rest.text)
|
appendParagraph(walk, opened.rest.text)
|
||||||
return
|
return
|
||||||
@@ -122,12 +149,12 @@ function swallowsLines(leaf: OpenLeaf | undefined): boolean {
|
|||||||
// A directive container has no continuation marker, so every line continues it.
|
// A directive container has no continuation marker, so every line continues it.
|
||||||
function matchContainers(walk: Walk, line: Line): { depth: number; rest: Line } {
|
function matchContainers(walk: Walk, line: Line): { depth: number; rest: Line } {
|
||||||
let rest = line
|
let rest = line
|
||||||
for (const { container, depth } of walk.edges) {
|
for (const { container, depth } of walk.stack.edges) {
|
||||||
const next = continuesContainer(walk, container, rest)
|
const next = continuesContainer(walk, container, rest)
|
||||||
if (next === undefined) return { depth, rest }
|
if (next === undefined) return { depth, rest }
|
||||||
rest = next
|
rest = next
|
||||||
}
|
}
|
||||||
return { depth: walk.stack.length, rest }
|
return { depth: walk.stack.open.length, rest }
|
||||||
}
|
}
|
||||||
|
|
||||||
function continuesContainer(walk: Walk, container: EdgeContainer, line: Line): Line | undefined {
|
function continuesContainer(walk: Walk, container: EdgeContainer, line: Line): Line | undefined {
|
||||||
@@ -145,7 +172,7 @@ function blockquoteRest(opener: Line): Line | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function openContainers(walk: Walk, line: Line, paragraphOpen: boolean, depth: number): { opened: boolean; rest: Line } {
|
function openContainers(walk: Walk, line: Line, paragraphOpen: boolean, depth: number): { opened: boolean; rest: Line } {
|
||||||
const unmatched = walk.stack[depth]
|
const unmatched = walk.stack.open[depth]
|
||||||
const tail = thematicBreakTail(line.text)
|
const tail = thematicBreakTail(line.text)
|
||||||
let opened = false
|
let opened = false
|
||||||
let rest = line
|
let rest = line
|
||||||
@@ -202,16 +229,12 @@ function openContainer(walk: Walk, start: ContainerStart): void {
|
|||||||
if (start.kind === 'blockquote') {
|
if (start.kind === 'blockquote') {
|
||||||
const blockquote: EdgeContainer = { blocks, kind: 'blockquote', position: walk.position }
|
const blockquote: EdgeContainer = { blocks, kind: 'blockquote', position: walk.position }
|
||||||
currentBlocks(walk).push(blockquote)
|
currentBlocks(walk).push(blockquote)
|
||||||
pushEdge(walk, blockquote)
|
walk.stack.push(blockquote)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const list = openedList(walk, start)
|
const list = openedList(walk, start)
|
||||||
list.items.push(blocks)
|
list.items.push(blocks)
|
||||||
pushEdge(walk, { blocks, indentation: start.indentation, kind: 'item', list })
|
walk.stack.push({ blocks, indentation: start.indentation, kind: 'item', list })
|
||||||
}
|
|
||||||
|
|
||||||
function pushEdge(walk: Walk, container: EdgeContainer): void {
|
|
||||||
walk.edges.push({ container, depth: walk.stack.push(container) - 1 })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Two lists of a kind never sit adjacent: one `- ` spelling reads them back as one (spec/flavour.md).
|
// Two lists of a kind never sit adjacent: one `- ` spelling reads them back as one (spec/flavour.md).
|
||||||
@@ -226,7 +249,7 @@ function openedList(walk: Walk, start: Extract<ContainerStart, { kind: 'item' }>
|
|||||||
|
|
||||||
function closeContainers(walk: Walk, depth: number): void {
|
function closeContainers(walk: Walk, depth: number): void {
|
||||||
closeLeaf(walk)
|
closeLeaf(walk)
|
||||||
for (const container of dropContainers(walk, depth)) {
|
for (const container of walk.stack.drop(depth)) {
|
||||||
if (container.kind !== 'directive') continue
|
if (container.kind !== 'directive') continue
|
||||||
container.parent[container.index] = {
|
container.parent[container.index] = {
|
||||||
fault: malformedDirective(`the ${container.name} container is unclosed: no ${spellDirectiveCloser(container.name)} follows inside the block holding it; ${directiveEscape}`),
|
fault: malformedDirective(`the ${container.name} container is unclosed: no ${spellDirectiveCloser(container.name)} follows inside the block holding it; ${directiveEscape}`),
|
||||||
@@ -236,15 +259,6 @@ function closeContainers(walk: Walk, depth: number): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dropContainers(walk: Walk, depth: number): OpenContainer[] {
|
|
||||||
const dropped = walk.stack.splice(depth)
|
|
||||||
for (const container of dropped) {
|
|
||||||
if (container.kind === 'directive') container.depths.pop()
|
|
||||||
else walk.edges.pop()
|
|
||||||
}
|
|
||||||
return dropped
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyDirectiveLine(walk: Walk, directive: DirectiveLine): void {
|
function applyDirectiveLine(walk: Walk, directive: DirectiveLine): void {
|
||||||
if (directive.kind === 'closer') closeDirective(walk, directive.name)
|
if (directive.kind === 'closer') closeDirective(walk, directive.name)
|
||||||
else openDirective(walk, directive)
|
else openDirective(walk, directive)
|
||||||
@@ -268,7 +282,7 @@ function openDirective(walk: Walk, directive: Extract<DirectiveLine, { kind: 'op
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
const depths = entryOf(walk.directiveDepths, name, (): number[] => [])
|
const depths = entryOf(walk.directiveDepths, name, (): number[] => [])
|
||||||
depths.push(walk.stack.push({ blocks: block.blocks, depths, index, kind: 'directive', name, parent, position }) - 1)
|
walk.stack.push({ blocks: block.blocks, depths, index, kind: 'directive', name, parent, position })
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeDirective(walk: Walk, name: string): void {
|
function closeDirective(walk: Walk, name: string): void {
|
||||||
@@ -283,13 +297,13 @@ function closeDirective(walk: Walk, name: string): void {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
closeContainers(walk, depth + 1)
|
closeContainers(walk, depth + 1)
|
||||||
dropContainers(walk, depth)
|
walk.stack.drop(depth)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A closer crosses no list item or blockquote edge.
|
// A closer crosses no list item or blockquote edge.
|
||||||
function openDirectiveDepth(walk: Walk, name: string): number | undefined {
|
function openDirectiveDepth(walk: Walk, name: string): number | undefined {
|
||||||
const depth = walk.directiveDepths.get(name)?.at(-1)
|
const depth = walk.directiveDepths.get(name)?.at(-1)
|
||||||
return depth === undefined || depth < (walk.edges.at(-1)?.depth ?? -1) ? undefined : depth
|
return depth === undefined || depth < (walk.stack.edges.at(-1)?.depth ?? -1) ? undefined : depth
|
||||||
}
|
}
|
||||||
|
|
||||||
function faultLeafOpener(walk: Walk, name: string, fault: ConvertFault): void {
|
function faultLeafOpener(walk: Walk, name: string, fault: ConvertFault): void {
|
||||||
@@ -497,7 +511,7 @@ function takeParagraph(walk: Walk): Extract<Block, { kind: 'paragraph' }> | unde
|
|||||||
}
|
}
|
||||||
|
|
||||||
function currentBlocks(walk: Walk): Block[] {
|
function currentBlocks(walk: Walk): Block[] {
|
||||||
return walk.stack.at(-1)?.blocks ?? walk.blocks
|
return walk.stack.open.at(-1)?.blocks ?? walk.blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
function* sourceLines(markdown: string): Generator<{ position: SourcePosition; text: string }> {
|
function* sourceLines(markdown: string): Generator<{ position: SourcePosition; text: string }> {
|
||||||
|
|||||||
@@ -1054,6 +1054,18 @@ The done `todo.md` items in full, as they were written. `todo.md` keeps a one-li
|
|||||||
layout. What `src/result.ts`, `README.md` §The errors and `ls src` already say went, and
|
layout. What `src/result.ts`, `README.md` §The errors and `ls src` already say went, and
|
||||||
the level's definition, `matchEmphasis`'s transcription and `emitLine`'s termination moved
|
the level's definition, `matchEmphasis`'s transcription and `emitLine`'s termination moved
|
||||||
to one line at the code each defines.
|
to one line at the code each defines.
|
||||||
|
- [x] **26 — The two mutable structures say what they guarantee (`0.2.0`).** `escapedIndexes` fills
|
||||||
|
the `escaped` set left to right while the predicates it calls read the half-built set, then
|
||||||
|
`escapeClosedRuns` walks the same set right to left and adds to it; the order is load-bearing
|
||||||
|
and asserted nowhere, and a refactor to `filter`/`map` breaks it silently. Separately,
|
||||||
|
`walk.edges` is the blockquote and list-item subsequence of `walk.stack` with each entry's
|
||||||
|
stack index, maintained by hand in six places and stated in none. Put each invariant where it
|
||||||
|
cannot be got wrong — a type, a derived value, a named phase — rather than in a comment. The
|
||||||
|
panel's first and second hardest places. `escapeClaims` hands each predicate the one fact
|
||||||
|
it reads — whether the character before was escaped — and `escapeClosedRuns` takes that
|
||||||
|
pass's set read-only and returns the whole. `containerStack` is the one writer of the stack,
|
||||||
|
`edges` and each directive's `depths`, all read-only elsewhere; deriving `edges` from the
|
||||||
|
stack instead makes every line walk the open directives, quadratic in their nesting.
|
||||||
|
|
||||||
## 5 — Ship `0.1.0`
|
## 5 — Ship `0.1.0`
|
||||||
|
|
||||||
|
|||||||
@@ -58,14 +58,7 @@ chunk clearing a §11 seam.
|
|||||||
the measured one.
|
the measured one.
|
||||||
- [x] **24 — The conformance gates have a directory (`0.2.0`).**
|
- [x] **24 — The conformance gates have a directory (`0.2.0`).**
|
||||||
- [x] **25 — AGENTS.md §8 and §11 are findable (`0.2.0`).**
|
- [x] **25 — AGENTS.md §8 and §11 are findable (`0.2.0`).**
|
||||||
- [ ] **26 — The two mutable structures say what they guarantee (`0.2.0`).** `escapedIndexes` fills
|
- [x] **26 — The two mutable structures say what they guarantee (`0.2.0`).**
|
||||||
the `escaped` set left to right while the predicates it calls read the half-built set, then
|
|
||||||
`escapeClosedRuns` walks the same set right to left and adds to it; the order is load-bearing
|
|
||||||
and asserted nowhere, and a refactor to `filter`/`map` breaks it silently. Separately,
|
|
||||||
`walk.edges` is the blockquote and list-item subsequence of `walk.stack` with each entry's
|
|
||||||
stack index, maintained by hand in six places and stated in none. Put each invariant where it
|
|
||||||
cannot be got wrong — a type, a derived value, a named phase — rather than in a comment. The
|
|
||||||
panel's first and second hardest places.
|
|
||||||
- [ ] **27 — The dead `headroom` write goes (`0.2.0`).** `directiveItems`
|
- [ ] **27 — The dead `headroom` write goes (`0.2.0`).** `directiveItems`
|
||||||
(`emit/adf-to-markdown.ts:258`) writes `item.walk.headroom - 1` onto each `PlacedBlock`, and
|
(`emit/adf-to-markdown.ts:258`) writes `item.walk.headroom - 1` onto each `PlacedBlock`, and
|
||||||
nothing on that path reads a block's `headroom`: `joinBlocks` and `separationBetween` read
|
nothing on that path reads a block's `headroom`: `joinBlocks` and `separationBetween` read
|
||||||
|
|||||||
Reference in New Issue
Block a user