13b - link takes the directive form where CommonMark cannot spell it, unspellable-link leaves the code list #93
+1
-1
@@ -61,7 +61,7 @@ normalizes to it through the round-trip.
|
||||
code span, so such a string still closes one.
|
||||
Where a paragraph opens with what reads as a link reference definition, which resolves before
|
||||
any inline construct binds (a `]` inside a code span or `{attrs}` counts), an opening text `[`
|
||||
escapes and an opening link's nodes ride the carry.
|
||||
escapes and an opening link takes the directive link (Marks).
|
||||
An emphasis delimiter run in text escapes where CommonMark can open **or** close with it, so
|
||||
`*not emphasis*` is `\*not emphasis\*` — no delimiter the emitter did not write reaches the
|
||||
matching below, which is what lets the emitter decide its own pairings.
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { InlineDirective } from '../../adf/inline-directives.ts'
|
||||
import { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, type LineContainer, type NodeRange } from './line-escaping.ts'
|
||||
import { carriedInline } from '../opaque-carry.ts'
|
||||
import { claimsLine, holdsNullCharacter } from '../commonmark-grammar.ts'
|
||||
import { commonMarkLink, markSpelling, spellMarkAttributes, type CommonMarkLink } from '../mark-spellings.ts'
|
||||
import { commonMarkLink, markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
||||
import { escapeUnbalanced, spellDestination } from '../link-syntax.ts'
|
||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { inlineDirective } from '../../adf/inline-directives.ts'
|
||||
@@ -257,13 +257,8 @@ function emitMarkedRun(nodes: readonly AdfNode[], mark: AdfMark, depth: number,
|
||||
if (attributes === undefined) return success({ carry: range })
|
||||
if (spelling.kind === 'code') return emitCodeSpan(nodes, depth, range, path)
|
||||
if (spelling.kind === 'emphasis') return emitEmphasis(nodes, spelling.spelling, depth, range, context)
|
||||
if (spelling.kind === 'link') {
|
||||
const href = nodeAttrs(mark)['href']
|
||||
if (typeof href !== 'string') return success({ carry: range })
|
||||
const opening = depth === 0 && index === 0 && context.openingLinkAsDirective
|
||||
const commonMark = opening ? undefined : commonMarkLink(nodeAttrs(mark), href, isBareLink(nodes, href, depth), context.bracketed)
|
||||
if (commonMark !== undefined) return emitLink(nodes, href, commonMark, depth, range, context)
|
||||
}
|
||||
const link = spelling.kind === 'link' ? emitLink(nodes, mark, depth, range, context) : undefined
|
||||
if (link !== undefined) return link
|
||||
const inner = emitRun(nodes, depth + 1, index, { ...context, bracketed: true, spansLines: false })
|
||||
if (!inner.ok) return inner
|
||||
if (inner.value.carry !== undefined) return inner
|
||||
@@ -304,12 +299,13 @@ function needsPadding(text: string): boolean {
|
||||
return text.startsWith(' ') && text.endsWith(' ') && /[^ ]/.test(text)
|
||||
}
|
||||
|
||||
function isBareLink(nodes: readonly AdfNode[], href: string, depth: number): boolean {
|
||||
const node = nodes[0]
|
||||
return nodes.length === 1 && node !== undefined && node.type === 'text' && node.text === href && nodeMarks(node).length === depth + 1
|
||||
}
|
||||
|
||||
function emitLink(nodes: readonly AdfNode[], href: string, commonMark: CommonMarkLink, depth: number, range: NodeRange, context: InlineContext): Result<Emission> {
|
||||
// `undefined` where the link takes the directive form the caller spells.
|
||||
function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range: NodeRange, context: InlineContext): Result<Emission> | undefined {
|
||||
const href = nodeAttrs(mark)['href']
|
||||
if (typeof href !== 'string') return success({ carry: range })
|
||||
const opening = depth === 0 && range.first === 0 && context.openingLinkAsDirective
|
||||
const commonMark = opening ? undefined : commonMarkLink(nodeAttrs(mark), href, nodes, depth + 1, context.bracketed)
|
||||
if (commonMark === undefined) return undefined
|
||||
if (commonMark.form === 'autolink') return success({ segments: [syntax(`<${href}>`)] })
|
||||
const inner = emitRun(nodes, depth + 1, range.first, { ...context, bracketed: true })
|
||||
if (!inner.ok) return inner
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AdfAttributes, AdfMark } from '../adf/document.ts'
|
||||
import type { AdfAttributes, AdfMark, AdfNode } from '../adf/document.ts'
|
||||
import type { AttributeVocabulary } from '../adf/attribute-vocabulary.ts'
|
||||
import type { MarkType } from '../adf/mark-attributes.ts'
|
||||
import { escapeUnbalanced, spellLinkTarget } from './link-syntax.ts'
|
||||
@@ -6,7 +6,7 @@ import { holdsDirectivePrefix, spellAttributes, spellVocabulary } from './direct
|
||||
import { holdsEntityReference } from './entity-references.ts'
|
||||
import { isAutolink } from './commonmark-grammar.ts'
|
||||
import { isMarkType, markAttributes } from '../adf/mark-attributes.ts'
|
||||
import { nodeAttrs } from '../adf/document.ts'
|
||||
import { nodeAttrs, nodeMarks } from '../adf/document.ts'
|
||||
import { vocabularyPairs } from '../adf/attribute-vocabulary.ts'
|
||||
|
||||
type Spelling = { kind: 'code' | 'directive' | 'link'; spelling?: undefined } | { kind: 'emphasis'; spelling: string }
|
||||
@@ -35,16 +35,21 @@ export function markSpelling(type: string): MarkSpelling | undefined {
|
||||
return { attributes, kind: spelling.kind }
|
||||
}
|
||||
|
||||
// spec/flavour.md, Marks. `bare`: one text node reading exactly `href`, the link innermost.
|
||||
export function commonMarkLink(attrs: AdfAttributes, href: string, bare: boolean, bracketed: boolean): CommonMarkLink | undefined {
|
||||
// spec/flavour.md, Marks. `marksInside` counts the marks the link's nodes carry within it: the emitter's depth, the parser's none.
|
||||
export function commonMarkLink(attrs: AdfAttributes, href: string, nodes: readonly AdfNode[], marksInside: number, bracketed: boolean): CommonMarkLink | undefined {
|
||||
if (Object.keys(attrs).some((key) => key !== 'href' && key !== 'title')) return undefined
|
||||
const title = attrs['title']
|
||||
const autolinkHolds = !bracketed || (!href.includes('`') && !holdsDirectivePrefix(href) && escapeUnbalanced(href, '[', ']') === href)
|
||||
if (bare && autolinkHolds && title === undefined && isAutolink(href) && !holdsEntityReference(href)) return { form: 'autolink' }
|
||||
if (isBareLink(nodes, href, marksInside) && autolinkHolds && title === undefined && isAutolink(href) && !holdsEntityReference(href)) return { form: 'autolink' }
|
||||
const target = spellLinkTarget(href, typeof title === 'string' ? title : undefined)
|
||||
return target === undefined ? undefined : { form: 'inline', target }
|
||||
}
|
||||
|
||||
function isBareLink(nodes: readonly AdfNode[], href: string, marksInside: number): boolean {
|
||||
const node = nodes[0]
|
||||
return nodes.length === 1 && node !== undefined && node.type === 'text' && node.text === href && nodeMarks(node).length === marksInside
|
||||
}
|
||||
|
||||
export function spellMarkAttributes(mark: AdfMark, vocabulary: AttributeVocabulary): string | undefined {
|
||||
const pairs = vocabularyPairs(nodeAttrs(mark), vocabulary, [])
|
||||
return pairs === undefined ? undefined : spellAttributes(spellVocabulary(pairs))
|
||||
|
||||
@@ -51,7 +51,7 @@ type SlotContent = { carry: boolean; nodes: AdfNode[] }
|
||||
|
||||
const carriedInMark = 'no mark spelling wraps an opaque carry: the carried node restores exactly, marks included'
|
||||
const imageAlone = 'an image fits only as a paragraph of its own: this one sits inside other content'
|
||||
const spellableLink = 'link takes the directive form only where CommonMark cannot spell it: this one it can'
|
||||
const spellableLink = 'link takes the directive form only where CommonMark cannot spell it: this one it can, as [text](url "title") or <url>'
|
||||
|
||||
export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer): Result<InlineContent> {
|
||||
return parseInline(source, definitions, path, container)
|
||||
@@ -190,28 +190,27 @@ function directivePiece(scan: Scan, span: DirectiveSpan, index: number): Result<
|
||||
const slot = slotContent(scan, span.content)
|
||||
if (!slot.ok) return slot
|
||||
const mark = readDirectiveMark(span.name, span.attributes, scan.path)
|
||||
if (mark !== undefined) {
|
||||
if (!mark.ok) return mark
|
||||
if (slot.value === undefined || slot.value.nodes.length === 0) {
|
||||
return failure('unsupported-node-shape', `the ${span.name} mark wraps the [content] it marks: this one wraps none`, scan.path)
|
||||
}
|
||||
if (slot.value.carry) return failure('unsupported-node-shape', carriedInMark, scan.path)
|
||||
const refused = mark.value.type === 'link' ? refuseSpellableLink(scan, mark.value, slot.value.nodes, index) : undefined
|
||||
if (refused !== undefined) return refused
|
||||
return success({ kind: 'nodes', nodes: applyMark(slot.value.nodes, mark.value) })
|
||||
}
|
||||
if (mark !== undefined) return mark.ok ? directiveMarkPiece(scan, span.name, mark.value, slot.value, index) : mark
|
||||
const node = readInlineDirectiveNode(span.name, span.attributes, slot.value?.nodes, scan.path)
|
||||
if (!node.ok) return node
|
||||
return success({ kind: 'nodes', nodes: [node.value] })
|
||||
}
|
||||
|
||||
function directiveMarkPiece(scan: Scan, name: string, mark: AdfMark, slot: SlotContent | undefined, index: number): Result<Piece> {
|
||||
if (slot === undefined || slot.nodes.length === 0) {
|
||||
return failure('unsupported-node-shape', `the ${name} mark wraps the [content] it marks: this one wraps none`, scan.path)
|
||||
}
|
||||
if (slot.carry) return failure('unsupported-node-shape', carriedInMark, scan.path)
|
||||
const refused = mark.type === 'link' ? refuseSpellableLink(scan, mark, slot.nodes, index) : undefined
|
||||
if (refused !== undefined) return refused
|
||||
return success({ kind: 'nodes', nodes: applyMark(slot.nodes, mark) })
|
||||
}
|
||||
|
||||
// spec/flavour.md, Marks. A link opening a paragraph may still need the directive form for the line it opens, which `assemble` asks the emitter.
|
||||
function refuseSpellableLink(scan: Scan, mark: AdfMark, nodes: readonly AdfNode[], index: number): Result<Piece> | undefined {
|
||||
const href = nodeAttrs(mark)['href']
|
||||
if (typeof href !== 'string') return undefined
|
||||
const only = nodes[0]
|
||||
const bare = nodes.length === 1 && only !== undefined && only.type === 'text' && only.text === href && nodeMarks(only).length === 0
|
||||
if (commonMarkLink(nodeAttrs(mark), href, bare, scan.container === undefined) === undefined) return undefined
|
||||
if (commonMarkLink(nodeAttrs(mark), href, nodes, 0, scan.container === undefined) === undefined) return undefined
|
||||
if (index !== 0 || scan.container !== 'paragraph') return failure('unsupported-node-shape', spellableLink, scan.path)
|
||||
scan.openingSpellableLink = true
|
||||
return undefined
|
||||
|
||||
@@ -970,7 +970,7 @@ test('names the mark markdown spells, never a directive', () => {
|
||||
|
||||
// spec/flavour.md, Marks: the directive link is refused wherever the emitter would pick the markdown spelling.
|
||||
test('refuses the directive link CommonMark could spell, and reads the one it could not', () => {
|
||||
const refused = 'unsupported-node-shape: link takes the directive form only where CommonMark cannot spell it: this one it can'
|
||||
const refused = 'unsupported-node-shape: link takes the directive form only where CommonMark cannot spell it: this one it can, as [text](url "title") or <url>'
|
||||
assert.equal(content(markdownToAdf('!adf:link[a]{href="/u"}\n')), refused)
|
||||
assert.equal(content(markdownToAdf('See !adf:link[a]{href="/u"}.\n')), refused)
|
||||
assert.equal(content(markdownToAdf('!adf:link[https://example.com/]{href="https://example.com/"}\n')), refused)
|
||||
|
||||
Reference in New Issue
Block a user