5c: match the mark set's nesting on both sides of the round-trip
This commit is contained in:
@@ -133,8 +133,10 @@ shape, and threading a path to the ninth — a malformed node anywhere in the tr
|
||||
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
|
||||
message, so an attribute value past 500 levels is `unsupported-nesting-depth` from the emitter as
|
||||
it already is from the parser, both directions refusing the same value — the count is the levels
|
||||
an attribute holds, never the `attrs` object holding it. `isAdfDocument` is true for a depth fault:
|
||||
it already is from the parser, both directions refusing the same value. A node's attribute is
|
||||
counted from the value itself, never from the `attrs` object holding it; a mark's is counted three
|
||||
levels in, because the block directive spells the whole mark set as one JSON attribute and the
|
||||
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
|
||||
|
||||
+4
-2
@@ -19,9 +19,11 @@ fi
|
||||
|
||||
name=$(read_field name)
|
||||
version=$(read_field version)
|
||||
published=$(published_version "$name" "$version")
|
||||
tagged=$(git ls-remote --tags origin "v$version")
|
||||
|
||||
# Both steps observe their own end state, so a partial run converges on the next push to main.
|
||||
if [ -z "$(published_version "$name" "$version")" ]; then
|
||||
if [ -z "$published" ]; then
|
||||
: "${NPM_TOKEN:?the publish needs NPM_TOKEN}"
|
||||
in_image "$node_image" npm ci
|
||||
in_image "$node_image" npm run build
|
||||
@@ -29,7 +31,7 @@ if [ -z "$(published_version "$name" "$version")" ]; then
|
||||
'printf "//registry.npmjs.org/:_authToken=%s\n" "$NPM_TOKEN" > "$HOME/.npmrc" && npm publish --access public'
|
||||
fi
|
||||
|
||||
if [ -z "$(git ls-remote --tags origin "v$version")" ]; then
|
||||
if [ -z "$tagged" ]; then
|
||||
git tag "v$version"
|
||||
git push origin "v$version"
|
||||
fi
|
||||
|
||||
@@ -61,14 +61,16 @@ 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', () => {
|
||||
const deeper = (key: string, type: string): string => `the ${key} attribute of ${type} nests deeper than the ${largestNesting} levels an attribute carries`
|
||||
const deeper = (key: string, type: string, levels: number = largestNesting): string =>
|
||||
`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 + 1))), deeper('a', 'paragraph'))
|
||||
assert.equal(faultCode(withAttribute(nested(largestNesting + 1))), 'unsupported-nesting-depth')
|
||||
assert.equal(isAdfDocument(withAttribute(nested(largestNesting + 1))), true)
|
||||
const marked = { content: [{ marks: [{ attrs: { a: nested(largestNesting + 1) }, type: 'link' }], text: 'x', type: 'text' }], type: 'doc', version: 1 }
|
||||
assert.equal(fault(marked), deeper('a', 'link'))
|
||||
assert.equal(isAdfDocument(marked), true)
|
||||
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 - 2)), deeper('a', 'link', largestNesting - 3))
|
||||
assert.equal(isAdfDocument(marked(largestNesting - 2)), true)
|
||||
})
|
||||
|
||||
test('accepts the JSON values an attribute may hold', () => {
|
||||
|
||||
+8
-5
@@ -23,6 +23,9 @@ export type AdfDocument = {
|
||||
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 markKeys = ['attrs', 'type']
|
||||
const nodeKeys = ['attrs', 'content', 'marks', 'text', 'type']
|
||||
@@ -44,8 +47,8 @@ export function adfDocumentFault(value: unknown): ConvertFault | undefined {
|
||||
return nestingFault(content)
|
||||
}
|
||||
|
||||
export function attributeNestingMessage(key: string, type: string): string {
|
||||
return `the ${key} attribute of ${type} nests deeper than the ${largestNesting} levels an attribute carries`
|
||||
export function attributeNestingMessage(key: string, type: string, levels: number = largestNesting): string {
|
||||
return `the ${key} attribute of ${type} nests deeper than the ${levels} levels an attribute carries`
|
||||
}
|
||||
|
||||
export function carriesOnly(node: AdfNode, attributes: readonly string[]): boolean {
|
||||
@@ -101,15 +104,15 @@ function nestingFault(nodes: readonly AdfNode[]): ConvertFault | undefined {
|
||||
|
||||
function marksFault(marks: readonly AdfMark[] | undefined): ConvertFault | undefined {
|
||||
for (const mark of marks ?? []) {
|
||||
const fault = attributesFault(mark.attrs, mark.type)
|
||||
const fault = attributesFault(mark.attrs, mark.type, markAttributeNesting)
|
||||
if (fault !== undefined) return fault
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function attributesFault(attrs: AdfAttributes | undefined, type: string): ConvertFault | undefined {
|
||||
function attributesFault(attrs: AdfAttributes | undefined, type: string, levels: number = largestNesting): ConvertFault | undefined {
|
||||
for (const [key, value] of Object.entries(attrs ?? {})) {
|
||||
if (overNested(value)) return { code: 'unsupported-nesting-depth', message: attributeNestingMessage(key, type) }
|
||||
if (overNested(value, levels)) return { code: 'unsupported-nesting-depth', message: attributeNestingMessage(key, type, levels) }
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -336,19 +336,33 @@ 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')
|
||||
let attrs: AdfMark['attrs'] = { depth: 'x' }
|
||||
for (let depth = 0; depth < 600; depth += 1) attrs = { depth: attrs }
|
||||
const deeper = (key: string, type: string): string =>
|
||||
`unsupported-nesting-depth: the ${key} attribute of ${type} nests deeper than the ${largestNesting} levels an attribute carries`
|
||||
assert.equal(markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs, type: 'em' }], text: 'x', type: 'text' })))), deeper('depth', 'em'))
|
||||
const card = (levels: number): AdfNode => {
|
||||
let data: JsonValue = 1
|
||||
for (let level = 0; level < levels; level += 1) data = [data]
|
||||
return { attrs: { data, url: 'https://example.com/a' }, type: 'inlineCard' }
|
||||
const deeper = (key: string, type: string, levels: number = largestNesting): string =>
|
||||
`unsupported-nesting-depth: the ${key} attribute of ${type} nests deeper than the ${levels} levels an attribute carries`
|
||||
const nested = (levels: number): JsonValue => {
|
||||
let value: JsonValue = 1
|
||||
for (let level = 0; level < levels; level += 1) value = [value]
|
||||
return value
|
||||
}
|
||||
const card = (levels: number): AdfNode => ({ attrs: { data: nested(levels), url: 'https://example.com/a' }, type: 'inlineCard' })
|
||||
// A block directive spells the mark set as one JSON attribute, so a mark's value is read three levels in.
|
||||
const marked = (levels: number): AdfNode => ({
|
||||
attrs: { panelType: 'info' },
|
||||
content: [paragraph({ text: 'x', type: 'text' })],
|
||||
marks: [{ attrs: { deep: nested(levels) }, type: 'em' }],
|
||||
type: 'panel',
|
||||
})
|
||||
const roundTrips = (node: AdfNode): void => {
|
||||
const spelled = adfToMarkdown(document(node))
|
||||
assert.ok(spelled.ok, spelled.ok ? '' : spelled.error.message)
|
||||
assert.deepEqual(markdownToAdf(spelled.value), { ok: true, 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(card(largestNesting + 1))))), deeper('data', 'inlineCard'))
|
||||
assert.deepEqual(path(adfToMarkdown(document(paragraph(card(largestNesting + 1))))), [])
|
||||
const spelled = adfToMarkdown(document(paragraph(card(largestNesting))))
|
||||
assert.ok(spelled.ok, spelled.ok ? '' : spelled.error.message)
|
||||
assert.deepEqual(markdownToAdf(spelled.value), { ok: true, value: document(paragraph(card(largestNesting))) })
|
||||
roundTrips(paragraph(card(largestNesting)))
|
||||
assert.equal(markdown(adfToMarkdown(document(marked(largestNesting - 2)))), deeper('deep', 'em', largestNesting - 3))
|
||||
roundTrips(marked(largestNesting - 3))
|
||||
})
|
||||
|
||||
test('escapes a literal delimiter that would merge with an emitted one', () => {
|
||||
|
||||
@@ -441,6 +441,7 @@ test('names the depth an attribute value nests past, never the kind the JSON rea
|
||||
`unsupported-nesting-depth: the ${key} attribute of ${type} nests deeper than the ${largestNesting} levels an attribute carries`
|
||||
assert.equal(content(markdownToAdf(`:::tableCell {colwidth="${nested(largestNesting + 1)}"}\n:::\n`)), deeper('colwidth', 'tableCell'))
|
||||
assert.equal(content(markdownToAdf(`::rule {marks="${nested(largestNesting + 1)}"}\n`)), deeper('marks', 'rule'))
|
||||
assert.equal(content(markdownToAdf(`::rule {marks="[{\\"attrs\\":{\\"deep\\":${nested(largestNesting - 2)}},\\"type\\":\\"em\\"}]"}\n`)), deeper('marks', 'rule'))
|
||||
assert.equal(content(markdownToAdf(`::media {width="${nested(largestNesting + 1)}"}\n`)), 'unsupported-node-shape: the width attribute of media is no number')
|
||||
})
|
||||
|
||||
|
||||
@@ -95,7 +95,11 @@ The numbering is the order the work was planned in, not the order it ships.
|
||||
fallback. Memoizing `emitBlock` is the shortcut, and the node reference is the wrong key: a
|
||||
caller may hold one node object at two positions, where the cached depth and path are
|
||||
another node's. `0.1.0` ships with the retry in it, so a deep document is slow rather than
|
||||
wrong until the patch.
|
||||
wrong until the patch. `adfDocumentFault` is the second site to look at: `isNodeArray` reads
|
||||
every node and attribute value, then `nestingFault` reads them again, so the emit entry the
|
||||
export persona runs in bulk walks the document twice. Both walks are linear, so this is a
|
||||
constant factor rather than 4b's class change, and the parting is what gives depth its own
|
||||
code (§8) — measure before joining them back.
|
||||
- [ ] **4c — The scanning rule's remaining sites (`0.1.1`).** A trailing-anchored regex re-walks
|
||||
its run from every start position, so an interior whitespace run costs quadratic time rather
|
||||
than linear — 3h measured 80k spaces inside an ATX heading at 11.3s, and 3ms once the walk
|
||||
|
||||
Reference in New Issue
Block a user