16 - no link wraps a link #104
@@ -55,7 +55,9 @@ Round-trip equality is a property tested over a corpus, not a claim made in pros
|
|||||||
spells the mark; `!adf:link[text]{attrs}` where it does not — an attribute CommonMark cannot
|
spells the mark; `!adf:link[text]{attrs}` where it does not — an attribute CommonMark cannot
|
||||||
hold, an `href` or `title` no canonical escape spells, a paragraph opening whose CommonMark
|
hold, an `href` or `title` no canonical escape spells, a paragraph opening whose CommonMark
|
||||||
spelling would read as a link reference definition — and a directive link CommonMark could spell
|
spelling would read as a link reference definition — and a directive link CommonMark could spell
|
||||||
is refused (the maintainer, 2026-09-13).
|
is refused (the maintainer, 2026-09-13). No link wraps a link — the bracket form goes literal,
|
||||||
|
the directive form refused — which is CommonMark's prose where its reference implementation
|
||||||
|
nests one `<a>` in another (the maintainer, 2026-09-17).
|
||||||
- Identity-bearing nodes carry their ids in attributes; a document is only portable within its
|
- Identity-bearing nodes carry their ids in attributes; a document is only portable within its
|
||||||
site — accepted.
|
site — accepted.
|
||||||
- The HTML dialect mirrors this: semantic elements, stable `adf-*` classes, `data-*` for what HTML
|
- The HTML dialect mirrors this: semantic elements, stable `adf-*` classes, `data-*` for what HTML
|
||||||
|
|||||||
+9
-1
@@ -6,7 +6,7 @@ Directives moved under the `!adf:` prefix. `0.2.0` reads `0.1.0`'s spelling with
|
|||||||
turning each directive into text and each carried node into an `adf` code block. Before `0.2.0`
|
turning each directive into text and each carried node into an `adf` code block. Before `0.2.0`
|
||||||
reads any `0.1.0` markdown, convert what is stored or in flight (an open editor, a queue) with the
|
reads any `0.1.0` markdown, convert what is stored or in flight (an open editor, a queue) with the
|
||||||
recipe below, and rewrite markdown your code writes or matches (templates, prompts, patterns) by
|
recipe below, and rewrite markdown your code writes or matches (templates, prompts, patterns) by
|
||||||
the spelling table. Stored ADF needs no change.
|
the tables below. Stored ADF needs no change.
|
||||||
|
|
||||||
### Convert markdown
|
### Convert markdown
|
||||||
|
|
||||||
@@ -47,6 +47,14 @@ function migrateMarkdown(stored: string) {
|
|||||||
A colon run and `:name[` are plain text now, and `adf` an ordinary code block language; text
|
A colon run and `:name[` are plain text now, and `adf` an ordinary code block language; text
|
||||||
holding an unescaped `!adf:` and a `carry` fence are claimed instead.
|
holding an unescaped `!adf:` and a `carry` fence are claimed instead.
|
||||||
|
|
||||||
|
### Readings
|
||||||
|
|
||||||
|
Markdown the spelling table leaves alone, which `0.2.0` reads as a different document.
|
||||||
|
|
||||||
|
| Input | `0.1.0` | `0.2.0` |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| a link whose text already holds one (`[a<https://example.com/>b](/v)`) | marks every node the inner link does not, splitting the outer link around it | leaves the outer brackets literal text; write the pieces as separate links to keep them |
|
||||||
|
|
||||||
### Error codes
|
### Error codes
|
||||||
|
|
||||||
`unspellable-link` leaves `ConvertErrorCode`: a `switch` naming it stops compiling, and the link
|
`unspellable-link` leaves `ConvertErrorCode`: a `switch` naming it stops compiling, and the link
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ Parsing — `markdownToAdf`, and `htmlToAdf` at `0.2.0`:
|
|||||||
| `malformed-pipe-table` | a pipe row that is no pipe table — a missing or ragged `---` delimiter row, an alignment colon in it, or a row not opening with a pipe | open every row with a pipe and give the delimiter row the header's cell count; to keep the lines literal text instead, escape the leading pipe of every one — escaping a single row leaves the next to open a fresh table and fail the same way |
|
| `malformed-pipe-table` | a pipe row that is no pipe table — a missing or ragged `---` delimiter row, an alignment colon in it, or a row not opening with a pipe | open every row with a pipe and give the delimiter row the header's cell count; to keep the lines literal text instead, escape the leading pipe of every one — escaping a single row leaves the next to open a fresh table and fail the same way |
|
||||||
| `unknown-directive-name` | a directive whose name is no node or mark this version spells | check the name in `spec/flavour.md`, or escape the prefix as `\!adf:`; the spelling itself is well formed, so a later minor may give the name meaning |
|
| `unknown-directive-name` | a directive whose name is no node or mark this version spells | check the name in `spec/flavour.md`, or escape the prefix as `\!adf:`; the spelling itself is well formed, so a later minor may give the name meaning |
|
||||||
| `unmappable-html` | the markdown holds a raw HTML tag, comment or processing instruction | remove it or write it in the flavour — ADF holds no raw-HTML node, and the element mapping lands at `0.2.0` |
|
| `unmappable-html` | the markdown holds a raw HTML tag, comment or processing instruction | remove it or write it in the flavour — ADF holds no raw-HTML node, and the element mapping lands at `0.2.0` |
|
||||||
| `unmappable-image` | an image sits inside other content, or carries a title | give the image a paragraph of its own and drop the title |
|
| `unmappable-image` | an image sits inside other content that is not another image's description, or carries a title | give the image a paragraph of its own and drop the title |
|
||||||
|
|
||||||
Emitting — `adfToMarkdown`, and `adfToHtml` at `0.2.0`:
|
Emitting — `adfToMarkdown`, and `adfToHtml` at `0.2.0`:
|
||||||
|
|
||||||
@@ -111,15 +111,19 @@ emit refuses:
|
|||||||
- Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML below, with three
|
- Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML below, with three
|
||||||
carve-outs — literal text matching directive, pipe-table or strikethrough syntax is claimed
|
carve-outs — literal text matching directive, pipe-table or strikethrough syntax is claimed
|
||||||
(escapable — `spec/flavour.md`) — and one gap: a CommonMark image fits only as its own
|
(escapable — `spec/flavour.md`) — and one gap: a CommonMark image fits only as its own
|
||||||
title-less paragraph; mid-text and titled images are error results. Converting back yields the
|
title-less paragraph; mid-text and titled images are error results, save an image inside
|
||||||
|
another's description, which flattens into the alt text. Converting back yields the
|
||||||
library's canonical spelling, which round-trips byte-identically — where it converts back at
|
library's canonical spelling, which round-trips byte-identically — where it converts back at
|
||||||
all: a parse succeeding is no promise of that, so keep the source until the way back succeeds.
|
all: a parse succeeding is no promise of that, so keep the source until the way back succeeds.
|
||||||
``` ` `` ` ``` reads cleanly and then refuses.
|
``` ` `` ` ``` reads cleanly and then refuses.
|
||||||
- Three CommonMark spellings parse without an error and build a document the reference renders
|
- Four CommonMark spellings parse without an error and build a document the reference
|
||||||
differently: `[](/url)` and `[]()` stay literal text against CommonMark's empty link, a list
|
implementation renders differently: `[](/url)` and `[]()` stay literal text against CommonMark's
|
||||||
continuing past a marker change stays one list against CommonMark's two, and a shortcut
|
empty link, a list continuing past a marker change stays one list against CommonMark's two, a
|
||||||
reference matching its definition only under Unicode case folding stays unresolved. Each is
|
shortcut reference matching its definition only under Unicode case folding stays unresolved, and
|
||||||
pinned `pending` in `corpus/commonmark-spec/exceptions.json`.
|
a link whose text holds an autolink keeps the inner link and leaves the outer brackets literal
|
||||||
|
text, which the spec requires and the reference itself breaks, nesting one `<a>` in the other.
|
||||||
|
The first three are pinned `pending` in `corpus/commonmark-spec/exceptions.json`; the suite
|
||||||
|
holds no example of the fourth.
|
||||||
- Raw HTML in markdown input is an error result, never a silent drop — a tag, a comment and a
|
- Raw HTML in markdown input is an error result, never a silent drop — a tag, a comment and a
|
||||||
processing instruction alike. ADF holds no raw-HTML node; the element mapping ships at `0.2.0`.
|
processing instruction alike. ADF holds no raw-HTML node; the element mapping ships at `0.2.0`.
|
||||||
- Not every document converts back: `adfToMarkdown` is partial on valid ADF — a text node holding
|
- Not every document converts back: `adfToMarkdown` is partial on valid ADF — a text node holding
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
unsupported-node-shape
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
See !adf:link[<https://example.com/docs>]{collection=contentId-98237 href="/docs"} here.
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "[",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "https://example.com/"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "https://example.com/",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "](/v)",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "[a",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "https://example.com/b"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "https://example.com/b",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "c](/v)",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "[",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"collection": "contentId-98237",
|
||||||
|
"href": "/docs"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "the docs",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "](/v)",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "[",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "/u"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "a",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "](/v)",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
[<https://example.com/>](/v)
|
||||||
|
|
||||||
|
[a<https://example.com/b>c](/v)
|
||||||
|
|
||||||
|
[!adf:link[the docs]{collection=contentId-98237 href="/docs"}](/v)
|
||||||
|
|
||||||
|
[[a](/u)](/v)
|
||||||
+3
-1
@@ -474,7 +474,9 @@ the inline directive `!adf:link[text]{attrs}` only where CommonMark does not: an
|
|||||||
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. Every such spelling carries an `href`: a directive link CommonMark could
|
reference definition. Every such spelling carries an `href`: a directive link CommonMark could
|
||||||
spell is a named error, and so is one spelling none.
|
spell is a named error, and so is one spelling none. No link wraps a link at any nesting, which is
|
||||||
|
CommonMark's own rule: a `[text]` already holding one leaves the outer brackets literal text, and
|
||||||
|
the directive form, open to no literal reading, is a named error.
|
||||||
|
|
||||||
- `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.
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ type Run = { canClose: boolean; canOpen: boolean; character: string; index: numb
|
|||||||
// `container` is `undefined` inside a directive's content slot, the emitter's `bracketed`.
|
// `container` is `undefined` inside a directive's content slot, the emitter's `bracketed`.
|
||||||
type Scan = {
|
type Scan = {
|
||||||
container: LineContainer | undefined
|
container: LineContainer | undefined
|
||||||
|
// Pieces below this have been walked for openers to deactivate: an image close folds the link-marked piece into alt text, leaving this the only record that the brackets around it are doomed.
|
||||||
|
deactivatedBefore: number
|
||||||
definitions: LinkDefinitions
|
definitions: LinkDefinitions
|
||||||
openingSpellableLink: boolean
|
openingSpellableLink: boolean
|
||||||
path: ConvertErrorPath
|
path: ConvertErrorPath
|
||||||
@@ -53,6 +55,7 @@ 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 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 linkInLink = 'no link wraps a link: the [content] this one marks already holds one'
|
||||||
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>'
|
||||||
|
|
||||||
export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer): Result<InlineContent> {
|
export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer): Result<InlineContent> {
|
||||||
@@ -60,7 +63,7 @@ export function parseInlineContent(source: string, definitions: LinkDefinitions,
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseInline(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer | undefined, spans: NestedSpans): Result<InlineContent> {
|
function parseInline(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer | undefined, spans: NestedSpans): Result<InlineContent> {
|
||||||
const scan: Scan = { container, definitions, openingSpellableLink: false, path, pending: '', pieces: [], source, spans }
|
const scan: Scan = { container, deactivatedBefore: 0, definitions, openingSpellableLink: false, path, pending: '', pieces: [], source, spans }
|
||||||
let index = 0
|
let index = 0
|
||||||
while (index < source.length) {
|
while (index < source.length) {
|
||||||
switch (source.charAt(index)) {
|
switch (source.charAt(index)) {
|
||||||
@@ -218,6 +221,7 @@ function directiveMarkPiece(scan: Scan, name: string, mark: AdfMark, slot: SlotC
|
|||||||
function refuseLinkDirective(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 = linkHref(nodeAttrs(mark))
|
const href = linkHref(nodeAttrs(mark))
|
||||||
if (href === undefined) return failure('unsupported-node-shape', hreflessLink, scan.path)
|
if (href === undefined) return failure('unsupported-node-shape', hreflessLink, scan.path)
|
||||||
|
if (marksLink(nodes)) return failure('unsupported-node-shape', linkInLink, 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
|
||||||
@@ -264,6 +268,15 @@ function holdsImage(pieces: readonly Piece[]): boolean {
|
|||||||
return pieces.some((piece) => piece.kind === 'image')
|
return pieces.some((piece) => piece.kind === 'image')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A carry rides its own piece: the carried node's marks restore with it rather than riding a spelling, so the guard below answers for it.
|
||||||
|
function holdsLink(pieces: readonly Piece[]): boolean {
|
||||||
|
return pieces.some((piece) => piece.kind === 'nodes' && marksLink(piece.nodes))
|
||||||
|
}
|
||||||
|
|
||||||
|
function marksLink(nodes: readonly AdfNode[]): boolean {
|
||||||
|
return nodes.some((node) => nodeMarks(node).some((mark) => mark.type === 'link'))
|
||||||
|
}
|
||||||
|
|
||||||
function readDelimiterRun(scan: Scan, index: number): number {
|
function readDelimiterRun(scan: Scan, index: number): number {
|
||||||
const character = scan.source.charAt(index)
|
const character = scan.source.charAt(index)
|
||||||
const length = runLength(scan.source, index)
|
const length = runLength(scan.source, index)
|
||||||
@@ -340,29 +353,47 @@ function resolveTarget(scan: Scan, bracket: Bracket, index: number): { definitio
|
|||||||
return { definition, length: label?.length ?? 0 }
|
return { definition, length: label?.length ?? 0 }
|
||||||
}
|
}
|
||||||
|
|
||||||
// `false` where the link text is empty: the mark has no node to ride, so the brackets stay text.
|
|
||||||
function closeLink(scan: Scan, at: number, inner: readonly Piece[], definition: LinkDefinition): Result<boolean> {
|
function closeLink(scan: Scan, at: number, inner: readonly Piece[], definition: LinkDefinition): Result<boolean> {
|
||||||
|
// Ahead of the guards below: brackets going literal put the image and the carry inside no mark for either to refuse.
|
||||||
|
if (holdsLink(inner)) {
|
||||||
|
deactivateOpeners(scan, at)
|
||||||
|
return success(false)
|
||||||
|
}
|
||||||
if (holdsImage(inner)) return failure('unmappable-image', imageAlone, scan.path)
|
if (holdsImage(inner)) return failure('unmappable-image', imageAlone, scan.path)
|
||||||
if (holdsCarry(inner)) return failure('unsupported-node-shape', carriedInMark, scan.path)
|
if (holdsCarry(inner)) return failure('unsupported-node-shape', carriedInMark, scan.path)
|
||||||
const resolved = resolveNodes(inner, scan.path)
|
const resolved = resolveNodes(inner, scan.path)
|
||||||
if (!resolved.ok) return resolved
|
if (!resolved.ok) return resolved
|
||||||
const nodes = resolved.value
|
const nodes = resolved.value
|
||||||
|
// An empty link text gives the mark no node to ride, so the brackets stay text.
|
||||||
if (nodes.length === 0) return success(false)
|
if (nodes.length === 0) return success(false)
|
||||||
const attrs = definition.title === undefined ? { href: definition.destination } : { href: definition.destination, title: definition.title }
|
const attrs = definition.title === undefined ? { href: definition.destination } : { href: definition.destination, title: definition.title }
|
||||||
scan.pieces.length = at
|
truncatePieces(scan, at)
|
||||||
// CommonMark: no link nests inside another, though an image's description holds one.
|
deactivateOpeners(scan, at)
|
||||||
for (const piece of scan.pieces) if (piece.kind === 'open' && !piece.image) piece.active = false
|
|
||||||
scan.pieces.push({ kind: 'nodes', nodes: applyMark(nodes, { attrs, type: 'link' }) })
|
scan.pieces.push({ kind: 'nodes', nodes: applyMark(nodes, { attrs, type: 'link' }) })
|
||||||
return success(true)
|
return success(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CommonMark: no link nests inside another, though an image's description holds one.
|
||||||
|
function deactivateOpeners(scan: Scan, before: number): void {
|
||||||
|
for (let index = scan.deactivatedBefore; index < before; index += 1) {
|
||||||
|
const piece = scan.pieces[index]
|
||||||
|
if (piece?.kind === 'open' && !piece.image) piece.active = false
|
||||||
|
}
|
||||||
|
scan.deactivatedBefore = before
|
||||||
|
}
|
||||||
|
|
||||||
|
function truncatePieces(scan: Scan, to: number): void {
|
||||||
|
scan.pieces.length = to
|
||||||
|
scan.deactivatedBefore = Math.min(scan.deactivatedBefore, to)
|
||||||
|
}
|
||||||
|
|
||||||
function closeImage(scan: Scan, at: number, inner: readonly Piece[], definition: LinkDefinition): Result<null> {
|
function closeImage(scan: Scan, at: number, inner: readonly Piece[], definition: LinkDefinition): Result<null> {
|
||||||
if (definition.title !== undefined) return failure('unmappable-image', 'no media node carries a link title', scan.path)
|
if (definition.title !== undefined) return failure('unmappable-image', 'no media node carries a link title', scan.path)
|
||||||
const resolved = imageAlt(inner, scan.path)
|
const resolved = imageAlt(inner, scan.path)
|
||||||
if (!resolved.ok) return resolved
|
if (!resolved.ok) return resolved
|
||||||
const alt = resolved.value
|
const alt = resolved.value
|
||||||
const attrs = alt === '' ? { type: 'external', url: definition.destination } : { alt, type: 'external', url: definition.destination }
|
const attrs = alt === '' ? { type: 'external', url: definition.destination } : { alt, type: 'external', url: definition.destination }
|
||||||
scan.pieces.length = at
|
truncatePieces(scan, at)
|
||||||
scan.pieces.push({ alt, kind: 'image', node: { attrs: { layout: 'center' }, content: [{ attrs, type: 'media' }], type: 'mediaSingle' } })
|
scan.pieces.push({ alt, kind: 'image', node: { attrs: { layout: 'center' }, content: [{ attrs, type: 'media' }], type: 'mediaSingle' } })
|
||||||
return success(null)
|
return success(null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -416,6 +416,7 @@ test('names the mark spelling no opaque carry sits inside', () => {
|
|||||||
assert.equal(content(markdownToAdf(`**${carried}**\n`)), named)
|
assert.equal(content(markdownToAdf(`**${carried}**\n`)), named)
|
||||||
assert.equal(content(markdownToAdf(`~~a ${carried}~~\n`)), named)
|
assert.equal(content(markdownToAdf(`~~a ${carried}~~\n`)), named)
|
||||||
assert.equal(content(markdownToAdf(`[a ${carried} b](https://example.com/x)\n`)), named)
|
assert.equal(content(markdownToAdf(`[a ${carried} b](https://example.com/x)\n`)), named)
|
||||||
|
assert.equal(content(markdownToAdf(`[*<http://x/>${carried}*](/w)\n`)), named)
|
||||||
assert.equal(content(markdownToAdf(`!adf:underline[${carried}]\n`)), named)
|
assert.equal(content(markdownToAdf(`!adf:underline[${carried}]\n`)), named)
|
||||||
assert.equal(content(markdownToAdf(`!adf:textColor[a ${carried}]{color="#ae2e24"}\n`)), named)
|
assert.equal(content(markdownToAdf(`!adf:textColor[a ${carried}]{color="#ae2e24"}\n`)), named)
|
||||||
assert.equal(content(markdownToAdf(`\n`)), named)
|
assert.equal(content(markdownToAdf(`\n`)), named)
|
||||||
@@ -825,6 +826,33 @@ test('leaves the bracket pair no link parses as the text it holds', () => {
|
|||||||
])
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('leaves the brackets of a link whose text already holds one the text they are', () => {
|
||||||
|
const held: AdfMark = { attrs: { collection: 'c', href: '/u' }, type: 'link' }
|
||||||
|
assert.deepEqual(content(markdownToAdf('[<http://x/>](/v)\n')), [
|
||||||
|
{ content: [text('['), marked('http://x/', link('http://x/')), text('](/v)')], type: 'paragraph' },
|
||||||
|
])
|
||||||
|
assert.deepEqual(content(markdownToAdf('[a<http://x/>b](/v)\n')), [
|
||||||
|
{ content: [text('[a'), marked('http://x/', link('http://x/')), text('b](/v)')], type: 'paragraph' },
|
||||||
|
])
|
||||||
|
assert.deepEqual(content(markdownToAdf('[!adf:link[a]{collection=c href="/u"}](/v)\n')), [
|
||||||
|
{ content: [text('['), marked('a', held), text('](/v)')], type: 'paragraph' },
|
||||||
|
])
|
||||||
|
assert.deepEqual(content(markdownToAdf('[<http://x/>][r]\n\n[r]: /v\n')), [
|
||||||
|
{ content: [text('['), marked('http://x/', link('http://x/')), text(']'), marked('r', link('/v'))], type: 'paragraph' },
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('keeps the carry and the image the brackets a nested link leaves literal hold', () => {
|
||||||
|
assert.deepEqual(content(markdownToAdf(`[<http://x/>${carried}](/w)\n`)), [
|
||||||
|
{ content: [text('['), marked('http://x/', link('http://x/')), { type: 'placeholder' }, text('](/w)')], type: 'paragraph' },
|
||||||
|
])
|
||||||
|
assert.deepEqual(content(markdownToAdf(`[[<http://x/>](/c)${carried}](/w)\n`)), [
|
||||||
|
{ content: [text('[['), marked('http://x/', link('http://x/')), text('](/c)'), { type: 'placeholder' }, text('](/w)')], type: 'paragraph' },
|
||||||
|
])
|
||||||
|
// The failing bracket sits inside the image, not beside it: beside it the link-marked piece survives, and the deactivation stops being what the assertion pins.
|
||||||
|
assert.deepEqual(content(markdownToAdf(' ](/i)[](/y)](/w)](/v)\n')), [image('/v', 'a b [[http://x/](/c)](/w)')])
|
||||||
|
})
|
||||||
|
|
||||||
test('reads the reference links a definition resolves, and leaves the rest literal', () => {
|
test('reads the reference links a definition resolves, and leaves the rest literal', () => {
|
||||||
assert.deepEqual(content(markdownToAdf('[a][r]\n\n[r]: /url\n')), [{ content: [marked('a', link('/url'))], type: 'paragraph' }])
|
assert.deepEqual(content(markdownToAdf('[a][r]\n\n[r]: /url\n')), [{ content: [marked('a', link('/url'))], type: 'paragraph' }])
|
||||||
assert.deepEqual(content(markdownToAdf('[a][]\n\n[a]: /url\n')), [{ content: [marked('a', link('/url'))], type: 'paragraph' }])
|
assert.deepEqual(content(markdownToAdf('[a][]\n\n[a]: /url\n')), [{ content: [marked('a', link('/url'))], type: 'paragraph' }])
|
||||||
@@ -996,6 +1024,15 @@ test('names the href the directive link spells no value for', () => {
|
|||||||
assert.equal(content(markdownToAdf('!adf:link[a]\n')), named)
|
assert.equal(content(markdownToAdf('!adf:link[a]\n')), named)
|
||||||
assert.equal(content(markdownToAdf('!adf:link[a]{title=t}\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('See !adf:link[a]{id=01a032c3-7a90-70c9-88f6-c60f710eda07}.\n')), named)
|
||||||
|
assert.equal(content(markdownToAdf('!adf:link[<http://x/>]{collection=c}\n')), named)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('names the link a directive link wraps, no link holding another', () => {
|
||||||
|
const named = 'unsupported-node-shape: no link wraps a link: the [content] this one marks already holds one'
|
||||||
|
assert.equal(content(markdownToAdf('!adf:link[<http://x/>]{collection=c href="/u"}\n')), named)
|
||||||
|
assert.equal(content(markdownToAdf('!adf:link[[a](/v)]{collection=c href="/u"}\n')), named)
|
||||||
|
assert.equal(content(markdownToAdf('!adf:link[a <http://x/> b]{collection=c href="/u"}\n')), named)
|
||||||
|
assert.equal(content(markdownToAdf('!adf:link[<http://x/>]{href="/u"}\n')), named)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('names the directive mark left without the content it wraps', () => {
|
test('names the directive mark left without the content it wraps', () => {
|
||||||
|
|||||||
@@ -814,6 +814,12 @@ The done `todo.md` items in full, as they were written. `todo.md` keeps a one-li
|
|||||||
parses to a mark the emitter writes back as a carry, while the schema requires `href` and
|
parses to a mark the emitter writes back as a carry, while the schema requires `href` and
|
||||||
every other directive mark spells without attributes in both directions alike (the
|
every other directive mark spells without attributes in both directions alike (the
|
||||||
stability-reviewer, 2026-09-16; the maintainer, 2026-09-17).
|
stability-reviewer, 2026-09-16; the maintainer, 2026-09-17).
|
||||||
|
- [x] **16 — The link wrapping a link (`0.2.0`).** Read `[<http://x/>](/v)` and
|
||||||
|
`[!adf:link[a]{href="/u"}](/v)` as `[[a](/u)](/v)` reads — the inner link wins and the outer
|
||||||
|
brackets stay literal text, CommonMark's rule that no link holds another — rather than
|
||||||
|
dropping the outer link silently as `closeLink`'s `applyMark` does today, with a
|
||||||
|
normalization fixture per shape (the stability-reviewer, 2026-09-16; the maintainer,
|
||||||
|
2026-09-17).
|
||||||
|
|
||||||
## 5 — Ship `0.1.0`
|
## 5 — Ship `0.1.0`
|
||||||
|
|
||||||
|
|||||||
@@ -210,11 +210,7 @@ bundle size and the tagline.
|
|||||||
- [x] **13b — The directive link.**
|
- [x] **13b — The directive link.**
|
||||||
- [x] **14 — The CommonMark subset's directory (`0.2.0`).**
|
- [x] **14 — The CommonMark subset's directory (`0.2.0`).**
|
||||||
- [x] **15 — The href-less directive link (`0.2.0`).**
|
- [x] **15 — The href-less directive link (`0.2.0`).**
|
||||||
- [ ] **16 — The link wrapping a link (`0.2.0`).** Read `[<http://x/>](/v)` and
|
- [x] **16 — The link wrapping a link (`0.2.0`).**
|
||||||
`[!adf:link[a]{href="/u"}](/v)` as `[[a](/u)](/v)` reads — the inner link wins and the outer
|
|
||||||
brackets stay literal text, CommonMark's rule that no link holds another — rather than
|
|
||||||
dropping the outer link silently as `closeLink`'s `applyMark` does today, with a normalization
|
|
||||||
fixture per shape (the stability-reviewer, 2026-09-16; the maintainer, 2026-09-17).
|
|
||||||
- [ ] **17 — A machine-enforced size guardrail (`0.2.0`).** Add a per-function complexity check to
|
- [ ] **17 — A machine-enforced size guardrail (`0.2.0`).** Add a per-function complexity check to
|
||||||
the gate — branch count or size — so the fits-in-your-head guardrail fails the build rather
|
the gate — branch count or size — so the fits-in-your-head guardrail fails the build rather
|
||||||
than waiting for a review to catch it (the systems-architect, 2026-09-16). It reads ahead of
|
than waiting for a review to catch it (the systems-architect, 2026-09-16). It reads ahead of
|
||||||
|
|||||||
Reference in New Issue
Block a user