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

Answer stability round 1: 0 🔴, 6 🟡 (0 misread); restore inlineRuns's early exit, one mark equality, refuse a pass taking no mark, pin the carried range and the opening link's run

This commit is contained in:
2026-11-03 21:54:29 +01:00
parent e0ed1eb4a4
commit 84c45c30a5
7 changed files with 61 additions and 17 deletions
+10
View File
@@ -49,6 +49,16 @@ test('refuses a node whose carry nests too deep ahead of a later node in its lin
assert.deepEqual(path(refused), ['content', 0, 'content', 0])
})
test('refuses a carried run at the node whose carry nests too deep, among the run\'s own nodes', () => {
let deep: JsonValue = 'a'
for (let depth = 0; depth < largestNesting - 1; depth += 1) deep = { deep }
const carriedEm: AdfNode = { marks: [{ attrs: { deep }, type: 'em' }], text: 'x', type: 'text' }
const twiceStrong: AdfNode = { marks: [{ type: 'strong' }, { type: 'strong' }], text: 'y', type: 'text' }
const refused = adfToLosslessMarkdown(document(paragraph({ text: 'ok', type: 'text' }, carriedEm, twiceStrong)))
assert.equal(code(refused), 'unsupported-nesting-depth')
assert.deepEqual(path(refused), ['content', 0, 'content', 1])
})
test('refuses a value that is not an ADF document, naming the check it failed', () => {
assert.equal(markdown(adfToLosslessMarkdown({ type: 'doc', version: Number.NaN })), 'not-an-adf-document: an ADF document holds a version number: found NaN')
})
+7 -5
View File
@@ -234,13 +234,15 @@ function inlineRuns(nodes: readonly RunNode[], firstIndex: number, carried: Read
for (const [offset, runNode] of nodes.entries()) {
const index = firstIndex + offset
const { node, outer } = runNode
const previous = runs[runs.length - 1]
// A carried node spells its marks inside the carry.
const carriedNode = carries(node, carried, index)
const inside = carriedNode || previous?.kind !== 'marked' ? undefined : insideRun(runNode, previous.mark)
const mark = carriedNode ? undefined : nodeMarks(node)[outer]
const mark = carries(node, carried, index) ? undefined : nodeMarks(node)[outer]
if (mark === undefined) {
runs.push({ index, kind: 'plain', node })
continue
}
const previous = runs[runs.length - 1]
const inside = previous?.kind === 'marked' ? insideRun(runNode, previous.mark) : undefined
if (previous?.kind === 'marked' && inside !== undefined) previous.nodes.push(inside)
else if (mark === undefined) runs.push({ index, kind: 'plain', node })
else runs.push({ index, kind: 'marked', mark, nodes: [{ node, outer: outer + 1 }] })
}
return runs
@@ -465,6 +465,11 @@ test('writes what the editor-normal form of the document writes', () => {
assert.equal(portable(paragraph({ marks: [{ attrs: {}, type: 'code' }], text: 'a', type: 'text' }, { marks: [{ type: 'code' }], text: '|b', type: 'text' })), '`a|b`\n')
})
test('keeps the code mark of a node past an opening link when the fallback drops the link\'s own', () => {
const link: AdfMark = { attrs: { href: 'u' }, type: 'link' }
assert.equal(portable(paragraph(text('x]: y', link, code), text('z', code))), '[x\\]: y](u)`z`\n')
})
test('writes a text node holding content the same in either order beside its neighbour', () => {
const holding: AdfNode = { content: [text('inner')], text: 'a', type: 'text' }
assert.equal(portable(paragraph(holding, text('b'))), 'ab\n')
+1 -5
View File
@@ -1,6 +1,6 @@
import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
import type { JsonValue } from '../../json-value.ts'
import { identicalMark, mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
import { mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
import { joinsWhenRead } from '../adjacent-text.ts'
type JsonContainer = JsonValue[] | { [key: string]: JsonValue }
@@ -13,10 +13,6 @@ export function joinsWhenEditorNormal(previous: AdfNode, node: AdfNode): boolean
return joinsWhenRead(normalNode(previous), normalNode(node))
}
export function sameMarkWhenEditorNormal(left: AdfMark, right: AdfMark): boolean {
return identicalMark(normalMark(left), normalMark(right))
}
export function toEditorNormal(document: AdfDocument): AdfDocument {
const normal: AdfDocument = { type: document.type, version: Object.is(document.version, -0) ? 0 : document.version }
const pending: { holder: NodeHolder; source: NodeHolder }[] = [{ holder: normal, source: document }]
@@ -37,6 +37,30 @@ test('reduces a line alike dropping every run a pass may or the first alone', ()
})
// Drops only the first run each pass finds, with every misflanked highlight, and takes no cap.
test('refuses an opening-link fallback whose link run holds no code span', () => {
const link = { attrs: { href: 'u' }, type: 'link' }
const lines: AdfNode[][] = [
[{ marks: [link], text: 'a', type: 'text' }],
[{ marks: [link, { type: 'em' }], text: 'a', type: 'text' }],
[{ marks: [link], text: 'a', type: 'text' }, { marks: [{ type: 'code' }], text: 'c', type: 'text' }],
]
for (const line of lines) {
const refused = withoutFallback(line, { kind: 'opening-link' }, [])
assert.equal(refused.ok ? 'kept' : refused.error.code, 'unsupported-node-shape')
}
})
test('takes code off every node of an opening link\'s run that an opening-link fallback names', () => {
const link = { attrs: { href: 'u' }, type: 'link' }
const line: AdfNode[] = [{ marks: [link, { type: 'code' }], text: 'a', type: 'text' }, { marks: [link], text: 'b', type: 'text' }]
assert.deepEqual(withoutFallback(line, { kind: 'opening-link' }, []), success([{ marks: [link], text: 'a', type: 'text' }, { marks: [link], text: 'b', type: 'text' }]))
})
test('refuses an unspellable-run fallback that takes no mark', () => {
const refused = withoutFallback([{ marks: [{ type: 'em' }], text: 'a', type: 'text' }], { kind: 'unspellable-run', runs: [{ depth: 1, first: 0, last: 0 }] }, [])
assert.equal(refused.ok ? 'kept' : refused.error.code, 'unsupported-node-shape')
})
function firstRunLine(nodes: readonly AdfNode[], container: LineContainer): Result<AdfNode[]> {
const leaves = portableLeaves(nodes, container, [], 0)
if (!leaves.ok) return leaves
+12 -7
View File
@@ -3,11 +3,11 @@ import type { LineContainer } from '../line-container.ts'
import type { MarkRun } from '../emit/line-escaping.ts'
import { codeTextMarkTypes } from '../../adf/inline-nodes.ts'
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { identicalMark, mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
import { imageAside } from '../image-aside.ts'
import { insideRun, isEmphasis, type RunNode } from '../mark-spellings.ts'
import { joinsWhenEditorNormal, sameMarkWhenEditorNormal } from './editor-normal.ts'
import { joinsWhenEditorNormal } from './editor-normal.ts'
import { largestNesting } from '../../nesting.ts'
import { mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
import { partsShownText, referenceName, shownImage, shownLink, shownText, shownUrl } from '../../adf/shown-text.ts'
import { portableLineFallback, readBackPasses, type PortableLineFallback } from '../emit/inline-line.ts'
import { spellDestination } from '../commonmark/link-syntax.ts'
@@ -145,11 +145,11 @@ function highlightsInsideSharedMarks(leaves: readonly AdfNode[]): AdfNode[] {
const marks = nodeMarks(leaf)
if (marks[0]?.type === highlight) {
const held = marks.slice(1)
shared = run.length === 0 ? held : shared.filter((mark) => held.some((other) => sameMarkWhenEditorNormal(other, mark)))
shared = run.length === 0 ? held : shared.filter((mark) => held.some((other) => identicalMark(other, mark)))
run.push(leaf)
continue
}
for (const held of run) spelled.push(withMarks(held, [...shared, highlightMark, ...nodeMarks(held).slice(1).filter((mark) => !shared.some((other) => sameMarkWhenEditorNormal(other, mark)))]))
for (const held of run) spelled.push(withMarks(held, [...shared, highlightMark, ...nodeMarks(held).slice(1).filter((mark) => !shared.some((other) => identicalMark(other, mark)))]))
run = []
spelled.push(leaf)
}
@@ -229,7 +229,7 @@ function sharedRuns(marks: readonly AdfMark[], neighbour: AdfNode): number {
}
function spellableLine(leaves: AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<AdfNode[]> {
// Terminates: each fallback takes a mark off some leaf, and trimmedEdges adds none to any character.
// Terminates: withoutFallback refuses a pass that takes no mark, and trimmedEdges adds none to any character.
for (let current = leaves, pass = 1; ; pass += 1) {
const fallback = portableLineFallback(current, container, path)
if (!fallback.ok) return fallback
@@ -260,7 +260,7 @@ function withoutEmphasis(leaves: readonly AdfNode[]): AdfNode[] {
}
// The runs one pass takes back may nest, so a leaf drops its mark at each depth a run over it names.
function withoutMarks(leaves: readonly AdfNode[], runs: readonly MarkRun[]): AdfNode[] {
function withoutMarks(leaves: readonly AdfNode[], runs: readonly MarkRun[]): AdfNode[] | undefined {
const depths = new Map<number, number[]>()
for (const run of runs) {
for (let index = run.first; index <= run.last; index += 1) {
@@ -269,8 +269,13 @@ function withoutMarks(leaves: readonly AdfNode[], runs: readonly MarkRun[]): Adf
else held.push(run.depth)
}
}
return leaves.map((leaf, index) => {
const kept = leaves.map((leaf, index) => {
const dropped = depths.get(index)
return dropped === undefined ? leaf : withMarks(leaf, nodeMarks(leaf).filter((_, held) => !dropped.includes(held)))
})
return markCount(kept) < markCount(leaves) ? kept : undefined
}
function markCount(leaves: readonly AdfNode[]): number {
return leaves.reduce((count, leaf) => count + nodeMarks(leaf).length, 0)
}
+2
View File
@@ -81,6 +81,8 @@ mention or code spelled `!adf:text{text=" "}`. Joining the run across the break
`inline-line.ts` ruled worse with two comprehension panels (PR #22): a joined run that cannot be spelled needs its own fallback,
first as a set of parted breaks, then as a flag the first fallback clears.
Once a run spans a node that doesn't hold its mark, `outer` and `depth` differ. Settle which one `tryLink`'s `depth + 1` and `MarkRun.depth` mean, and keep `MarkRun.depth`'s comment true.
### 133. Write mentions, emoji, dates, statuses and inline cards as semantic HTML in `adfToHtml` and read them back in `htmlToAdf`, in place of the carry element.
A writer panel picks the elements, classes and `data-*` attributes. Where a link marks a card, both