diff --git a/src/markdown/emit/adf-to-markdown.test.ts b/src/markdown/emit/adf-to-markdown.test.ts index ece6199..0707fc4 100644 --- a/src/markdown/emit/adf-to-markdown.test.ts +++ b/src/markdown/emit/adf-to-markdown.test.ts @@ -502,6 +502,8 @@ test('parts a nested list the tight spelling would swallow from the block above test('refuses marks and attributes nested deeper than the emitter carries', () => { const marks: AdfMark[] = Array.from({ length: 600 }, (_, index) => ({ type: index % 2 === 0 ? 'em' : 'strong' })) assert.equal(code(adfToLosslessMarkdown(document(paragraph({ marks, text: 'x', type: 'text' })))), 'unsupported-nesting-depth') + const deepest: AdfMark[] = Array.from({ length: 20_000 }, (_, index) => ({ type: index % 2 === 0 ? 'em' : 'strong' })) + assert.equal(code(adfToLosslessMarkdown(document(paragraph({ marks: deepest, text: 'x', type: 'text' }, { type: 'hardBreak' }, { marks: deepest, text: 'y', 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 => @@ -923,7 +925,7 @@ test('spells an emphasis or link run once across the hard breaks between two nod assert.equal(readsBack(document(paragraph({ text: 'x', type: 'text' }, marked('.', em), hardBreak, marked('b', em)))), 'x!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\".\\",\\"type\\":\\"text\\"}"}\\\n_b_\n') }) -test('spells an emphasis run once across the code spans, inline nodes and inline media between two nodes holding the mark', () => { +test('spells an emphasis run once across the code spans, mentions, dates and inline media between two nodes holding the mark', () => { const strong: AdfMark = { type: 'strong' } const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' }) const mention: AdfNode = { attrs: { id: 'x', text: '@A' }, type: 'mention' } @@ -934,12 +936,13 @@ test('spells an emphasis run once across the code spans, inline nodes and inline assert.equal(readsBack(document(paragraph(marked('a ', strong), marked('c', { type: 'code' }), mention, { type: 'hardBreak' }, marked(' b', strong)))), '**a `c`!adf:mention[@A]{id=x}\\\n!adf:text{text=" "}b**\n') }) -test('spells a link run once across the inline nodes between two nodes holding it, and parts it at code and inline media', () => { +test('spells a link run once across the mentions and inline cards between two nodes holding it, and parts it at code and inline media that do not hold it', () => { const link: AdfMark = { attrs: { href: '/u' }, type: 'link' } const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' }) assert.equal(readsBack(document(paragraph(marked('a ', link), { attrs: { id: 'x', text: '@A' }, type: 'mention' }, marked(' b', link)))), '[a !adf:mention[@A]{id=x} b](/u)\n') assert.equal(readsBack(document(paragraph(marked('a', link), { attrs: { url: 'https://c/' }, type: 'inlineCard' }, marked('b', link)))), '[a!adf:inlineCard{url="https://c/"}b](/u)\n') assert.equal(readsBack(document(paragraph(marked('a', link), marked('c', { type: 'code' }), marked('b', link)))), '[a](/u)`c`[b](/u)\n') + assert.equal(readsBack(document(paragraph(marked('a', link), { attrs: { id: 'm' }, type: 'mediaInline' }, marked('b', link)))), '[a](/u)!adf:mediaInline{id=m}[b](/u)\n') }) test('carries a hard break holding marks, which Atlassian\'s schema withholds', () => { diff --git a/src/markdown/emit/inline-line.ts b/src/markdown/emit/inline-line.ts index f1e3121..9f48ddf 100644 --- a/src/markdown/emit/inline-line.ts +++ b/src/markdown/emit/inline-line.ts @@ -69,7 +69,6 @@ export function portableLineFallback(nodes: readonly AdfNode[], container: LineC const emission = lineSegments(nodes, container, path, fallbacks) if (!emission.ok) return emission if (fallbacks.carried.size > 0 || emission.value.carries !== undefined) return failure('unsupported-node-shape', 'an inline node on a portable line has no spelling but the carry', path) - if (spannedLine(nodes, container, path, fallbacks) !== undefined) return success(undefined) const verdict = lineVerdict(emission.value.segments, container, 'portable', { kind: 'drop', nodes }) return success(verdict.kind === 'line' ? undefined : verdict) } @@ -117,12 +116,14 @@ export function linePass(nodes: readonly AdfNode[], container: LineContainer, pa return success(segments === undefined ? { fallback: carries } : attemptLine(segments, container, fallbacks.flavour)) } -// spec/flavour.md, Marks: a run spans the nodes its mark skips where the line reads back so; the line parted at them is the one whose fallbacks a pass takes. +// spec/flavour.md, Marks: a pass keeps the spanned line only where it needs no fallback; otherwise it writes the parted line and takes that line's fallbacks. function spannedLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, fallbacks: LineFallbacks): LineAttempt | undefined { const spanned = spanSkippedNodes(nodes, (node, index) => carries(node, fallbacks.carried, index)) if (spanned === nodes) return undefined const emission = lineSegments(spanned, container, path, fallbacks) if (!emission.ok || emission.value.segments === undefined) return undefined + // A span that leaves whitespace at a line edge spells it `!adf:text`, which portable markdown lacks. + if (fallbacks.flavour === 'portable' && emission.value.segments.some((segment) => segment.escaping === 'none' && segment.text.startsWith('!adf:'))) return undefined const attempt = attemptLine(emission.value.segments, container, fallbacks.flavour) return attempt.fallback === undefined ? attempt : undefined } diff --git a/src/markdown/portable/adf-to-portable-markdown.test.ts b/src/markdown/portable/adf-to-portable-markdown.test.ts index 85def69..73c19ab 100644 --- a/src/markdown/portable/adf-to-portable-markdown.test.ts +++ b/src/markdown/portable/adf-to-portable-markdown.test.ts @@ -427,6 +427,8 @@ test('spells an emphasis once across the code spans and hard breaks between two assert.equal(portable(paragraph(text('One', em), { type: 'hardBreak' }, text('Two', em))), '_One\\\nTwo_\n') assert.equal(portable(paragraph(text('a', link('/u')), { type: 'hardBreak' }, text('b', link('/u')))), '[a\\\nb](/u)\n') assert.equal(portable(paragraph(text('Ping ', strong), { attrs: { id: 'x', text: '@A' }, type: 'mention' }, text(' now', strong))), '**Ping** @A **now**\n') + assert.equal(portable(paragraph(text('a', link('/u')), { type: 'hardBreak' }, text(' b', link('/u')))), '[a](/u)\\\n[ b](/u)\n') + assert.equal(portable(paragraph(text('a\n b', link('/u')))), '[a](/u)\\\n[ b](/u)\n') }) test('breaks a line at a newline and trims whitespace at every edge CommonMark strips', () => {