diff --git a/corpus/round-trip/commonmark-subset/link-empty-destination.json b/corpus/round-trip/commonmark-subset/link-empty-destination.json new file mode 100644 index 0000000..9cad883 --- /dev/null +++ b/corpus/round-trip/commonmark-subset/link-empty-destination.json @@ -0,0 +1,24 @@ +{ + "content": [ + { + "content": [ + { + "marks": [ + { + "attrs": { + "href": "", + "title": "" + }, + "type": "link" + } + ], + "text": "a", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/round-trip/commonmark-subset/link-empty-destination.md b/corpus/round-trip/commonmark-subset/link-empty-destination.md new file mode 100644 index 0000000..95ed36f --- /dev/null +++ b/corpus/round-trip/commonmark-subset/link-empty-destination.md @@ -0,0 +1 @@ +[a](<> "") diff --git a/spec/flavour.md b/spec/flavour.md index 853f94b..8ae4022 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -43,10 +43,11 @@ normalizes to it through the round-trip. CommonMark admits no spelling — the end of a block, inside an ATX heading — or where the node carries an attribute, it is the inline directive. - An empty paragraph — real payloads carry them — is `::paragraph`. -- Links `[text](url)`; `<…>` around a destination containing spaces; title in double quotes. A - backslash escapes a parenthesis the destination leaves unbalanced, and a quote inside the title; - a balanced pair stays bare. `` autolink form only when the text equals the destination and - the destination is a valid CommonMark autolink (absolute URI). +- Links `[text](url)`; `<…>` around a destination containing spaces, `<>` an empty one beside a + title; title in double quotes. A backslash escapes a parenthesis the destination leaves + unbalanced, and a quote inside the title; a balanced pair stays bare. `` autolink form only + when the text equals the destination and the destination is a valid CommonMark autolink + (absolute URI). - Paragraphs on one line — no soft wrapping; a soft line break in input becomes a single space. - Entity references in input decode to their characters; output backslash-escapes only where text would otherwise parse as syntax, scanning the assembled line rather than each text node: escape diff --git a/src/markdown/emit/inline-line.ts b/src/markdown/emit/inline-line.ts index 71aaaa1..807357e 100644 --- a/src/markdown/emit/inline-line.ts +++ b/src/markdown/emit/inline-line.ts @@ -12,7 +12,7 @@ import { markSpelling, spellMarkAttributes } from '../mark-spellings.ts' import { nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts' import { sameMark } from '../../adf/editor-normal.ts' import { slotLineEndingFault, spellLeafDirective } from '../directive-syntax.ts' -import { spellDestination, spellTitle } from '../link-syntax.ts' +import { spellDestination, spellLinkTarget } from '../link-syntax.ts' import { spellInlineNodeAttributes } from './inline-directive-spelling.ts' import { spellTextDirective } from '../text-directive.ts' @@ -285,13 +285,11 @@ function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range const node = nodes[0] const bare = nodes.length === 1 && node !== undefined && node.type === 'text' && node.text === href && nodeMarks(node).length === depth + 1 if (bare && title === undefined && isAutolink(href) && !holdsEntityReference(href)) return success({ segments: [syntax(`<${href}>`)] }) - const destination = spellDestination(href, path) - if (!destination.ok) return destination - const spelledTitle = typeof title === 'string' ? spellTitle(title, path) : success('') - if (!spelledTitle.ok) return spelledTitle + const target = spellLinkTarget(href, typeof title === 'string' ? title : undefined, path) + if (!target.ok) return target const inner = emitRun(nodes, depth + 1, range.first, { ...context, bracketed: true }) if (!inner.ok) return inner if (inner.value.carry !== undefined) return inner - return success({ segments: [syntax('['), ...inner.value.segments, syntax(`](${destination.value}${spelledTitle.value})`)] }) + return success({ segments: [syntax('['), ...inner.value.segments, syntax(`](${target.value})`)] }) } diff --git a/src/markdown/link-syntax.ts b/src/markdown/link-syntax.ts index 488011e..7be2a26 100644 --- a/src/markdown/link-syntax.ts +++ b/src/markdown/link-syntax.ts @@ -106,7 +106,15 @@ export function spellDestination(href: string, path: ConvertErrorPath): Result { +export function spellLinkTarget(href: string, title: string | undefined, path: ConvertErrorPath): Result { + const destination = spellDestination(href, path) + if (!destination.ok || title === undefined) return destination + const spelledTitle = spellTitle(title, path) + if (!spelledTitle.ok) return spelledTitle + return success(`${destination.value === '' ? '<>' : destination.value}${spelledTitle.value}`) +} + +function spellTitle(title: string, path: ConvertErrorPath): Result { if (/[\n\r\\]/.test(title)) { return failure('unspellable-link', 'no canonical escape spells a backslash or newline in a link title', path) }