diff --git a/AGENTS.md b/AGENTS.md index aa1e4bb..08a92e1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -122,8 +122,9 @@ live Atlassian APIs; property-generated ADF trees; the CommonMark spec suite aga - Emphasis is spelled against CommonMark's matching, never flanking alone: a delimiter run in text escapes wherever CommonMark could open or close with it, leaving the emitter's own delimiters the only ones in play, and a pair that matching hands to another delimiter rides the carry instead. - `matchEmphasis` is a line-for-line transcription of the reference `process_emphasis` and stays one - function: split into named steps it drifts from the algorithm whose fidelity is the whole point. + `matchEmphasis` transcribes the reference `process_emphasis` line for line, and its closer walk and + opener search stay whole: broken into named steps they drift from the algorithm being faithful is + the whole point of. - A readable spelling tried ahead of a general one — the image, the pipe table, a pipe cell — returns `string | undefined`, never a `Result`: any failure is the fallback signal, and the general form owns the refusal. Refusing there refuses a document the general form spells. diff --git a/corpus/round-trip/combinations/mark-run-matching.json b/corpus/round-trip/combinations/mark-run-matching.json index cb35ec6..179aece 100644 --- a/corpus/round-trip/combinations/mark-run-matching.json +++ b/corpus/round-trip/combinations/mark-run-matching.json @@ -80,6 +80,68 @@ } ], "type": "paragraph" + }, + { + "content": [ + { + "marks": [ + { + "type": "strike" + } + ], + "text": "un", + "type": "text" + }, + { + "marks": [ + { + "type": "strike" + }, + { + "type": "em" + } + ], + "text": "a", + "type": "text" + }, + { + "marks": [ + { + "type": "strike" + }, + { + "type": "em" + }, + { + "type": "strong" + } + ], + "text": "b", + "type": "text" + }, + { + "marks": [ + { + "type": "strike" + }, + { + "type": "strong" + } + ], + "text": "c", + "type": "text" + }, + { + "marks": [ + { + "type": "strike" + } + ], + "text": "istic", + "type": "text" + } + ], + "type": "paragraph" } ], "type": "doc", diff --git a/corpus/round-trip/combinations/mark-run-matching.md b/corpus/round-trip/combinations/mark-run-matching.md index 5c9aa25..bc30a65 100644 --- a/corpus/round-trip/combinations/mark-run-matching.md +++ b/corpus/round-trip/combinations/mark-run-matching.md @@ -1,3 +1,5 @@ un:adf{json="{\"marks\":[{\"type\":\"em\"}],\"text\":\"a\",\"type\":\"text\"}"}:adf{json="{\"marks\":[{\"type\":\"em\"},{\"type\":\"strong\"}],\"text\":\"b\",\"type\":\"text\"}"}**c**istic ***a*b**:adf{json="{\"marks\":[{\"type\":\"strong\"},{\"type\":\"em\"}],\"text\":\"c\",\"type\":\"text\"}"} + +~~un~~:adf{json="{\"marks\":[{\"type\":\"strike\"},{\"type\":\"em\"}],\"text\":\"a\",\"type\":\"text\"}"}:adf{json="{\"marks\":[{\"type\":\"strike\"},{\"type\":\"em\"},{\"type\":\"strong\"}],\"text\":\"b\",\"type\":\"text\"}"}~~**c**istic~~ diff --git a/src/adf-to-markdown.test.ts b/src/adf-to-markdown.test.ts index 1977135..6a7f150 100644 --- a/src/adf-to-markdown.test.ts +++ b/src/adf-to-markdown.test.ts @@ -297,32 +297,19 @@ test('escapes a literal delimiter that would merge with an emitted one', () => { assert.equal(emitted(marked('x', { attrs: { href: 'https://example.com/' }, type: 'link' }), { text: '{}', type: 'text' }), '[x](https://example.com/){}\n') }) -test('escapes a literal delimiter run that flanks either way', () => { - const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' }) - const emitted = (...content: AdfNode[]): string => markdown(adfToMarkdown(document(paragraph(...content)))) - assert.equal(emitted({ text: 'un', type: 'text' }, marked('a* b', { type: 'em' }), { text: 'istic', type: 'text' }), 'un*a\\* b*istic\n') - assert.equal(emitted(marked('a~~ b', { type: 'strike' })), '~~a\\~~ b~~\n') - assert.equal(emitted(marked('a_ b', { type: 'em' })), '_a\\_ b_\n') - assert.equal(emitted({ text: 'a* b', type: 'text' }), 'a\\* b\n') - assert.equal(emitted({ text: '2 * 3', type: 'text' }), '2 * 3\n') - assert.equal(emitted({ text: 'snake_case_name', type: 'text' }), 'snake_case_name\n') +test('escapes a literal delimiter run that only closes', () => { + const emitted = (text: string): string => markdown(adfToMarkdown(document(paragraph({ text, type: 'text' })))) + assert.equal(emitted('a* b'), 'a\\* b\n') + assert.equal(emitted('2 * 3'), '2 * 3\n') }) -test('carries a mark run CommonMark matching pairs elsewhere', () => { +test("spells a mark run CommonMark's matching pairs as written", () => { const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' }) const emitted = (...content: AdfNode[]): string => markdown(adfToMarkdown(document(paragraph(...content)))) const em: AdfMark = { type: 'em' } const strong: AdfMark = { type: 'strong' } - assert.equal( - emitted({ text: 'un', type: 'text' }, marked('a', em), marked('b', em, strong), marked('c', strong), { text: 'istic', type: 'text' }), - 'un:adf{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\"a\\",\\"type\\":\\"text\\"}"}' + - ':adf{json="{\\"marks\\":[{\\"type\\":\\"em\\"},{\\"type\\":\\"strong\\"}],\\"text\\":\\"b\\",\\"type\\":\\"text\\"}"}**c**istic\n', - ) - assert.equal( - emitted(marked('a', strong, em), marked('b', strong), marked('c', strong, em)), - '***a*b**:adf{json="{\\"marks\\":[{\\"type\\":\\"strong\\"},{\\"type\\":\\"em\\"}],\\"text\\":\\"c\\",\\"type\\":\\"text\\"}"}\n', - ) assert.equal(emitted({ text: 'un', type: 'text' }, marked('a', em, strong), { text: 'istic', type: 'text' }), 'un***a***istic\n') + assert.equal(emitted({ text: 're', type: 'text' }, marked('structure', strong), { text: ' the code', type: 'text' }), 're**structure** the code\n') assert.equal( emitted({ text: 'un', type: 'text' }, marked('a', em), marked('b', em, strong), marked('c', em), { text: 'istic', type: 'text' }), 'un*a**b**c*istic\n', diff --git a/src/corpus.test.ts b/src/corpus.test.ts index c729a68..64f4251 100644 --- a/src/corpus.test.ts +++ b/src/corpus.test.ts @@ -72,7 +72,6 @@ function roundTripFixtures(): { name: string; path: string }[] { ) } -// One spelling for two documents is a round-trip break no parser can undo, and no parser is needed to see it. test('no two round-trip documents share one markdown spelling', () => { const spellings = new Map() for (const fixture of roundTripFixtures()) { @@ -89,7 +88,7 @@ test('no round-trip fixture repeats the document another holds', () => { const documents = new Map() for (const fixture of roundTripFixtures()) { const parsed: unknown = JSON.parse(readFileSync(fixture.path, 'utf8')) - assert.ok(isJsonValue(parsed)) + assert.ok(isJsonValue(parsed), `${fixture.name} does not hold a JSON value`) const document = serializeCanonicalJson(parsed, 'compact') assert.equal(documents.get(document), undefined, `${fixture.name} repeats the document ${documents.get(document)} holds`) documents.set(document, fixture.name) diff --git a/src/emphasis-matching.ts b/src/emphasis-matching.ts index 8792ce0..a5897d0 100644 --- a/src/emphasis-matching.ts +++ b/src/emphasis-matching.ts @@ -1,8 +1,8 @@ import { isUnicodeWhitespace } from './commonmark-grammar.ts' -export type DelimiterRun = { canClose: boolean; canOpen: boolean; character: string; length: number } +type DelimiterRun = { canClose: boolean; canOpen: boolean; character: string; length: number } -export type EmphasisPairing = { closer: Run; closerOffset: number; opener: Run; openerOffset: number; used: number } +type EmphasisPairing = { closer: Run; closerOffset: number; opener: Run; openerOffset: number; used: number } type Candidate = { head: number @@ -16,7 +16,6 @@ type Candidate = { const unicodePunctuation = /[\p{P}\p{S}]/u -// spec/flavour.md, Canonical form: CommonMark's own can-open and can-close, which `~` follows too. export function delimiterFlags(character: string, before: string, after: string): { canClose: boolean; canOpen: boolean } { const left = isLeftFlanking(before, after) const right = isRightFlanking(before, after) @@ -28,7 +27,6 @@ export function isWordCharacter(character: string): boolean { return character !== '' && !isWhitespace(character) && !isPunctuation(character) } -// Flanking decides which delimiters may pair; this decides which ones do, and a pair it leaves out reads back as another document. export function matchEmphasis(runs: readonly Run[]): EmphasisPairing[] { const pairings: EmphasisPairing[] = [] const bottoms = new Map | undefined>() diff --git a/src/markdown-escaping.ts b/src/markdown-escaping.ts index e3cb889..40ad3ec 100644 --- a/src/markdown-escaping.ts +++ b/src/markdown-escaping.ts @@ -257,9 +257,6 @@ function runLength(scan: string, index: number): number { return length } - - - function charAt(text: string, index: number): string { return index < 0 ? '' : text.charAt(index) }