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:
+7
-1
@@ -27,6 +27,12 @@
|
||||
"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/**"],
|
||||
"rules": {
|
||||
@@ -36,7 +42,7 @@
|
||||
{
|
||||
"files": ["src/markdown/emit/**"],
|
||||
"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" }] }]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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/parse/boundary-probe.ts ../emit/adf-to-html.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-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-emit.ts ../emit/image.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-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-html.ts ../../html/emit/adf-to-html.ts
|
||||
src/markdown/parse/boundary-probe-portable.ts ../portable/conventions.ts"
|
||||
|
||||
+9
-3
@@ -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
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
`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
|
||||
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
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
@@ -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.
|
||||
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.
|
||||
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 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 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' }
|
||||
|
||||
// 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/`.
|
||||
export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer, flavour: Flavour, openingLinkTakesDirective: (nodes: readonly AdfNode[], path: ConvertErrorPath) => Result<boolean>): Result<InlineContent> {
|
||||
// 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): Result<OutermostContent> {
|
||||
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
|
||||
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 } {
|
||||
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 })
|
||||
@@ -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)])
|
||||
}
|
||||
|
||||
// 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 {
|
||||
const href = linkHref(nodeAttrs(mark))
|
||||
if (href === undefined) return failure('unsupported-node-shape', hreflessLink, scan.shared.path)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import type { ConvertFault } from '../../result.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, 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 { emptyKeys, nodeAttrs } from '../../adf/document.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 { notAString } from '../../not-a-string.ts'
|
||||
import { parseBlocks } from './blocks.ts'
|
||||
import { parseInlineContent } from '../inline-content.ts'
|
||||
import { readBlockDirectiveNode } from '../directive/node-reading.ts'
|
||||
import { readInlineContent } from './inline-content.ts'
|
||||
import { spellStringAttribute, unsupportedNodeShape } from '../directive/syntax.ts'
|
||||
import { spellsEmpty } from '../directive/empty-keys.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.
|
||||
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
|
||||
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, openingLinkTakesDirective)
|
||||
const content = readInlineContent(text, reading.definitions, path, container, reading.flavour)
|
||||
if (!content.ok) return content
|
||||
return success(withContent(node, content.value.nodes))
|
||||
}
|
||||
|
||||
@@ -7,10 +7,9 @@ import { containerNode, contentNode, listNode, readBlocks, readDocument, tooDeep
|
||||
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 { readInlineContent } from '../parse/inline-content.ts'
|
||||
import { withinEngineLimits } from '../../length-bound.ts'
|
||||
|
||||
type Paragraph = Extract<Block, { kind: 'paragraph' }>
|
||||
@@ -58,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, openingLinkTakesDirective)
|
||||
const title = readInlineContent(line, reading.definitions, path, 'paragraph', reading.flavour)
|
||||
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))
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
| 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 |
|
||||
| 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 |
|
||||
@@ -52,27 +51,6 @@
|
||||
|
||||
## 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.
|
||||
|
||||
Today `readAdfDocument` and the converters refuse every node, such as one with a `Date` in an
|
||||
|
||||
Reference in New Issue
Block a user