mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Answer architecture round 1: 0 🔴, 7 🟡 (0 misread); the read-back loop parts from the verdict, the reader takes the opening-link consult, the reading is absent on the parse path, a refused link escapes no backtick; root imports, highlight pairing and closeBracket filed as items 153 to 155
This commit is contained in:
+2
-1
@@ -950,7 +950,8 @@ by reading the escaped line back.
|
||||
A writer reads each escaped line back. Where a code span opens at a text backtick, every text
|
||||
backtick in the line escapes, and where a link or an image opens at a text `[`, every text `[`
|
||||
does: `[a](b) [c]` is written `\[a](b) \[c]`. A text `[` or backtick that opens nothing stays
|
||||
bare. Escaping only up to the last construct text opens read as arbitrary to the panel, and
|
||||
bare. A line the reader refuses without showing what opened at text escapes every text backtick
|
||||
and `[`. Escaping only up to the last construct text opens read as arbitrary to the panel, and
|
||||
escaping each opener in turn costs a read per opener, against goal 9. The grammar:
|
||||
`spec/flavour.md` §Canonical form.
|
||||
|
||||
|
||||
+2
-1
@@ -57,7 +57,8 @@ normalizes to it through the round-trip.
|
||||
A backtick run escapes whole. The escaped line is then read back. Where it opens a code span at a
|
||||
text backtick, every text backtick in the line escapes: CommonMark reads no escape inside a code
|
||||
span, so a backtick string beside an escape can still close one. Where it opens a link or an
|
||||
image at a text `[`, every text `[` in the line escapes.
|
||||
image at a text `[`, every text `[` in the line escapes. A line the reader refuses without
|
||||
showing what opened at text escapes every text backtick and `[`.
|
||||
Where a paragraph opens with what reads as a link reference definition, which resolves before
|
||||
any inline construct binds (a `]` inside a code span or `{attrs}` counts), an opening text `[`
|
||||
escapes and an opening link takes the directive link (Marks).
|
||||
|
||||
@@ -351,6 +351,11 @@ test('escapes every text bracket in a line where one opens link text, and every
|
||||
assert.equal(emitted('Use ` or ``code`` syntax'), 'Use \\` or \\`\\`code\\`\\` syntax\n')
|
||||
})
|
||||
|
||||
test('keeps a text backtick bare in a line where a text bracket opens a link the reader refuses', () => {
|
||||
const refused = paragraph({ text: 'a ` [', type: 'text' }, { attrs: { id: '1' }, type: 'mention' }, { text: '](u)', type: 'text' })
|
||||
assert.equal(markdown(adfToLosslessMarkdown(document(refused))), 'a ` \\[!adf:mention{id=1}](u)\n')
|
||||
})
|
||||
|
||||
test('escapes a heading closing sequence', () => {
|
||||
const heading = (text: string): string => markdown(adfToLosslessMarkdown(document({ attrs: { level: 2 }, content: [{ text, type: 'text' }], type: 'heading' })))
|
||||
assert.equal(heading('done #'), '## done \\#\n')
|
||||
|
||||
@@ -35,6 +35,9 @@ type Escapings = (index: number) => InlineEscaping | undefined
|
||||
// Where a segment's first character landed in the output.
|
||||
type PlacedSegment = { segment: InlineSegment; start: number }
|
||||
|
||||
// The escaped line once reading it back asks for no further escape.
|
||||
type ReadBack = { output: string; placed: PlacedSegment[]; reading: InlineReading }
|
||||
|
||||
// Which of a run's characters escape: every escapable one, or every one before `end`.
|
||||
type RunEscapes = { end: number; escapable: false } | { escapable: true }
|
||||
|
||||
@@ -87,6 +90,11 @@ function resolveEmphasis(segments: readonly InlineSegment[]): InlineSegment[] {
|
||||
}
|
||||
|
||||
function escape(segments: readonly InlineSegment[], container: LineContainer, claims: Claims): AssembledLine {
|
||||
const { output, placed, reading } = readBack(segments, container, claims)
|
||||
return { ...guardLinkDefinition(output, placed, container), unspellableRuns: unspellableRuns(placed, reading) }
|
||||
}
|
||||
|
||||
function readBack(segments: readonly InlineSegment[], container: LineContainer, claims: Claims): ReadBack {
|
||||
const scan = segments.map((segment) => segment.text).join('')
|
||||
// One byte a character, never an array element or Set entry: a line may run to hundreds of millions of characters.
|
||||
const codes = new Uint8Array(scan.length)
|
||||
@@ -105,18 +113,26 @@ function escape(segments: readonly InlineSegment[], container: LineContainer, cl
|
||||
for (;;) {
|
||||
const { output, placed } = backslashed(segments, scan, withTextEscapes(scan, escapings, claimed, escapes))
|
||||
const { faulted, reading } = readLineBack(output, container, claims)
|
||||
const opened = faulted ? { backticks: true, brackets: true } : textOpeners(placed, output.length, reading)
|
||||
const shown = textOpeners(placed, output.length, reading)
|
||||
// A code span a directive slot swallowed leaves no record before the directive fault.
|
||||
const opened = faulted && !shown.backticks && !shown.brackets ? { backticks: true, brackets: true } : shown
|
||||
const next = { backticks: escapes.backticks || opened.backticks, brackets: escapes.brackets || opened.brackets }
|
||||
if (next.backticks === escapes.backticks && next.brackets === escapes.brackets) {
|
||||
const unspellable = unspellableRuns(placed, reading)
|
||||
if (container !== 'paragraph' || !opensLinkDefinition(output)) return { line: output, unspellableRuns: unspellable }
|
||||
if (segments[0]?.nodes !== undefined) return { line: output, openingLinkAsDirective: true, unspellableRuns: [] }
|
||||
return { line: `\\${output}`, unspellableRuns: unspellable }
|
||||
}
|
||||
if (next.backticks === escapes.backticks && next.brackets === escapes.brackets) return { output, placed, reading }
|
||||
escapes = next
|
||||
}
|
||||
}
|
||||
|
||||
// spec/flavour.md, Canonical form: a paragraph opening with what reads as a link reference definition escapes its `[`, or spells the emitter's opening link as a directive.
|
||||
function guardLinkDefinition(output: string, placed: readonly PlacedSegment[], container: LineContainer): { line: string; openingLinkAsDirective?: true } {
|
||||
if (container !== 'paragraph' || !opensLinkDefinition(output)) return { line: output }
|
||||
return opensWithLink(placed) ? { line: output, openingLinkAsDirective: true } : { line: `\\${output}` }
|
||||
}
|
||||
|
||||
// Only the emitter's link opener is a `[` segment spelling nodes.
|
||||
function opensWithLink([first]: readonly PlacedSegment[]): boolean {
|
||||
return first?.segment.nodes !== undefined && first.segment.text === '['
|
||||
}
|
||||
|
||||
function withTextEscapes(scan: string, escapings: Escapings, claimed: Uint8Array, { backticks, brackets }: TextEscapes): Uint8Array {
|
||||
if (!backticks && !brackets) return claimed
|
||||
const escaped = claimed.slice()
|
||||
|
||||
@@ -69,7 +69,7 @@ type Piece =
|
||||
| { canClose: boolean; canOpen: boolean; character: string; kind: 'run'; length: number; offset: number }
|
||||
| { closes: boolean; kind: 'highlight'; offset: number; opens: boolean }
|
||||
|
||||
type Run = { canClose: boolean; canOpen: boolean; character: string; index: number; length: number; offset: number }
|
||||
type Run = Extract<Piece, { kind: 'run' }> & { index: number }
|
||||
|
||||
// What the reader took where, by offset into the outermost content, so a writer can check its line reads as it meant.
|
||||
export type InlineReading = {
|
||||
@@ -81,6 +81,8 @@ export type InlineReading = {
|
||||
readonly links: number[]
|
||||
}
|
||||
|
||||
type ReadingEntry = { at: number; closer: number; kind: 'emphasis'; width: number } | { at: number; closer: number; kind: 'highlights' } | { at: number; kind: 'codeSpans' | 'links' }
|
||||
|
||||
// `container` is `undefined` inside a directive's content slot, the emitter's `bracketed`.
|
||||
type Scan = {
|
||||
// CommonMark's bracket stack, innermost last.
|
||||
@@ -98,7 +100,8 @@ type Scan = {
|
||||
path: ConvertErrorPath
|
||||
pending: string
|
||||
pieces: Piece[]
|
||||
reading: InlineReading
|
||||
// Only a read-back takes a reading; parsing records none.
|
||||
reading: InlineReading | undefined
|
||||
source: string
|
||||
spans: NestedSpans
|
||||
targets: TargetMemo
|
||||
@@ -113,30 +116,37 @@ const hreflessLink = 'the link mark spells its href: this one spells none'
|
||||
const noDefinitions: LinkDefinitions = new Map()
|
||||
const linkInLink = 'no link wraps a link: the [content] this one marks already holds one'
|
||||
const unlinkable = 'the text of this link holds only nodes that take no link: write the link around text beside them'
|
||||
export const spellableLink = 'link takes the directive form only where CommonMark cannot spell it: this one it can, as [text](url "title") or <url>'
|
||||
const spellableLink = 'link takes the directive form only where CommonMark cannot spell it: this one it can, as [text](url "title") or <url>'
|
||||
const textBreak: TextBreak = { kind: 'textBreak' }
|
||||
|
||||
// The outermost content: only here does a text break see both its neighbours. Whether an opening link CommonMark spells may take the directive form is the emitter's to answer.
|
||||
export function readInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer, flavour: Flavour): Result<InlineContent & { openingSpellableLink: boolean }> {
|
||||
const scan = freshScan(source, { definitions, path, reading: emptyReading(), targets: targetMemo(source) }, { claims: flavourClaims[flavour], container, inLink: false, offset: 0, spans: noSpans })
|
||||
// The outermost content: only here does a text break see both its neighbours. Whether an opening link CommonMark spells may take the directive form is the emitter's to answer, so the caller passes its answer in.
|
||||
export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer, flavour: Flavour, openingLinkTakesDirective: (nodes: readonly AdfNode[], path: ConvertErrorPath) => Result<boolean>): Result<InlineContent> {
|
||||
const scan = freshScan(source, { definitions, path, reading: undefined, targets: targetMemo(source) }, { claims: flavourClaims[flavour], container, inLink: false, offset: 0, spans: noSpans })
|
||||
const scanned = scanInline(scan)
|
||||
if (!scanned.ok) return scanned
|
||||
const nodes = partText(contentNodes(scanned.value.pieces), scan)
|
||||
if (!nodes.ok) return nodes
|
||||
if (scan.openingSpellableLink) {
|
||||
const takesDirective = openingLinkTakesDirective(nodes.value, path)
|
||||
if (!takesDirective.ok) return takesDirective
|
||||
if (!takesDirective.value) return failure('unsupported-node-shape', spellableLink, path)
|
||||
}
|
||||
const { image } = scanned.value
|
||||
const { openingSpellableLink } = scan
|
||||
return success(image === undefined ? { nodes: nodes.value, openingSpellableLink } : { image, nodes: nodes.value, openingSpellableLink })
|
||||
return success(image === undefined ? { nodes: nodes.value } : { image, nodes: nodes.value })
|
||||
}
|
||||
|
||||
// A line that fails to read still holds what the reader took before the fault.
|
||||
export function readLineBack(line: string, container: LineContainer, claims: Claims): { faulted: boolean; reading: InlineReading } {
|
||||
const reading = emptyReading()
|
||||
const reading: InlineReading = { codeSpans: [], emphasis: new Map(), highlights: new Map(), links: [] }
|
||||
const scan = freshScan(line, { definitions: noDefinitions, path: [], reading, targets: targetMemo(line) }, { claims, container, inLink: false, offset: 0, spans: noSpans })
|
||||
return { faulted: !scanInline(scan).ok, reading }
|
||||
}
|
||||
|
||||
function emptyReading(): InlineReading {
|
||||
return { codeSpans: [], emphasis: new Map(), highlights: new Map(), links: [] }
|
||||
function record({ reading }: Pick<Scan, 'reading'>, entry: ReadingEntry): void {
|
||||
if (reading === undefined) return
|
||||
if (entry.kind === 'emphasis') reading.emphasis.set(entry.at, { closer: entry.closer, width: entry.width })
|
||||
else if (entry.kind === 'highlights') reading.highlights.set(entry.at, entry.closer)
|
||||
else reading[entry.kind].push(entry.at)
|
||||
}
|
||||
|
||||
function freshScan(source: string, shared: SharedScan, own: Pick<Scan, 'claims' | 'container' | 'inLink' | 'offset' | 'spans'>): Scan {
|
||||
@@ -173,7 +183,7 @@ function readToken(scan: Scan, inline: InlineSource, index: number, token: Inlin
|
||||
return closeBracket(scan, index)
|
||||
case 'code-span':
|
||||
flush(scan, false)
|
||||
scan.reading.codeSpans.push(scan.offset + index)
|
||||
record(scan, { at: scan.offset + index, kind: 'codeSpans' })
|
||||
pushNode(scan, { marks: [{ type: 'code' }], text: codeSpanText(scan.source.slice(index + token.opener, end - token.opener)), type: 'text' })
|
||||
return success(end)
|
||||
case 'delimiter-run':
|
||||
@@ -420,7 +430,7 @@ function closeBracket(scan: Scan, index: number): Result<number> {
|
||||
if (!open.image) scan.linkOpenersFrom = scan.brackets.length
|
||||
// Before `linkPieces`' own-marks guard: brackets that go literal mark nothing, so a carry inside them is no error.
|
||||
if (!open.image && holdsLink(inner)) return success(unopened(scan, open, index))
|
||||
scan.reading.links.push(scan.offset + open.start - 1)
|
||||
record(scan, { at: scan.offset + open.start - 1, kind: 'links' })
|
||||
const closed = open.image ? imagePieces(scan, inner, target.definition()) : linkPieces(scan, inner, target.definition())
|
||||
if (!closed.ok) return closed
|
||||
scan.pieces.length = open.index
|
||||
@@ -512,11 +522,11 @@ function markedNodes(pieces: readonly Piece[], scan: Scan, highlights: boolean):
|
||||
const nodes = pieces.map(pieceNodes)
|
||||
const runs = delimiterRuns(pieces)
|
||||
const pairings = matchEmphasis(runs)
|
||||
for (const { closer, closerOffset, opener, openerOffset, used } of pairings) scan.reading.emphasis.set(opener.offset + openerOffset, { closer: closer.offset + closerOffset, width: used })
|
||||
for (const { closer, closerOffset, opener, openerOffset, used } of pairings) record(scan, { at: opener.offset + openerOffset, closer: closer.offset + closerOffset, kind: 'emphasis', width: used })
|
||||
writeUnpaired(nodes, runs, pairings)
|
||||
if (!markPairings(pieces, nodes, pairings)) return failure('unsupported-node-shape', ownMarksInMark, scan.path)
|
||||
const paired = highlights ? pairedHighlights(pieces, nodes) : []
|
||||
for (const { closerOffset, openerOffset } of paired) scan.reading.highlights.set(openerOffset, closerOffset)
|
||||
for (const { closerOffset, openerOffset } of paired) record(scan, { at: openerOffset, closer: closerOffset, kind: 'highlights' })
|
||||
markHighlights(pieces, nodes, paired)
|
||||
return success(nodes)
|
||||
}
|
||||
@@ -563,7 +573,7 @@ function pieceNodes(piece: Piece): Inline[] {
|
||||
function delimiterRuns(pieces: readonly Piece[]): Run[] {
|
||||
const runs: Run[] = []
|
||||
for (const [index, piece] of pieces.entries()) {
|
||||
if (piece.kind === 'run') runs.push({ canClose: piece.canClose, canOpen: piece.canOpen, character: piece.character, index, length: piece.length, offset: piece.offset })
|
||||
if (piece.kind === 'run') runs.push({ ...piece, index })
|
||||
}
|
||||
return runs
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@ import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
||||
import type { Block, DirectiveBlock } from './blocks.ts'
|
||||
import type { BlockDirectiveNode } from '../directive/node-reading.ts'
|
||||
import type { ConvertFault } from '../../result.ts'
|
||||
import type { Flavour } from '../flavour.ts'
|
||||
import type { InlineContent, LinkDefinitions } from '../inline-content.ts'
|
||||
import type { LineContainer } from '../line-container.ts'
|
||||
import type { LinkDefinitions } from '../inline-content.ts'
|
||||
import { carryFenceType, readCarriedBlock } from '../opaque-carry.ts'
|
||||
import { commonMarkSpelling, openingLinkTakesDirective, type SpellingMemo } from '../emit/spelling-questions.ts'
|
||||
import { documentAttribute, documentName, documentSpelling } from '../directive/block.ts'
|
||||
@@ -15,7 +14,7 @@ import { languageSlot } from '../code-language.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { notAString } from '../../not-a-string.ts'
|
||||
import { parseBlocks } from './blocks.ts'
|
||||
import { readInlineContent, spellableLink } from '../inline-content.ts'
|
||||
import { parseInlineContent } from '../inline-content.ts'
|
||||
import { readBlockDirectiveNode } from '../directive/node-reading.ts'
|
||||
import { spellStringAttribute, unsupportedNodeShape } from '../directive/syntax.ts'
|
||||
import { spellsEmpty } from '../directive/empty-keys.ts'
|
||||
@@ -247,26 +246,14 @@ function codeBlockNode(language: string, text: string, reading: Reading, path: C
|
||||
|
||||
// spec/flavour.md, The CommonMark image: only a `paragraph` block gives a lone image the block it needs.
|
||||
function paragraphNode(text: string, reading: Reading, path: ConvertErrorPath): Result<AdfNode> {
|
||||
const content = parseInlineContent(text, reading.definitions, path, 'paragraph', reading.flavour)
|
||||
const content = parseInlineContent(text, reading.definitions, path, 'paragraph', reading.flavour, openingLinkTakesDirective)
|
||||
if (!content.ok) return content
|
||||
const image = content.value.image
|
||||
return success(image === undefined ? withContent({ type: 'paragraph' }, content.value.nodes) : image)
|
||||
}
|
||||
|
||||
export function contentNode(node: AdfNode, text: string, reading: Reading, path: ConvertErrorPath, container: LineContainer): Result<AdfNode> {
|
||||
const content = parseInlineContent(text, reading.definitions, path, container, reading.flavour)
|
||||
const content = parseInlineContent(text, reading.definitions, path, container, reading.flavour, openingLinkTakesDirective)
|
||||
if (!content.ok) return content
|
||||
return success(withContent(node, content.value.nodes))
|
||||
}
|
||||
|
||||
export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer, flavour: Flavour): Result<InlineContent> {
|
||||
const read = readInlineContent(source, definitions, path, container, flavour)
|
||||
if (!read.ok) return read
|
||||
const { image, nodes, openingSpellableLink } = read.value
|
||||
if (openingSpellableLink) {
|
||||
const takesDirective = openingLinkTakesDirective(nodes, path)
|
||||
if (!takesDirective.ok) return takesDirective
|
||||
if (!takesDirective.value) return failure('unsupported-node-shape', spellableLink, path)
|
||||
}
|
||||
return success(image === undefined ? { nodes } : { image, nodes })
|
||||
}
|
||||
|
||||
@@ -2,10 +2,12 @@ import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
||||
import type { Block } from '../parse/blocks.ts'
|
||||
import type { ConvertError, ConvertErrorPath, ParseError, Result, SourcePosition } from '../../result.ts'
|
||||
import type { FlavourBlockReaders, FlavourReading } from '../parse/markdown-to-adf.ts'
|
||||
import { containerNode, contentNode, listNode, parseInlineContent, readBlocks, readDocument, tooDeep } from '../parse/markdown-to-adf.ts'
|
||||
import { containerNode, contentNode, listNode, readBlocks, readDocument, tooDeep } from '../parse/markdown-to-adf.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { leadingMarker, readAlertMarker, readTaskMarker } from './conventions.ts'
|
||||
import { mintTaskIds } from './task-ids.ts'
|
||||
import { openingLinkTakesDirective } from '../emit/spelling-questions.ts'
|
||||
import { parseInlineContent } from '../inline-content.ts'
|
||||
import { pathTo, splitTaskList } from './task-list.ts'
|
||||
import { positioned, success } from '../../result.ts'
|
||||
import { withinEngineLimits } from '../../length-bound.ts'
|
||||
@@ -55,7 +57,7 @@ function quoteReader(foldedType: 'expand' | 'nestedExpand'): FlavourBlockReaders
|
||||
const { folded, panelType } = led.marker
|
||||
const { line, rest } = markerLine(led.text)
|
||||
if (!folded) return filledNode({ attrs: { panelType }, type: 'panel' }, readBlocks([...paragraphsOf(led.position, line, rest), ...body], reading, path, depth + 1))
|
||||
const title = parseInlineContent(line, reading.definitions, path, 'paragraph', reading.flavour)
|
||||
const title = parseInlineContent(line, reading.definitions, path, 'paragraph', reading.flavour, openingLinkTakesDirective)
|
||||
if (!title.ok) return title
|
||||
const text = titleText(title.value.nodes)
|
||||
return filledNode(text === '' ? { type: foldedType } : { attrs: { title: text }, type: foldedType }, readBlocks([...paragraphsOf(led.position, rest), ...body], { ...reading, blocks: expandBlockReaders }, path, depth + 1))
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
`Bar = 0.1`
|
||||
|
||||
`Next ID = 153`
|
||||
`Next ID = 156`
|
||||
|
||||
## Items
|
||||
|
||||
@@ -24,8 +24,11 @@
|
||||
| 132 | 0.3.0 | | | **Write tables as `<table>` in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.** | 2 | 2 | 2 | 4 | 5 | 6 by 4 | 2.4 |
|
||||
| 73 | 0.3.0 | 33, 91, 109, 115, 132, 133, 145, 146, 150, 152 | | **Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.** | 1 | 2 | 2 | 3 | 2 | 3 by 8 | 16.9 |
|
||||
| 72 | 0.3.0 | 33, 91, 109, 115, 132, 133, 145, 146, 150, 152 | decision | **Re-create `gitea.larvit.se/larvit/adf-codec` as a read-only pull mirror of GitHub, its issues and Actions off and its description pointing at GitHub.** | 1 | 2 | 1 | 2 | 1 | 3 by 1 | 2.5 |
|
||||
| 153 | 0.4.0 | | question | **Decide whether `src/markdown/` root may import `emit/`, `parse/` or `portable/`, and hold the answer in `.oxlintrc.json` and §The source parts by ADF and format.** | 1 | 2 | 2 | 3 | 2 | code health by 3 | 26.2 |
|
||||
| 128 | 0.4.0 | | | **Pin the portable flavour's block reading and writing with tests: callout titles past one line, a link beside other marks in a title, the nesting limit of callouts and task lists, refusals inside tasks and their paths, and empty or non-paragraph tasks.** | 2 | 1 | 1 | 2 | 3 | code health by 3 | 19.6 |
|
||||
| 75 | 0.4.0 | | | **Make the generated-markdown property write block carries as `adf:` fences, canonical and hostile.** | 1 | 1 | 1 | 1 | 2 | code health by 2 | 17.4 |
|
||||
| 154 | 0.4.0 | | | **Pair highlights over the inline reader's pieces by their offsets before building nodes, so no placeholder text node stands in for a delimiter.** | 2 | 1 | 2 | 3 | 4 | code health by 4 | 17.4 |
|
||||
| 155 | 0.4.0 | | | **Have `closeBracket` return the link, image or literal bracket it builds, and apply it to the scan in one caller.** | 2 | 1 | 2 | 3 | 4 | code health by 4 | 17.4 |
|
||||
| 45 | 0.4.0 | | defect | **Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.** | 2 | 1 | 3 | 4 | 3 | 1 by 2 | 13.1 |
|
||||
| 68 | 0.4.0 | | defect | **Carry a non-text inline node holding `content` or `text`, a block holding `text`, and a leaf block holding `content`.** | 2 | 1 | 2 | 3 | 3 | 1 by 2 | 13.1 |
|
||||
| 127 | 0.4.0 | | principle | **Hold what `entity-references.test.ts`, `json-text.test.ts` and `result.test.ts` check by asserting outcomes, so none reads source text.** | 2 | 2 | 1 | 3 | 3 | code health by 2 | 13.1 |
|
||||
@@ -186,6 +189,14 @@ Gitea copy holds no issues or releases to move. Actions stay off on the mirror:
|
||||
otherwise run there. When this lands, the `docs/decisions.md` entry "GitHub is canonical" loses its
|
||||
interim sentence.
|
||||
|
||||
### 153. Decide whether `src/markdown/` root may import `emit/`, `parse/` or `portable/`, and hold the answer in `.oxlintrc.json` and §The source parts by ADF and format.
|
||||
|
||||
The writers read each escaped line back through `src/markdown/inline-content.ts`'s scan. A writer
|
||||
question put back on that scan's path, such as the opening-link consult it takes as an argument
|
||||
today, would close the cycle writer → reader → writer and recurse without bound on a paragraph
|
||||
opening with a spellable `!adf:link` (§Nothing recurses unbounded). No `no-restricted-imports`
|
||||
override covers `src/markdown/*.ts`; adding one, with its `ci.sh` probe, extends the decision.
|
||||
|
||||
### 128. Pin the portable flavour's block reading and writing with tests: callout titles past one line, a link beside other marks in a title, the nesting limit of callouts and task lists, refusals inside tasks and their paths, and empty or non-paragraph tasks.
|
||||
|
||||
Mutation testing left about 40 mutants alive in code moved verbatim into `src/markdown/portable/`
|
||||
@@ -202,6 +213,19 @@ type", that is an ordinary code language, so the property reaches `readCarriedBl
|
||||
where a random edit lands in a canonical document. Write `adf:<type>` and `adf:` fences, canonical
|
||||
and hostile. Technical principle: for pure logic over many inputs, prefer a property test, so the property must reach the fenced carry's refusals itself. Found by the technical-principles audit, 2026-10-04.
|
||||
|
||||
### 154. Pair highlights over the inline reader's pieces by their offsets before building nodes, so no placeholder text node stands in for a delimiter.
|
||||
|
||||
A comprehension seat found `src/markdown/inline-content.ts` `resolveNodes` / `pairedHighlights`
|
||||
hard: an empty text node stands in for each `==` until pairing, then a cursor walk overwrites it.
|
||||
The pieces now carry their offsets, so the pairing can run on them first. Make that place easier to
|
||||
read.
|
||||
|
||||
### 155. Have `closeBracket` return the link, image or literal bracket it builds, and apply it to the scan in one caller.
|
||||
|
||||
A comprehension seat found `src/markdown/inline-content.ts` `closeBracket` / `truncatePieces`
|
||||
hard: `closeBracket`, `unopened`, `literalClose` and `popBracket` each write `scan.pieces`, the
|
||||
link openers or the reading. Make that place easier to read.
|
||||
|
||||
### 45. Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.
|
||||
|
||||
Lands with item 77: the reader returns item 77's copy. Goal 1 has every call return a result; the
|
||||
|
||||
Reference in New Issue
Block a user