From b0a94fb8967a688c2d26f2d14adca346439f4548 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 30 Oct 2026 21:00:05 +0100 Subject: [PATCH] =?UTF-8?q?Answer=20stability=20round=201:=200=20?= =?UTF-8?q?=F0=9F=94=B4,=204=20=F0=9F=9F=A1=20(0=20misread);=20cap=20the?= =?UTF-8?q?=20every=20mode=20alone,=20name=20the=20cap's=20steps,=20pin=20?= =?UTF-8?q?links=20and=20highlights=20at=20the=20cap?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/markdown/emit/adf-to-markdown.test.ts | 5 +++-- src/markdown/emit/inline-line.ts | 10 +++++++--- src/markdown/portable/adf-to-portable-markdown.test.ts | 6 +++--- src/markdown/portable/inline-reduction.test.ts | 1 + src/markdown/portable/inline-reduction.ts | 6 +++++- 5 files changed, 19 insertions(+), 9 deletions(-) diff --git a/src/markdown/emit/adf-to-markdown.test.ts b/src/markdown/emit/adf-to-markdown.test.ts index ebc6478..402f403 100644 --- a/src/markdown/emit/adf-to-markdown.test.ts +++ b/src/markdown/emit/adf-to-markdown.test.ts @@ -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', () => { diff --git a/src/markdown/emit/inline-line.ts b/src/markdown/emit/inline-line.ts index 9189a8c..2ff9550 100644 --- a/src/markdown/emit/inline-line.ts +++ b/src/markdown/emit/inline-line.ts @@ -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; 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]] } diff --git a/src/markdown/portable/adf-to-portable-markdown.test.ts b/src/markdown/portable/adf-to-portable-markdown.test.ts index c3f6216..750e7bd 100644 --- a/src/markdown/portable/adf-to-portable-markdown.test.ts +++ b/src/markdown/portable/adf-to-portable-markdown.test.ts @@ -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', () => { diff --git a/src/markdown/portable/inline-reduction.test.ts b/src/markdown/portable/inline-reduction.test.ts index b4bbe70..020e71d 100644 --- a/src/markdown/portable/inline-reduction.test.ts +++ b/src/markdown/portable/inline-reduction.test.ts @@ -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'], ], }, diff --git a/src/markdown/portable/inline-reduction.ts b/src/markdown/portable/inline-reduction.ts index a064feb..ca08eb5 100644 --- a/src/markdown/portable/inline-reduction.ts +++ b/src/markdown/portable/inline-reduction.ts @@ -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()