32 - a mark's attribute depth is counted from its value, the marks spelling refusing its own #121
@@ -158,20 +158,14 @@ converts no raw HTML, so at `0.2.0` the mapped elements stop erroring and the co
|
|||||||
no ADF node carries. A refusal found before its path is known — the block walk's, a directive
|
no ADF node carries. A refusal found before its path is known — the block walk's, a directive
|
||||||
reader's — is a `ConvertFault`, the code and message alone; the node walk attaches the path as it
|
reader's — is a `ConvertFault`, the code and message alone; the node walk attaches the path as it
|
||||||
descends, so a document reports its first error in document order. `not-an-adf-document` carries
|
descends, so a document reports its first error in document order. `not-an-adf-document` carries
|
||||||
the document's own path throughout: eight of the guard's nine branches read the document's own
|
the document's own path throughout: seven of the guard's eight branches read the document's own
|
||||||
shape, and threading a path to the ninth — a malformed node anywhere in the tree — wants the
|
shape, and threading a path to the eighth — a malformed node anywhere in the tree — wants the
|
||||||
manual stack §11's no-recursion rule forces, whose empty half no input reaches. The message names
|
manual stack §11's no-recursion rule forces, whose empty half no input reaches. The message names
|
||||||
the violation instead. Depth is not one of the nine: `adfDocumentFault` returns the code with the
|
the violation instead. An attribute value past 500 levels is `unsupported-nesting-depth` in both
|
||||||
message, so an attribute value past 500 levels is `unsupported-nesting-depth` from the emitter as
|
directions, the guard included. An attribute is counted from its value; a spelling that nests it
|
||||||
it already is from the parser, both directions refusing the same value. A node's attribute is
|
deeper — the block directive's `marks`, the carry — refuses in its own format, as its parser does.
|
||||||
counted from the value itself, never from the `attrs` object holding it; a mark's is counted three
|
A non-finite number takes two codes: `unsupported-node-shape` parsing, `not-an-adf-document`
|
||||||
levels in, because the block directive spells the whole mark set as one JSON attribute and the
|
emitting — no document holds one, so no round-trip crosses them.
|
||||||
parser reads the value at the bottom of array, mark and `attrs`. `isAdfDocument` is true for a depth fault:
|
|
||||||
a deep document is a document, as the 2000-level blocks and the 600-deep marks the guard already
|
|
||||||
waves through are, and depth is the walks' answer rather than the shape's. A non-finite number
|
|
||||||
stays parted where depth is joined: the parse says `unsupported-node-shape` because the markdown is
|
|
||||||
at fault, the emit `not-an-adf-document` because the input is, and unlike depth nothing round-trips
|
|
||||||
inconsistently between them.
|
|
||||||
|
|
||||||
`position` is the parse side's alone: an emitter reads no source, so an emit error carries `path`
|
`position` is the parse side's alone: an emitter reads no source, so an emit error carries `path`
|
||||||
and nothing more. It is `{ line, offset }` at the start of the line the block holding the refusal
|
and nothing more. It is `{ line, offset }` at the start of the line the block holding the refusal
|
||||||
|
|||||||
@@ -61,16 +61,15 @@ test('rejects a node whose shape ProseMirror JSON cannot hold', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('names the attribute nesting past the levels the parser reads one at, and still calls the value a document', () => {
|
test('names the attribute nesting past the levels the parser reads one at, and still calls the value a document', () => {
|
||||||
const deeper = (key: string, type: string, levels: number = largestNesting): string =>
|
const deeper = (key: string, type: string): string => `the ${key} attribute of ${type} nests deeper than the ${largestNesting} levels an attribute carries`
|
||||||
`the ${key} attribute of ${type} nests deeper than the ${levels} levels an attribute carries`
|
|
||||||
assert.equal(fault(withAttribute(nested(largestNesting))), 'accepted')
|
assert.equal(fault(withAttribute(nested(largestNesting))), 'accepted')
|
||||||
assert.equal(fault(withAttribute(nested(largestNesting + 1))), deeper('a', 'paragraph'))
|
assert.equal(fault(withAttribute(nested(largestNesting + 1))), deeper('a', 'paragraph'))
|
||||||
assert.equal(faultCode(withAttribute(nested(largestNesting + 1))), 'unsupported-nesting-depth')
|
assert.equal(faultCode(withAttribute(nested(largestNesting + 1))), 'unsupported-nesting-depth')
|
||||||
assert.equal(isAdfDocument(withAttribute(nested(largestNesting + 1))), true)
|
assert.equal(isAdfDocument(withAttribute(nested(largestNesting + 1))), true)
|
||||||
const marked = (levels: number): unknown => ({ content: [{ marks: [{ attrs: { a: nested(levels) }, type: 'link' }], text: 'x', type: 'text' }], type: 'doc', version: 1 })
|
const marked = (levels: number): unknown => ({ content: [{ marks: [{ attrs: { a: nested(levels) }, type: 'link' }], text: 'x', type: 'text' }], type: 'doc', version: 1 })
|
||||||
assert.equal(fault(marked(largestNesting - 3)), 'accepted')
|
assert.equal(fault(marked(largestNesting)), 'accepted')
|
||||||
assert.equal(fault(marked(largestNesting - 2)), deeper('a', 'link', largestNesting - 3))
|
assert.equal(fault(marked(largestNesting + 1)), deeper('a', 'link'))
|
||||||
assert.equal(isAdfDocument(marked(largestNesting - 2)), true)
|
assert.equal(isAdfDocument(marked(largestNesting + 1)), true)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('accepts the JSON values an attribute may hold', () => {
|
test('accepts the JSON values an attribute may hold', () => {
|
||||||
|
|||||||
+5
-8
@@ -23,9 +23,6 @@ export type AdfDocument = {
|
|||||||
version: number
|
version: number
|
||||||
}
|
}
|
||||||
|
|
||||||
// A block directive spells the whole mark set as one JSON attribute, so a mark's value sits three levels inside it.
|
|
||||||
const markAttributeNesting = largestNesting - 3
|
|
||||||
|
|
||||||
const documentKeys = ['content', 'type', 'version']
|
const documentKeys = ['content', 'type', 'version']
|
||||||
const markKeys = ['attrs', 'type']
|
const markKeys = ['attrs', 'type']
|
||||||
const nodeKeys = ['attrs', 'content', 'marks', 'text', 'type']
|
const nodeKeys = ['attrs', 'content', 'marks', 'text', 'type']
|
||||||
@@ -47,8 +44,8 @@ export function adfDocumentFault(value: unknown): ConvertFault | undefined {
|
|||||||
return nestingFault(content)
|
return nestingFault(content)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function attributeNestingMessage(key: string, type: string, levels: number = largestNesting): string {
|
export function attributeNestingMessage(key: string, type: string): string {
|
||||||
return `the ${key} attribute of ${type} nests deeper than the ${levels} levels an attribute carries`
|
return `the ${key} attribute of ${type} nests deeper than the ${largestNesting} levels an attribute carries`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function carriesOnly(node: AdfNode, attributes: readonly string[]): boolean {
|
export function carriesOnly(node: AdfNode, attributes: readonly string[]): boolean {
|
||||||
@@ -116,15 +113,15 @@ function nestingFault(nodes: readonly AdfNode[]): ConvertFault | undefined {
|
|||||||
|
|
||||||
function marksFault(marks: readonly AdfMark[]): ConvertFault | undefined {
|
function marksFault(marks: readonly AdfMark[]): ConvertFault | undefined {
|
||||||
for (const mark of marks) {
|
for (const mark of marks) {
|
||||||
const fault = attributesFault(nodeAttrs(mark), mark.type, markAttributeNesting)
|
const fault = attributesFault(nodeAttrs(mark), mark.type)
|
||||||
if (fault !== undefined) return fault
|
if (fault !== undefined) return fault
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function attributesFault(attrs: AdfAttributes, type: string, levels: number = largestNesting): ConvertFault | undefined {
|
function attributesFault(attrs: AdfAttributes, type: string): ConvertFault | undefined {
|
||||||
for (const [key, value] of Object.entries(attrs)) {
|
for (const [key, value] of Object.entries(attrs)) {
|
||||||
if (overNested(value, levels)) return { code: 'unsupported-nesting-depth', message: attributeNestingMessage(key, type, levels) }
|
if (overNested(value)) return { code: 'unsupported-nesting-depth', message: attributeNestingMessage(key, type) }
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -353,8 +353,8 @@ test('refuses marks and attributes nested deeper than the emitter carries', () =
|
|||||||
assert.equal(code(adfToMarkdown(document(paragraph({ marks, text: 'x', type: 'text' })))), 'unsupported-nesting-depth')
|
assert.equal(code(adfToMarkdown(document(paragraph({ marks, text: 'x', type: 'text' })))), 'unsupported-nesting-depth')
|
||||||
let attrs: AdfMark['attrs'] = { depth: 'x' }
|
let attrs: AdfMark['attrs'] = { depth: 'x' }
|
||||||
for (let depth = 0; depth < 600; depth += 1) attrs = { depth: attrs }
|
for (let depth = 0; depth < 600; depth += 1) attrs = { depth: attrs }
|
||||||
const deeper = (key: string, type: string, levels: number = largestNesting): string =>
|
const deeper = (key: string, type: string): string =>
|
||||||
`unsupported-nesting-depth: the ${key} attribute of ${type} nests deeper than the ${levels} levels an attribute carries`
|
`unsupported-nesting-depth: the ${key} attribute of ${type} nests deeper than the ${largestNesting} levels an attribute carries`
|
||||||
const nested = (levels: number): JsonValue => {
|
const nested = (levels: number): JsonValue => {
|
||||||
let value: JsonValue = 1
|
let value: JsonValue = 1
|
||||||
for (let level = 0; level < levels; level += 1) value = [value]
|
for (let level = 0; level < levels; level += 1) value = [value]
|
||||||
@@ -375,11 +375,18 @@ test('refuses marks and attributes nested deeper than the emitter carries', () =
|
|||||||
assert.deepEqual(toEditorNormal(read.value), document(node))
|
assert.deepEqual(toEditorNormal(read.value), document(node))
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.equal(markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs, type: 'em' }], text: 'x', type: 'text' })))), deeper('depth', 'em', largestNesting - 3))
|
assert.equal(markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs, type: 'em' }], text: 'x', type: 'text' })))), deeper('depth', 'em'))
|
||||||
|
assert.equal(
|
||||||
|
markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs: { deep: nested(largestNesting) }, type: 'em' }], text: 'x', type: 'text' })))),
|
||||||
|
`unsupported-nesting-depth: a carried node's JSON nests deeper than the ${largestNesting} levels its position leaves`,
|
||||||
|
)
|
||||||
assert.equal(markdown(adfToMarkdown(document(paragraph(card(largestNesting + 1))))), deeper('data', 'inlineCard'))
|
assert.equal(markdown(adfToMarkdown(document(paragraph(card(largestNesting + 1))))), deeper('data', 'inlineCard'))
|
||||||
assert.deepEqual(path(adfToMarkdown(document(paragraph(card(largestNesting + 1))))), [])
|
assert.deepEqual(path(adfToMarkdown(document(paragraph(card(largestNesting + 1))))), [])
|
||||||
roundTrips(paragraph(card(largestNesting)))
|
roundTrips(paragraph(card(largestNesting)))
|
||||||
assert.equal(markdown(adfToMarkdown(document(marked(largestNesting - 2)))), deeper('deep', 'em', largestNesting - 3))
|
assert.equal(markdown(adfToMarkdown(document(marked(largestNesting - 2)))), deeper('marks', 'panel'))
|
||||||
|
assert.deepEqual(path(adfToMarkdown(document(marked(largestNesting - 2)))), ['content', 0])
|
||||||
|
const markedCode: AdfNode = { content: [{ text: 'x', type: 'text' }], marks: [{ attrs: { deep: nested(largestNesting - 2) }, type: 'em' }], type: 'codeBlock' }
|
||||||
|
assert.equal(markdown(adfToMarkdown(document(markedCode))), deeper('marks', 'codeBlock'))
|
||||||
roundTrips(marked(largestNesting - 3))
|
roundTrips(marked(largestNesting - 3))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -147,9 +147,10 @@ function emitDirectiveBlock(node: AdfNode, model: BlockNodeModel, path: ConvertE
|
|||||||
if (node.text !== undefined) return failure('unsupported-node-shape', `a ${node.type} carries no text: this one holds text`, path)
|
if (node.text !== undefined) return failure('unsupported-node-shape', `a ${node.type} carries no text: this one holds text`, path)
|
||||||
if (blockDirectiveForm(node.type) === 'leaf' && nodeContent(node).length > 0) return failure('unsupported-node-shape', `a ${node.type} holds no content: this one holds some`, path)
|
if (blockDirectiveForm(node.type) === 'leaf' && nodeContent(node).length > 0) return failure('unsupported-node-shape', `a ${node.type} holds no content: this one holds some`, path)
|
||||||
if (model.contentModel === 'code') return emitCodeDirective(node, model, path, depth)
|
if (model.contentModel === 'code') return emitCodeDirective(node, model, path, depth)
|
||||||
const opener = spellBlockDirectiveOpener(node, model)
|
const opener = spellBlockDirectiveOpener(node, model, path)
|
||||||
if (opener === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
if (opener === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
||||||
return emitDirectiveBody(node, model, opener, path, walkBody)
|
if (!opener.ok) return opener
|
||||||
|
return emitDirectiveBody(node, model, opener.value, path, walkBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitDirectiveBody(node: AdfNode, model: BlockNodeModel, opener: string, path: ConvertErrorPath, walkBody: () => Result<Walk>): Result<EmittedBlock> {
|
function emitDirectiveBody(node: AdfNode, model: BlockNodeModel, opener: string, path: ConvertErrorPath, walkBody: () => Result<Walk>): Result<EmittedBlock> {
|
||||||
@@ -186,11 +187,12 @@ function tryCodeBlock(node: AdfNode, path: ConvertErrorPath): Result<EmittedBloc
|
|||||||
|
|
||||||
function emitCodeDirective(node: AdfNode, model: BlockNodeModel, path: ConvertErrorPath, depth: number): Result<EmittedBlock> {
|
function emitCodeDirective(node: AdfNode, model: BlockNodeModel, path: ConvertErrorPath, depth: number): Result<EmittedBlock> {
|
||||||
const slot = languageSlot(nodeAttrs(node)['language'])
|
const slot = languageSlot(nodeAttrs(node)['language'])
|
||||||
const opener = spellBlockDirectiveOpener(node, model, slot.kind === 'attribute' ? [] : ['language'])
|
const opener = spellBlockDirectiveOpener(node, model, path, slot.kind === 'attribute' ? [] : ['language'])
|
||||||
if (opener === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
if (opener === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
||||||
|
if (!opener.ok) return opener
|
||||||
const text = codeBlockText(node, path)
|
const text = codeBlockText(node, path)
|
||||||
if (!text.ok) return text
|
if (!text.ok) return text
|
||||||
return success(directivePair(node, opener, fencedCodeBlock(slot.kind === 'fence' ? slot.info : '', text.value)))
|
return success(directivePair(node, opener.value, fencedCodeBlock(slot.kind === 'fence' ? slot.info : '', text.value)))
|
||||||
}
|
}
|
||||||
|
|
||||||
function codeBlockText(node: AdfNode, path: ConvertErrorPath): Result<string> {
|
function codeBlockText(node: AdfNode, path: ConvertErrorPath): Result<string> {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import type { AdfNode } from '../../adf/document.ts'
|
import type { AdfNode } from '../../adf/document.ts'
|
||||||
import type { BlockNodeModel } from '../../adf/block-nodes.ts'
|
import type { BlockNodeModel } from '../../adf/block-nodes.ts'
|
||||||
|
import { attributeNestingMessage, nodeAttrs, nodeMarks } from '../../adf/document.ts'
|
||||||
import { blockArgument } from '../block-directive-arguments.ts'
|
import { blockArgument } from '../block-directive-arguments.ts'
|
||||||
|
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
import { isBareToken, spellAttributes, spellDirectiveOpener, spellJsonAttribute, spellVocabulary } from '../directive-syntax.ts'
|
import { isBareToken, spellAttributes, spellDirectiveOpener, spellJsonAttribute, spellVocabulary } from '../directive-syntax.ts'
|
||||||
import { markValues, marksAttribute } from '../block-directive-marks.ts'
|
import { markValues, marksAttribute } from '../block-directive-marks.ts'
|
||||||
import { nodeAttrs, nodeMarks } from '../../adf/document.ts'
|
import { overNested } from '../../json-value.ts'
|
||||||
import { vocabularyPairs } from '../../adf/attribute-vocabulary.ts'
|
import { vocabularyPairs } from '../../adf/attribute-vocabulary.ts'
|
||||||
|
|
||||||
export function spellBlockDirectiveOpener(node: AdfNode, model: BlockNodeModel, spelledByBody: readonly string[] = []): string | undefined {
|
export function spellBlockDirectiveOpener(node: AdfNode, model: BlockNodeModel, path: ConvertErrorPath, spelledByBody: readonly string[] = []): Result<string> | undefined {
|
||||||
const argumentAttribute = blockArgument(node.type)
|
const argumentAttribute = blockArgument(node.type)
|
||||||
const slot = bareArgument(node, argumentAttribute)
|
const slot = bareArgument(node, argumentAttribute)
|
||||||
if (slot === undefined) return undefined
|
if (slot === undefined) return undefined
|
||||||
@@ -15,8 +17,12 @@ export function spellBlockDirectiveOpener(node: AdfNode, model: BlockNodeModel,
|
|||||||
if (pairs === undefined) return undefined
|
if (pairs === undefined) return undefined
|
||||||
const spelledPairs = spellVocabulary(pairs)
|
const spelledPairs = spellVocabulary(pairs)
|
||||||
const marks = nodeMarks(node)
|
const marks = nodeMarks(node)
|
||||||
if (marks.length > 0) spelledPairs.push([marksAttribute, spellJsonAttribute(markValues(marks))])
|
if (marks.length > 0) {
|
||||||
return spellDirectiveOpener(node.type, slot.argument, spellAttributes(spelledPairs))
|
const values = markValues(marks)
|
||||||
|
if (overNested(values)) return failure('unsupported-nesting-depth', attributeNestingMessage(marksAttribute, node.type), path)
|
||||||
|
spelledPairs.push([marksAttribute, spellJsonAttribute(values)])
|
||||||
|
}
|
||||||
|
return success(spellDirectiveOpener(node.type, slot.argument, spellAttributes(spelledPairs)))
|
||||||
}
|
}
|
||||||
|
|
||||||
// `undefined` where the argument slot holds a value no bare token spells.
|
// `undefined` where the argument slot holds a value no bare token spells.
|
||||||
|
|||||||
@@ -1018,6 +1018,18 @@ The done `todo.md` items in full, as they were written. `todo.md` keeps a one-li
|
|||||||
`parse/` imports `emit/` twice — `commonMarkSpelling` and `openingLinkTakesDirective` — and
|
`parse/` imports `emit/` twice — `commonMarkSpelling` and `openingLinkTakesDirective` — and
|
||||||
§11 says those two are the whole of the seam a `../emit/` grep under `parse/` reads.
|
§11 says those two are the whole of the seam a `../emit/` grep under `parse/` reads.
|
||||||
|
|
||||||
|
- [x] **32 — The mark depth `adf/` counts is stated in ADF's terms (`0.2.0`).**
|
||||||
|
`document.ts`'s `markAttributeNesting` is `largestNesting - 3`, and both the comment above it
|
||||||
|
and AGENTS.md §8 give the reason as markdown's: the block directive spells the whole mark set
|
||||||
|
as one JSON attribute, so the parser reads the value at the bottom of array, mark and
|
||||||
|
`attrs`. That is a format's spelling deciding a constant inside the directory §11 forbids to
|
||||||
|
know a format. Move the derivation to where that spelling lives, or state the three levels in
|
||||||
|
ADF's own vocabulary. Before 7 gives the constant a second format whose spelling may not
|
||||||
|
spend the same three levels.
|
||||||
|
**Done** (2026-09-23): `adf/` counts a mark's attribute from its value at 500, as a node's;
|
||||||
|
the block directive's `marks` spelling refuses its own over-deep JSON with the parser's
|
||||||
|
message and the node's path, and §8 says a deeper spelling refuses in its own format.
|
||||||
|
|
||||||
## 5 — Ship `0.1.0`
|
## 5 — Ship `0.1.0`
|
||||||
|
|
||||||
- [ ] **5 — Ship `0.1.0`.** Only the maintainer's own acts are left (§15): make the Gitea repo
|
- [ ] **5 — Ship `0.1.0`.** Only the maintainer's own acts are left (§15): make the Gitea repo
|
||||||
|
|||||||
@@ -56,14 +56,6 @@ chunk clearing a §11 seam.
|
|||||||
upward, so the first raise to the measured figure reddens a run that changed nothing. Make
|
upward, so the first raise to the measured figure reddens a run that changed nothing. Make
|
||||||
the measurement repeatable, or state the number the floor may be raised to and why it is not
|
the measurement repeatable, or state the number the floor may be raised to and why it is not
|
||||||
the measured one.
|
the measured one.
|
||||||
- [ ] **32 — The mark depth `adf/` counts is stated in ADF's terms (`0.2.0`).**
|
|
||||||
`document.ts`'s `markAttributeNesting` is `largestNesting - 3`, and both the comment above it
|
|
||||||
and AGENTS.md §8 give the reason as markdown's: the block directive spells the whole mark set
|
|
||||||
as one JSON attribute, so the parser reads the value at the bottom of array, mark and
|
|
||||||
`attrs`. That is a format's spelling deciding a constant inside the directory §11 forbids to
|
|
||||||
know a format. Move the derivation to where that spelling lives, or state the three levels in
|
|
||||||
ADF's own vocabulary. Before 7 gives the constant a second format whose spelling may not
|
|
||||||
spend the same three levels.
|
|
||||||
- [ ] **23 — The block-directive fragments are one file (`0.2.0`).** `block-directive-arguments.ts`,
|
- [ ] **23 — The block-directive fragments are one file (`0.2.0`).** `block-directive-arguments.ts`,
|
||||||
`-forms.ts` and `-marks.ts` are three files under 25 lines answering one question. Fold them,
|
`-forms.ts` and `-marks.ts` are three files under 25 lines answering one question. Fold them,
|
||||||
and take `src/markdown/` — the worst level both architects named, 13 entries with no
|
and take `src/markdown/` — the worst level both architects named, 13 entries with no
|
||||||
@@ -292,6 +284,7 @@ chunk clearing a §11 seam.
|
|||||||
- [x] **28 — `emitLine`'s retry loop cannot spin (`0.2.0`).**
|
- [x] **28 — `emitLine`'s retry loop cannot spin (`0.2.0`).**
|
||||||
- [x] **29 — The README reads raw HTML as refused for good (`0.2.0`).**
|
- [x] **29 — The README reads raw HTML as refused for good (`0.2.0`).**
|
||||||
- [x] **30 — AGENTS.md says each thing once (`0.2.0`).**
|
- [x] **30 — AGENTS.md says each thing once (`0.2.0`).**
|
||||||
|
- [x] **32 — The mark depth `adf/` counts is stated in ADF's terms (`0.2.0`).**
|
||||||
|
|
||||||
## The ADF inventory to cover
|
## The ADF inventory to cover
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user