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 🔴, 4 🟡 (0 misread); cap the every mode alone, name the cap's steps, pin links and highlights at the cap

This commit is contained in:
2026-10-30 21:00:05 +01:00
committed by Lillem4n
parent d0b2e22aa8
commit b0a94fb896
5 changed files with 19 additions and 9 deletions
+3 -2
View File
@@ -452,12 +452,13 @@ test('carries the mark spelling that cannot close where it sits, and keeps the l
assert.equal(emitted(marked('^', { type: 'strike' }), b, marked('c', { type: 'strike' })), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strike\\"}],\\"text\\":\\"^\\",\\"type\\":\\"text\\"}"}b~~c~~\n')
})
test('carries every node holding em, strong or strike in a line still misread after 32 passes', () => {
test('carries every node holding em, strike or strong, and no other, in a line still misread after 32 passes', () => {
const marked = (text: string, ...types: string[]): AdfNode => ({ marks: types.map((type) => ({ type })), text, type: 'text' })
const carry = (node: AdfNode): string => `!adf:carry{json="${JSON.stringify(node).replaceAll('"', '\\"')}"}`
const repeat = [marked(' ~~ ', 'em', 'strong'), marked(')', 'em', 'strike', 'strong'), { text: 'a', type: 'text' }, marked('.unun', 'strike', 'strong')]
const written = repeat.map((node) => (node.marks === undefined ? 'a' : carry(node))).join('')
assert.equal(markdown(adfToLosslessMarkdown(document(paragraph(...Array.from({ length: 40 }, () => repeat).flat())))), `${written.repeat(40)}\n`)
const line = [...Array.from({ length: 40 }, () => repeat).flat(), { text: ' ', type: 'text' }, { marks: [{ attrs: { href: 'u' }, type: 'link' }], text: 'b', type: 'text' }]
assert.equal(markdown(adfToLosslessMarkdown(document(paragraph(...line)))), `${written.repeat(40)} [b](u)\n`)
})
test('refuses a node carrying one mark type twice', () => {
+7 -3
View File
@@ -27,7 +27,7 @@ type Emission = { carries: [NodeRange, ...NodeRange[]]; segments?: undefined } |
type Fallback = [NodeRange, ...NodeRange[]] | 'opening-link'
// How many fallbacks one pass applies: `every`, or the `first` alone; a property checks that both write the same line.
// How many fallbacks one pass applies: `every`, or the `first` alone and with no cap; a property checks that both write the same line.
export type FallbacksPerPass = 'every' | 'first'
type InlineContext = {
@@ -47,7 +47,7 @@ type LineAttempt = { fallback: Fallback; line?: undefined } | { fallback?: undef
type LineFallbacks = { carried: Set<number>; flavour: WrittenFlavour; openingLinkAsDirective: boolean }
// docs/decisions.md §A line reads back at most 32 times, then takes back every emphasis run.
// docs/decisions.md §After 32 passes, a line carries or drops every em, strike and strong mark.
export const readBackPasses = 32
export type PortableLineFallback = { kind: 'opening-link' } | { kind: 'unspellable-run'; runs: [MarkRun, ...MarkRun[]] }
@@ -103,10 +103,14 @@ function emitLine(nodes: readonly AdfNode[], container: LineContainer, path: Con
if (attempt.fallback === undefined) return success({ line: attempt.line, openingLinkAsDirective: fallbacks.openingLinkAsDirective, segments: attempt.segments })
const taken = takeFallback(fallbacks, takenThisPass(attempt.fallback, perPass), path)
if (!taken.ok) return taken
if (pass === readBackPasses) for (const [index, node] of nodes.entries()) if (nodeMarks(node).some(isEmphasis)) fallbacks.carried.add(index)
if (perPass === 'every' && pass === readBackPasses) carryEveryEmphasis(fallbacks, nodes)
}
}
function carryEveryEmphasis(fallbacks: LineFallbacks, nodes: readonly AdfNode[]): void {
for (const [index, node] of nodes.entries()) if (nodeMarks(node).some(isEmphasis)) fallbacks.carried.add(index)
}
function takenThisPass(fallback: Fallback, perPass: FallbacksPerPass): Fallback {
return fallback === 'opening-link' || perPass === 'every' ? fallback : [fallback[0]]
}
@@ -401,11 +401,11 @@ test('drops the mark of the run that cannot close where it sits, and keeps the l
assert.equal(portable(paragraph(text('~', highlight), text('b'), text('日本', highlight))), '~b==日本==\n')
})
test('drops every em, strong and strike mark in a line still misread after 32 passes', () => {
test('drops every em, strike and strong mark, and keeps every other, in a line still misread after 32 passes', () => {
const strike: AdfMark = { type: 'strike' }
const repeat = [text(' ~~ ', em, strong), text(')', em, strike, strong), text('a'), text('.unun', strike, strong)]
const line = Array.from({ length: 40 }, () => repeat).flat()
assert.equal(portable(paragraph(...line)), `${Array.from({ length: 40 }, () => '~~ )a.unun').join(' ')}\n`)
const line = [...Array.from({ length: 40 }, () => repeat).flat(), text(' '), text('b', { attrs: { href: 'u' }, type: 'link' }), text(' '), text('c', { type: 'backgroundColor' })]
assert.equal(portable(paragraph(...line)), `${Array.from({ length: 40 }, () => '~~ )a.unun').join(' ')} [b](u) ==c==\n`)
})
test('gives up every highlight run whose own delimiter cannot flank in one fallback, so a line of them re-emits once however many it holds', () => {
@@ -23,6 +23,7 @@ test('reduces a line alike dropping every run a pass may or the first alone', ()
[[...[1, 2, 3].flatMap(() => [{ marks: [{ type: 'strike' }], text: 'b(', type: 'text' }, { marks: [{ type: 'em' }], text: 'b(', type: 'text' }]), { text: 'z', type: 'text' }], 'paragraph'],
[[...[1, 2, 3].flatMap(() => [{ marks: [{ type: 'em' }, { type: 'strong' }], text: '(', type: 'text' }, { marks: [{ type: 'em' }], text: '(', type: 'text' }]), { text: 'a', type: 'text' }], 'paragraph'],
[[...[1, 2, 3, 4, 5].flatMap(() => [{ marks: [{ type: 'em' }, { type: 'strong' }], text: '~** ', type: 'text' }, { marks: [{ type: 'em' }], text: '- _', type: 'text' }]), { text: 'r', type: 'text' }], 'table-cell'],
[[{ marks: [{ type: 'em' }, { type: 'strike' }], text: '-', type: 'text' }, { text: 'a(.', type: 'text' }, ...[1, 2, 3, 4, 5].flatMap(() => [{ marks: [{ type: 'strong' }, { type: 'strike' }, { type: 'em' }], text: '0.-', type: 'text' }, { marks: [{ type: 'link' }], text: 'un', type: 'text' }, { marks: [{ type: 'strike' }, { type: 'em' }, { type: 'strong' }], text: '=', type: 'text' }]), { marks: [{ type: 'strong' }, { type: 'em' }], text: '[', type: 'text' }, { text: '`=', type: 'text' }], 'table-cell'],
[[...[1, 2, 3].flatMap(() => [{ marks: [{ type: 'em' }, { type: 'strong' }], text: ' ~~ ', type: 'text' }, { marks: [{ type: 'em' }, { type: 'strike' }, { type: 'strong' }], text: ')', type: 'text' }]), { text: 'a', type: 'text' }, { marks: [{ type: 'strike' }, { type: 'strong' }], text: '.unun', type: 'text' }], 'heading'],
],
},
+5 -1
View File
@@ -226,7 +226,7 @@ function spellableLine(leaves: AdfNode[], container: LineContainer, path: Conver
if (fallback.value === undefined) return success(current)
const fixed = withoutFallback(current, fallback.value, perPass)
if (fixed === undefined) return failure('unsupported-node-shape', 'a portable line keeps a spelling that dropping a mark does not change', path)
current = trimmedEdges(pass === readBackPasses ? fixed.map((leaf) => withMarks(leaf, nodeMarks(leaf).filter((mark) => !isEmphasis(mark)))) : fixed)
current = trimmedEdges(perPass === 'every' && pass === readBackPasses ? withoutEmphasis(fixed) : fixed)
}
}
@@ -241,6 +241,10 @@ function withoutFallback(leaves: readonly AdfNode[], fallback: PortableLineFallb
return spans ? leaves.map((leaf, index) => (index > last ? leaf : withMarks(leaf, nodeMarks(leaf).filter((held) => held.type !== 'code')))) : undefined
}
function withoutEmphasis(leaves: readonly AdfNode[]): AdfNode[] {
return leaves.map((leaf) => withMarks(leaf, nodeMarks(leaf).filter((mark) => !isEmphasis(mark))))
}
// The runs may nest, so a leaf drops the mark at every depth a run holds it.
function withoutMarks(leaves: readonly AdfNode[], runs: readonly MarkRun[]): AdfNode[] {
const depths = new Map<number, number[]>()