15 - refuse the directive link spelling no href #103
@@ -0,0 +1 @@
|
|||||||
|
unsupported-node-shape
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
See !adf:link[the docs]{title="Setup guide"} here.
|
||||||
+2
-1
@@ -473,7 +473,8 @@ the inline directive `!adf:link[text]{attrs}` only where CommonMark does not: an
|
|||||||
`href` and `title`, an `href` or `title` no canonical escape spells (a control character, a
|
`href` and `title`, an `href` or `title` no canonical escape spells (a control character, a
|
||||||
backslash, an entity reference, an angle bracket beside a space or opening a bare destination, a
|
backslash, an entity reference, an angle bracket beside a space or opening a bare destination, a
|
||||||
newline in the title), or a link opening a paragraph whose markdown spelling would read as a link
|
newline in the title), or a link opening a paragraph whose markdown spelling would read as a link
|
||||||
reference definition. A directive link CommonMark could spell is a named error.
|
reference definition. Every such spelling carries an `href`: a directive link CommonMark could
|
||||||
|
spell is a named error, and so is one spelling no `href`, the mark without one riding the carry.
|
||||||
|
|
||||||
- `border` — Attributes: `color` (string, `#rrggbb` or `#rrggbbaa`), `size` (number, 1–3).
|
- `border` — Attributes: `color` (string, `#rrggbb` or `#rrggbbaa`), `size` (number, 1–3).
|
||||||
- `code`, `em`, `strike`, `strong` — Attributes: none.
|
- `code`, `em`, `strike`, `strong` — Attributes: none.
|
||||||
|
|||||||
@@ -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 { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, type LineContainer, type NodeRange } from './line-escaping.ts'
|
||||||
import { carriedInline } from '../opaque-carry.ts'
|
import { carriedInline } from '../opaque-carry.ts'
|
||||||
import { claimsLine, holdsNullCharacter, trimTrailingSpace } from '../commonmark/grammar.ts'
|
import { claimsLine, holdsNullCharacter, trimTrailingSpace } from '../commonmark/grammar.ts'
|
||||||
import { commonMarkLink, markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
import { commonMarkLink, linkHref, markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
||||||
import { escapeUnbalanced, spellDestination } from '../commonmark/link-syntax.ts'
|
import { escapeUnbalanced, spellDestination } from '../commonmark/link-syntax.ts'
|
||||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
import { inlineDirective } from '../../adf/inline-directives.ts'
|
import { inlineDirective } from '../../adf/inline-directives.ts'
|
||||||
@@ -300,8 +300,8 @@ function needsPadding(text: string): boolean {
|
|||||||
|
|
||||||
// `undefined` where the link takes the directive form the caller spells.
|
// `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 {
|
function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range: NodeRange, context: InlineContext): Result<Emission> | undefined {
|
||||||
const href = nodeAttrs(mark)['href']
|
const href = linkHref(nodeAttrs(mark))
|
||||||
if (typeof href !== 'string') return success({ carry: range })
|
if (href === undefined) return success({ carry: range })
|
||||||
const opening = depth === 0 && range.first === 0 && context.openingLinkAsDirective
|
const opening = depth === 0 && range.first === 0 && context.openingLinkAsDirective
|
||||||
const commonMark = opening ? undefined : commonMarkLink(nodeAttrs(mark), href, nodes, depth + 1, context.bracketed)
|
const commonMark = opening ? undefined : commonMarkLink(nodeAttrs(mark), href, nodes, depth + 1, context.bracketed)
|
||||||
if (commonMark === undefined) return undefined
|
if (commonMark === undefined) return undefined
|
||||||
|
|||||||
@@ -35,6 +35,11 @@ export function markSpelling(type: string): MarkSpelling | undefined {
|
|||||||
return { attributes, kind: spelling.kind }
|
return { attributes, kind: spelling.kind }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function linkHref(attrs: AdfAttributes): string | undefined {
|
||||||
|
const href = attrs['href']
|
||||||
|
return typeof href === 'string' ? href : undefined
|
||||||
|
}
|
||||||
|
|
||||||
// spec/flavour.md, Marks. `marksInside` counts the marks the link's nodes carry within it: the emitter's depth + 1, the parser's 0.
|
// spec/flavour.md, Marks. `marksInside` counts the marks the link's nodes carry within it: the emitter's depth + 1, the parser's 0.
|
||||||
export function commonMarkLink(attrs: AdfAttributes, href: string, nodes: readonly AdfNode[], marksInside: number, bracketed: boolean): CommonMarkLink | undefined {
|
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
|
if (Object.keys(attrs).some((key) => key !== 'href' && key !== 'title')) return undefined
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { LineContainer } from '../emit/line-escaping.ts'
|
|||||||
import type { LinkDefinition } from '../commonmark/link-syntax.ts'
|
import type { LinkDefinition } from '../commonmark/link-syntax.ts'
|
||||||
import { backslashEscape, decodeTextEscapes, inlineHtmlConstruct, readBracketedAutolink, readEmailAutolink, trimTrailingSpace } from '../commonmark/grammar.ts'
|
import { backslashEscape, decodeTextEscapes, inlineHtmlConstruct, readBracketedAutolink, readEmailAutolink, trimTrailingSpace } from '../commonmark/grammar.ts'
|
||||||
import { backtickRun, closingBacktickRun } from '../commonmark/backtick-runs.ts'
|
import { backtickRun, closingBacktickRun } from '../commonmark/backtick-runs.ts'
|
||||||
import { commonMarkLink } from '../mark-spellings.ts'
|
import { commonMarkLink, linkHref } from '../mark-spellings.ts'
|
||||||
import { delimiterFlags, matchEmphasis, runLength } from '../commonmark/emphasis-matching.ts'
|
import { delimiterFlags, matchEmphasis, runLength } from '../commonmark/emphasis-matching.ts'
|
||||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
import { inlineDirective } from '../../adf/inline-directives.ts'
|
import { inlineDirective } from '../../adf/inline-directives.ts'
|
||||||
@@ -51,6 +51,7 @@ type Scan = {
|
|||||||
type SlotContent = { carry: boolean; nodes: AdfNode[] }
|
type SlotContent = { carry: boolean; nodes: AdfNode[] }
|
||||||
|
|
||||||
const carriedInMark = 'no mark spelling wraps an opaque carry: the carried node restores exactly, marks included'
|
const carriedInMark = 'no mark spelling wraps an opaque carry: the carried node restores exactly, marks included'
|
||||||
|
const hreflessLink = 'the link mark spells its href: this one spells none'
|
||||||
const imageAlone = 'an image fits only as a paragraph of its own: this one sits inside other content'
|
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, as [text](url "title") or <url>'
|
const spellableLink = 'link takes the directive form only where CommonMark cannot spell it: this one it can, as [text](url "title") or <url>'
|
||||||
|
|
||||||
@@ -208,15 +209,15 @@ function directiveMarkPiece(scan: Scan, name: string, mark: AdfMark, slot: SlotC
|
|||||||
return failure('unsupported-node-shape', `the ${name} mark wraps the [content] it marks: this one wraps none`, scan.path)
|
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)
|
if (slot.carry) return failure('unsupported-node-shape', carriedInMark, scan.path)
|
||||||
const refused = mark.type === 'link' ? refuseSpellableLink(scan, mark, slot.nodes, index) : undefined
|
const refused = mark.type === 'link' ? refuseLinkDirective(scan, mark, slot.nodes, index) : undefined
|
||||||
if (refused !== undefined) return refused
|
if (refused !== undefined) return refused
|
||||||
return success({ kind: 'nodes', nodes: applyMark(slot.nodes, mark) })
|
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.
|
// 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 {
|
function refuseLinkDirective(scan: Scan, mark: AdfMark, nodes: readonly AdfNode[], index: number): Result<Piece> | undefined {
|
||||||
const href = nodeAttrs(mark)['href']
|
const href = linkHref(nodeAttrs(mark))
|
||||||
if (typeof href !== 'string') return undefined
|
if (href === undefined) return failure('unsupported-node-shape', hreflessLink, scan.path)
|
||||||
if (commonMarkLink(nodeAttrs(mark), href, nodes, 0, 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)
|
if (index !== 0 || scan.container !== 'paragraph') return failure('unsupported-node-shape', spellableLink, scan.path)
|
||||||
scan.openingSpellableLink = true
|
scan.openingSpellableLink = true
|
||||||
|
|||||||
@@ -984,8 +984,6 @@ test('refuses the directive link CommonMark could spell, and reads the one it co
|
|||||||
assert.equal(content(markdownToAdf('| !adf:link[`]: a`]{href="/u"} |\n| --- |\n')), refused)
|
assert.equal(content(markdownToAdf('| !adf:link[`]: a`]{href="/u"} |\n| --- |\n')), refused)
|
||||||
assert.equal(content(markdownToAdf('!adf:underline[!adf:link[a]{href="/u"}]\n')), refused)
|
assert.equal(content(markdownToAdf('!adf:underline[!adf:link[a]{href="/u"}]\n')), refused)
|
||||||
assert.deepEqual(path(markdownToAdf('Part.\n\nSee !adf:link[a]{href="/u"}.\n')), ['content', 1])
|
assert.deepEqual(path(markdownToAdf('Part.\n\nSee !adf:link[a]{href="/u"}.\n')), ['content', 1])
|
||||||
const titled: AdfNode = { marks: [{ attrs: { title: 't' }, type: 'link' }], text: 'a', type: 'text' }
|
|
||||||
assert.deepEqual(content(markdownToAdf('!adf:link[a]{title=t}\n')), [{ content: [titled], type: 'paragraph' }])
|
|
||||||
const opening: AdfNode = { marks: [{ attrs: { href: '/u' }, type: 'link' }, { type: 'code' }], text: ']: a', type: 'text' }
|
const opening: AdfNode = { marks: [{ attrs: { href: '/u' }, type: 'link' }, { type: 'code' }], text: ']: a', type: 'text' }
|
||||||
assert.deepEqual(content(markdownToAdf('!adf:link[`]: a`]{href="/u"}\n')), [{ content: [opening], type: 'paragraph' }])
|
assert.deepEqual(content(markdownToAdf('!adf:link[`]: a`]{href="/u"}\n')), [{ content: [opening], type: 'paragraph' }])
|
||||||
assert.deepEqual(content(markdownToAdf('!adf:heading {level=1 localId=h}\n!adf:link[`]: a`]{href="/u"}\n!adf:/heading\n')), [
|
assert.deepEqual(content(markdownToAdf('!adf:heading {level=1 localId=h}\n!adf:link[`]: a`]{href="/u"}\n!adf:/heading\n')), [
|
||||||
@@ -993,6 +991,15 @@ test('refuses the directive link CommonMark could spell, and reads the one it co
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('names the href the directive link spells no value for', () => {
|
||||||
|
const named = 'unsupported-node-shape: the link mark spells its href: this one spells none'
|
||||||
|
assert.equal(content(markdownToAdf('!adf:link[a]\n')), named)
|
||||||
|
assert.equal(content(markdownToAdf('!adf:link[a]{title=t}\n')), named)
|
||||||
|
assert.equal(content(markdownToAdf('See !adf:link[a]{id=01a032c3-7a90-70c9-88f6-c60f710eda07}.\n')), named)
|
||||||
|
assert.equal(content(markdownToAdf('!adf:underline[!adf:link[a]{title=t}]\n')), named)
|
||||||
|
assert.deepEqual(path(markdownToAdf('Part.\n\nSee !adf:link[a]{title=t}.\n')), ['content', 1])
|
||||||
|
})
|
||||||
|
|
||||||
test('names the directive mark left without the content it wraps', () => {
|
test('names the directive mark left without the content it wraps', () => {
|
||||||
const named = 'unsupported-node-shape: the underline mark wraps the [content] it marks: this one wraps none'
|
const named = 'unsupported-node-shape: the underline mark wraps the [content] it marks: this one wraps none'
|
||||||
assert.equal(content(markdownToAdf('!adf:underline[]\n')), named)
|
assert.equal(content(markdownToAdf('!adf:underline[]\n')), named)
|
||||||
|
|||||||
Reference in New Issue
Block a user