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

Ask the writer about an opening link in parse/ alone, and ban the markdown root from importing emit/, parse/ and portable/

This commit is contained in:
2026-11-10 21:20:47 +01:00
committed by Lillem4n
parent b3b5b66c0c
commit bffeb1f7d6
9 changed files with 88 additions and 44 deletions
+7 -1
View File
@@ -27,6 +27,12 @@
"no-restricted-imports": ["error", { "patterns": [{ "group": ["**/html/**"], "message": "docs/decisions.md §The source parts by ADF and format" }] }] "no-restricted-imports": ["error", { "patterns": [{ "group": ["**/html/**"], "message": "docs/decisions.md §The source parts by ADF and format" }] }]
} }
}, },
{
"files": ["src/markdown/*.ts"],
"rules": {
"no-restricted-imports": ["error", { "patterns": [{ "group": ["**/emit/**", "**/html/**", "**/parse/**", "**/portable/**"], "message": "A root file of src/markdown/ is read by the writers and the readers and imports neither: docs/decisions.md §The source parts by ADF and format" }] }]
}
},
{ {
"files": ["src/markdown/directive/**"], "files": ["src/markdown/directive/**"],
"rules": { "rules": {
@@ -36,7 +42,7 @@
{ {
"files": ["src/markdown/emit/**"], "files": ["src/markdown/emit/**"],
"rules": { "rules": {
"no-restricted-imports": ["error", { "patterns": [{ "group": ["**/html/**", "**/portable/**"], "message": "docs/decisions.md §The source parts by ADF and format" }] }] "no-restricted-imports": ["error", { "patterns": [{ "group": ["**/html/**", "**/parse/**", "**/portable/**"], "message": "docs/decisions.md §The source parts by ADF and format" }] }]
} }
}, },
{ {
+4
View File
@@ -13,12 +13,16 @@ src/adf/boundary-probe-html.ts ../html/emit/adf-to-html.ts
src/html/boundary-probe.ts ../markdown/flavour.ts src/html/boundary-probe.ts ../markdown/flavour.ts
src/html/parse/boundary-probe.ts ../emit/adf-to-html.ts src/html/parse/boundary-probe.ts ../emit/adf-to-html.ts
src/html/parse/boundary-probe-markdown.ts ../../markdown/flavour.ts src/html/parse/boundary-probe-markdown.ts ../../markdown/flavour.ts
src/markdown/boundary-probe-emit.ts ./emit/adf-to-markdown.ts
src/markdown/boundary-probe-html.ts ../html/emit/adf-to-html.ts src/markdown/boundary-probe-html.ts ../html/emit/adf-to-html.ts
src/markdown/boundary-probe-parse.ts ./parse/blocks.ts
src/markdown/boundary-probe-portable.ts ./portable/conventions.ts
src/markdown/directive/boundary-probe.ts ../parse/blocks.ts src/markdown/directive/boundary-probe.ts ../parse/blocks.ts
src/markdown/directive/boundary-probe-emit.ts ../emit/image.ts src/markdown/directive/boundary-probe-emit.ts ../emit/image.ts
src/markdown/directive/boundary-probe-html.ts ../../html/emit/adf-to-html.ts src/markdown/directive/boundary-probe-html.ts ../../html/emit/adf-to-html.ts
src/markdown/emit/boundary-probe.ts ../portable/conventions.ts src/markdown/emit/boundary-probe.ts ../portable/conventions.ts
src/markdown/emit/boundary-probe-html.ts ../../html/emit/adf-to-html.ts src/markdown/emit/boundary-probe-html.ts ../../html/emit/adf-to-html.ts
src/markdown/emit/boundary-probe-parse.ts ../parse/blocks.ts
src/markdown/parse/boundary-probe.ts ../emit/adf-to-markdown.ts src/markdown/parse/boundary-probe.ts ../emit/adf-to-markdown.ts
src/markdown/parse/boundary-probe-html.ts ../../html/emit/adf-to-html.ts src/markdown/parse/boundary-probe-html.ts ../../html/emit/adf-to-html.ts
src/markdown/parse/boundary-probe-portable.ts ../portable/conventions.ts" src/markdown/parse/boundary-probe-portable.ts ../portable/conventions.ts"
+9 -3
View File
@@ -1175,7 +1175,7 @@ is the markdown flavour's choice, not ADF's.
2026-08-27, placement 2026-09-18, `adf/`'s bar 2026-09-21, `plain/` 2026-10-03, the import 2026-08-27, placement 2026-09-18, `adf/`'s bar 2026-09-21, `plain/` 2026-10-03, the import
boundaries' lint 2026-10-22, the maintainer; `parse/` and `emit/` free of `portable/`, boundaries' lint 2026-10-22, the maintainer; `parse/` and `emit/` free of `portable/`,
2026-10-22. Goal 2. Valid while each format has a reader and a writer through ADF. 2026-10-22; the root's imports and `emit/` free of `parse/`, 2026-11-10. Goal 2. Valid while each format has a reader and a writer through ADF.
`src/adf/` holds ADF's own knowledge, imports no format, and is where a construct both formats read `src/adf/` holds ADF's own knowledge, imports no format, and is where a construct both formats read
lives: the question is answered in ADF's vocabulary — a node type, an attribute kind, a content lives: the question is answered in ADF's vocabulary — a node type, an attribute kind, a content
@@ -1202,10 +1202,16 @@ whether a list marker interrupts a paragraph — is one function there too, neve
direction, however conservative the copy would be. Where the rule is the emitter's own choice, input direction, however conservative the copy would be. Where the rule is the emitter's own choice, input
consults it rather than restating it, so no fixture the emitter writes can be refused, and a consults it rather than restating it, so no fixture the emitter writes can be refused, and a
spelling the emitter refuses gives its own error rather than a second name for it. That consulting spelling the emitter refuses gives its own error rather than a second name for it. That consulting
is the only import `parse/` takes from `emit/`, through `emit/spelling-questions.ts`. is the only import `parse/` takes from `emit/`, through `emit/spelling-questions.ts`. The root
imports neither `emit/`, `parse/` nor `portable/`, and `emit/` does not import `parse/`. The scan in
`inline-content.ts` reports whether a spellable link opens the paragraph, and
`parse/inline-content.ts` alone asks the writer what that link needs. A read-back (`readLineBack`)
never asks the writer: the writer calls it as it spells a line, so a read-back that asked would
recurse without bound (§Nothing recurses unbounded).
`.oxlintrc.json` enforces that limit for `markdown/parse/`, keeps `html/parse/` from importing `.oxlintrc.json` enforces that limit for `markdown/parse/`, keeps `html/parse/` from importing
`html/emit/`, keeps markdown's `parse/` and `emit/` from importing `portable/`, keeps `html/emit/`, keeps markdown's `parse/` and `emit/` from importing `portable/`, keeps the root's files from
importing `emit/`, `parse/` and `portable/`, keeps `emit/` from importing `parse/`, keeps
`markdown/directive/` from importing `emit/` or `parse/`, keeps `markdown/` and `html/` from `markdown/directive/` from importing `emit/` or `parse/`, keeps `markdown/` and `html/` from
importing each other, and keeps `adf/` from importing either, as `no-restricted-imports` overrides. importing each other, and keeps `adf/` from importing either, as `no-restricted-imports` overrides.
A later override replaces an earlier one's rule, so each subdirectory's override repeats its A later override replaces an earlier one's rule, so each subdirectory's override repeats its
+35
View File
@@ -0,0 +1,35 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import type { AdfNode } from '../adf/document.ts'
import type { Result } from '../result.ts'
import type { InlineContent } from './inline-content.ts'
import { parseInlineContent } from './inline-content.ts'
function read(source: string): Result<InlineContent & { openingSpellableLink: boolean }> {
return parseInlineContent(source, new Map(), [], 'paragraph', 'lossless')
}
function nodes(result: Result<InlineContent>): AdfNode[] | string {
return result.ok ? result.value.nodes : result.error.code
}
test('reports a directive link opening the line that CommonMark could spell', () => {
const opening = read('!adf:link[a]{href="/u"}')
assert.equal(opening.ok && opening.value.openingSpellableLink, true)
assert.deepEqual(nodes(opening), [{ marks: [{ attrs: { href: '/u' }, type: 'link' }], text: 'a', type: 'text' }])
})
test('reports no spellable opening link where the line opens with text', () => {
const text = read('See a')
assert.equal(text.ok && text.value.openingSpellableLink, false)
})
test('reports the opening link whose CommonMark spelling would open a link definition, leaving the writer to answer', () => {
const definition = read('!adf:link[`]: a`]{href="/u"}')
assert.equal(definition.ok && definition.value.openingSpellableLink, true)
})
test('still refuses a spellable directive link past the opening of the line', () => {
assert.equal(nodes(read('See !adf:link[a]{href="/u"}.')), 'unsupported-node-shape')
})
+10 -11
View File
@@ -26,6 +26,9 @@ import { readTextDirective } from './directive/text.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. // 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.
export type InlineContent = { image?: AdfNode; nodes: AdfNode[] } export type InlineContent = { image?: AdfNode; nodes: AdfNode[] }
// `openingSpellableLink`: a directive link opens the paragraph that CommonMark could spell; `parse/` asks the writer whether the line still needs the directive.
export type OutermostContent = InlineContent & { openingSpellableLink: boolean }
// The text break leaf: it holds no marks, and stands between the nodes it parts until the outermost content checks and drops it. // The text break leaf: it holds no marks, and stands between the nodes it parts until the outermost content checks and drops it.
type TextBreak = { kind: 'textBreak' } type TextBreak = { kind: 'textBreak' }
@@ -114,26 +117,22 @@ const hreflessLink = 'the link mark spells its href: this one spells none'
const linkInLink = 'no link wraps a link: the [content] this one marks already holds one' const linkInLink = 'no link wraps a link: the [content] this one marks already holds one'
const noDefinitions: LinkDefinitions = new Map() const noDefinitions: LinkDefinitions = new Map()
const unlinkable = 'the text of this link holds only nodes that take no link: write the link around text beside them' const unlinkable = 'the text of this link holds only nodes that take no link: write the link around text beside them'
const spellableLink = 'link takes the directive form only where CommonMark cannot spell it: this one it can, as [text](url "title") or <url>' 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 textBreak: TextBreak = { kind: 'textBreak' } const textBreak: TextBreak = { kind: 'textBreak' }
// The outermost content: only here does a text break see both its neighbours. The writers read lines back through this file, so it takes the emitter's `openingLinkTakesDirective` as an argument and imports nothing from `emit/`. // The outermost content: only here does a text break see both its neighbours.
export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer, flavour: Flavour, openingLinkTakesDirective: (nodes: readonly AdfNode[], path: ConvertErrorPath) => Result<boolean>): Result<InlineContent> { export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer, flavour: Flavour): Result<OutermostContent> {
const scan = freshScan(source, { definitions, path, reading: undefined, targets: targetMemo(source) }, { claims: flavourClaims[flavour], container, inLink: false, offset: 0, spans: noSpans }) 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) const scanned = scanInline(scan)
if (!scanned.ok) return scanned if (!scanned.ok) return scanned
const nodes = partText(contentNodes(scanned.value.pieces), scan) const nodes = partText(contentNodes(scanned.value.pieces), scan)
if (!nodes.ok) return nodes 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 { image } = scanned.value
return success(image === undefined ? { nodes: nodes.value } : { image, nodes: nodes.value }) const { openingSpellableLink } = scan
return success(image === undefined ? { nodes: nodes.value, openingSpellableLink } : { image, nodes: nodes.value, openingSpellableLink })
} }
// The reading of a line that faults still holds what the reader took before the fault. // A read-back never asks the writer for anything: the writer calls it while it spells a line. The reading of a line that faults still holds what the reader took before the fault.
export function readLineBack(line: string, container: LineContainer, claims: Claims): { faulted: boolean; reading: InlineReading } { export function readLineBack(line: string, container: LineContainer, claims: Claims): { faulted: boolean; reading: InlineReading } {
const reading: InlineReading = { codeSpans: [], emphasis: new Map(), links: [] } const reading: InlineReading = { codeSpans: [], emphasis: new Map(), links: [] }
const scan = freshScan(line, { definitions: noDefinitions, path: [], reading, targets: targetMemo(line) }, { claims, container, inLink: false, offset: 0, spans: noSpans }) const scan = freshScan(line, { definitions: noDefinitions, path: [], reading, targets: targetMemo(line) }, { claims, container, inLink: false, offset: 0, spans: noSpans })
@@ -272,7 +271,7 @@ function directiveMarkPieces(scan: Scan, name: string, mark: AdfMark, slot: read
return success([...marked.filter((piece) => piece.kind === 'nodes'), ...besideLink(images, link)]) return success([...marked.filter((piece) => piece.kind === 'nodes'), ...besideLink(images, link)])
} }
// spec/flavour.md, Marks. A link opening a paragraph may still need the directive form for the line it opens, which `parseInlineContent` asks the emitter. // spec/flavour.md, Marks. A link opening a paragraph may still need the directive form for the line it opens, which `parse/` asks the writer once `parseInlineContent` reports the opening.
function refuseLinkDirective(scan: Scan, mark: AdfMark, nodes: readonly Inline[], index: number): Result<Piece[]> | undefined { function refuseLinkDirective(scan: Scan, mark: AdfMark, nodes: readonly Inline[], index: number): Result<Piece[]> | undefined {
const href = linkHref(nodeAttrs(mark)) const href = linkHref(nodeAttrs(mark))
if (href === undefined) return failure('unsupported-node-shape', hreflessLink, scan.shared.path) if (href === undefined) return failure('unsupported-node-shape', hreflessLink, scan.shared.path)
+17
View File
@@ -0,0 +1,17 @@
import type { ConvertErrorPath, Result } 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 { failure, success } from '../../result.ts'
import { openingLinkTakesDirective } from '../emit/spelling-questions.ts'
import { parseInlineContent, spellableLink } from '../inline-content.ts'
// The only place a reader asks the writer: whether a spellable link opening the paragraph still needs its directive for the line it opens (spec/flavour.md, Marks).
export function readInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer, flavour: Flavour): Result<InlineContent> {
const content = parseInlineContent(source, definitions, path, container, flavour)
if (!content.ok || !content.value.openingSpellableLink) return content
const takesDirective = openingLinkTakesDirective(content.value.nodes, path)
if (!takesDirective.ok) return takesDirective
if (!takesDirective.value) return failure('unsupported-node-shape', spellableLink, path)
return success(content.value)
}
+4 -4
View File
@@ -5,7 +5,7 @@ import type { ConvertFault } from '../../result.ts'
import type { LineContainer } from '../line-container.ts' import type { LineContainer } from '../line-container.ts'
import type { LinkDefinitions } from '../inline-content.ts' import type { LinkDefinitions } from '../inline-content.ts'
import { carryFenceType, readCarriedBlock } from '../opaque-carry.ts' import { carryFenceType, readCarriedBlock } from '../opaque-carry.ts'
import { commonMarkSpelling, openingLinkTakesDirective, spellingMemo, type SpellingMemo } from '../emit/spelling-questions.ts' import { commonMarkSpelling, spellingMemo, type SpellingMemo } from '../emit/spelling-questions.ts'
import { documentAttribute, documentName, documentSpelling } from '../directive/block.ts' import { documentAttribute, documentName, documentSpelling } from '../directive/block.ts'
import { emptyKeys, nodeAttrs } from '../../adf/document.ts' import { emptyKeys, nodeAttrs } from '../../adf/document.ts'
import { failure, faulted, positioned, success, type ConvertErrorPath, type ParseError, type Result } from '../../result.ts' import { failure, faulted, positioned, success, type ConvertErrorPath, type ParseError, type Result } from '../../result.ts'
@@ -14,8 +14,8 @@ import { languageSlot } from '../code-language.ts'
import { largestNesting } from '../../nesting.ts' import { largestNesting } from '../../nesting.ts'
import { notAString } from '../../not-a-string.ts' import { notAString } from '../../not-a-string.ts'
import { parseBlocks } from './blocks.ts' import { parseBlocks } from './blocks.ts'
import { parseInlineContent } from '../inline-content.ts'
import { readBlockDirectiveNode } from '../directive/node-reading.ts' import { readBlockDirectiveNode } from '../directive/node-reading.ts'
import { readInlineContent } from './inline-content.ts'
import { spellStringAttribute, unsupportedNodeShape } from '../directive/syntax.ts' import { spellStringAttribute, unsupportedNodeShape } from '../directive/syntax.ts'
import { spellsEmpty } from '../directive/empty-keys.ts' import { spellsEmpty } from '../directive/empty-keys.ts'
import { withinEngineLimits } from '../../length-bound.ts' import { withinEngineLimits } from '../../length-bound.ts'
@@ -246,14 +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. // 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> { function paragraphNode(text: string, reading: Reading, path: ConvertErrorPath): Result<AdfNode> {
const content = parseInlineContent(text, reading.definitions, path, 'paragraph', reading.flavour, openingLinkTakesDirective) const content = readInlineContent(text, reading.definitions, path, 'paragraph', reading.flavour)
if (!content.ok) return content if (!content.ok) return content
const image = content.value.image const image = content.value.image
return success(image === undefined ? withContent({ type: 'paragraph' }, content.value.nodes) : 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> { 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, openingLinkTakesDirective) const content = readInlineContent(text, reading.definitions, path, container, reading.flavour)
if (!content.ok) return content if (!content.ok) return content
return success(withContent(node, content.value.nodes)) return success(withContent(node, content.value.nodes))
} }
@@ -7,10 +7,9 @@ import { containerNode, contentNode, listNode, readBlocks, readDocument, tooDeep
import { largestNesting } from '../../nesting.ts' import { largestNesting } from '../../nesting.ts'
import { leadingMarker, readAlertMarker, readTaskMarker } from './conventions.ts' import { leadingMarker, readAlertMarker, readTaskMarker } from './conventions.ts'
import { mintTaskIds } from './task-ids.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 { pathTo, splitTaskList } from './task-list.ts'
import { positioned, success } from '../../result.ts' import { positioned, success } from '../../result.ts'
import { readInlineContent } from '../parse/inline-content.ts'
import { withinEngineLimits } from '../../length-bound.ts' import { withinEngineLimits } from '../../length-bound.ts'
type Paragraph = Extract<Block, { kind: 'paragraph' }> type Paragraph = Extract<Block, { kind: 'paragraph' }>
@@ -58,7 +57,7 @@ function quoteReader(foldedType: 'expand' | 'nestedExpand'): FlavourBlockReaders
const { folded, panelType } = led.marker const { folded, panelType } = led.marker
const { line, rest } = markerLine(led.text) 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)) 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, openingLinkTakesDirective) const title = readInlineContent(line, reading.definitions, path, 'paragraph', reading.flavour)
if (!title.ok) return title if (!title.ok) return title
const text = titleText(title.value.nodes) 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)) return filledNode(text === '' ? { type: foldedType } : { attrs: { title: text }, type: foldedType }, readBlocks([...paragraphsOf(led.position, rest), ...body], { ...reading, blocks: expandBlockReaders }, path, depth + 1))
-22
View File
@@ -12,7 +12,6 @@
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score | | ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|---|---|---|---|---|---|---|---|---|---|---|---| |---|---|---|---|---|---|---|---|---|---|---|---|
| 153 | 0.4.0 | | | **Return from `parseInlineContent` whether a link opens the paragraph in a spellable form, ask the writer only in `parse/`, and ban `src/markdown/` root files from importing `emit/`, `parse/` and `portable/`, and `emit/` from importing `parse/`.** | 2 | 1 | 2 | 3 | 4 | code health by 3 | 13.1 |
| 80 | 0.4.0 | | | **Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document.** | 2 | 1 | 2 | 3 | 3 | 6 by 4 | 6.0 | | 80 | 0.4.0 | | | **Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document.** | 2 | 1 | 2 | 3 | 3 | 6 by 4 | 6.0 |
| 171 | 0.4.0 | | principle | **Assert the facts an error message must carry, never its whole sentence, in `markdown-to-adf.test.ts`, `document.test.ts` and `html-to-adf.test.ts`.** | 3 | 1 | 1 | 3 | 6 | code health by 3 | 4.4 | | 171 | 0.4.0 | | principle | **Assert the facts an error message must carry, never its whole sentence, in `markdown-to-adf.test.ts`, `document.test.ts` and `html-to-adf.test.ts`.** | 3 | 1 | 1 | 3 | 6 | code health by 3 | 4.4 |
| 173 | 0.4.0 | | principle | **Move each CommonMark spelling's description out of `commonMarkSpellings`, beside the emitter's spelling it describes.** | 2 | 1 | 1 | 2 | 4 | code health by 1 | 4.4 | | 173 | 0.4.0 | | principle | **Move each CommonMark spelling's description out of `commonMarkSpellings`, beside the emitter's spelling it describes.** | 2 | 1 | 1 | 2 | 4 | code health by 1 | 4.4 |
@@ -52,27 +51,6 @@
## Details ## Details
### 153. Return from `parseInlineContent` whether a link opens the paragraph in a spellable form, ask the writer only in `parse/`, and ban `src/markdown/` root files from importing `emit/`, `parse/` and `portable/`, and `emit/` from importing `parse/`.
Today `parseInlineContent` (`src/markdown/inline-content.ts`) takes `openingLinkTakesDirective`, the
writer's answer, as an argument, and all three callers (`parse/markdown-to-adf.ts`,
`portable/portable-markdown-to-adf.ts`) pass the same function. The writers read lines back through
`readLineBack`, which never asks the writer, so nothing recurses today. A root file importing the
writer would close a module cycle, and a read-back honouring `openingSpellableLink` would recurse
without bound on a paragraph opening with a spellable `!adf:link` (§Nothing recurses unbounded).
- `parseInlineContent` drops the argument and returns `openingSpellableLink` beside the nodes and
the image. One function in `parse/` runs the scan, asks `emit/spelling-questions.ts` and refuses
with `spellableLink`; both readers call it, so `portable/` drops its `emit/` import.
- `.oxlintrc.json`: an override for `src/markdown/*.ts` bans `emit/`, `parse/`, `portable/` and
`html/`, since a later override replaces an earlier one's rule; `emit/`'s override also bans
`parse/`. `ci.sh` probes both. §The source parts by ADF and format names the root's two bans.
- A comment on `readLineBack` says a read-back never asks the writer, and the false comment at
`inline-content.ts:120` goes. A round-trip test covers a paragraph opening with a spellable
`!adf:link`.
- The reader panel's smaller findings, not in this item: `commonmark/` and root names such as
`adjacent-text.ts` do not say which side they serve.
### 80. Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document. ### 80. Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document.
Today `readAdfDocument` and the converters refuse every node, such as one with a `Date` in an Today `readAdfDocument` and the converters refuse every node, such as one with a `Date` in an