13b - link takes the directive form where CommonMark cannot spell it, unspellable-link leaves the code list
This commit is contained in:
@@ -51,6 +51,11 @@ Round-trip equality is a property tested over a corpus, not a claim made in pros
|
|||||||
- Plain CommonMark is a subset, with carve-outs (`spec/flavour.md`): literal text shaped like a
|
- Plain CommonMark is a subset, with carve-outs (`spec/flavour.md`): literal text shaped like a
|
||||||
directive, a pipe table or a `~~` pair is claimed — plus one image gap.
|
directive, a pipe table or a `~~` pair is claimed — plus one image gap.
|
||||||
- Tables: one header row plus plain inline cells → pipe table; anything richer → directive form.
|
- Tables: one header row plus plain inline cells → pipe table; anything richer → directive form.
|
||||||
|
- Links: `[text](url "title")`, or `<url>` for a bare autolink-shaped text, wherever CommonMark
|
||||||
|
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
|
||||||
|
spelling would read as a link reference definition — and a directive link CommonMark could spell
|
||||||
|
is refused (the maintainer, 2026-09-13).
|
||||||
- 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
|
||||||
@@ -117,7 +122,7 @@ reuses a code where one fits; the list is complete at `0.1.0`. A code names the
|
|||||||
cause; where one cause recurs across node types, across one mark's attributes or across
|
cause; where one cause recurs across node types, across one mark's attributes or across
|
||||||
directions, one code covers them all and
|
directions, one code covers them all and
|
||||||
`path` and `message` say which — `unsupported-nesting-depth` is the 500-level guard whichever
|
`path` and `message` say which — `unsupported-nesting-depth` is the 500-level guard whichever
|
||||||
direction hits it, `unspellable-link` the destination and the title alike. Where two codes stay
|
direction hits it, `unspellable-character` the text node and the code block alike. Where two codes stay
|
||||||
apart, the line between them is what they name: `unspellable-character` is a character CommonMark
|
apart, the line between them is what they name: `unspellable-character` is a character CommonMark
|
||||||
rewrites wherever text holds it, `unspellable-whitespace` the newline no inline directive's
|
rewrites wherever text holds it, `unspellable-whitespace` the newline no inline directive's
|
||||||
content slot spans, in either direction. A claim code names the spelling claimed, never the node that spelling would have built:
|
content slot spans, in either direction. A claim code names the spelling claimed, never the node that spelling would have built:
|
||||||
@@ -125,7 +130,8 @@ a malformed `!adf:table` is a `malformed-directive`, and an alignment colon a `m
|
|||||||
the flavour's own delimiter row is `-` runs, so the grammar refuses the colon rather than ADF's
|
the flavour's own delimiter row is `-` runs, so the grammar refuses the colon rather than ADF's
|
||||||
missing column model doing it. A refusal no spelling recovers from is a gap in the flavour rather
|
missing column model doing it. A refusal no spelling recovers from is a gap in the flavour rather
|
||||||
than a code: give the flavour the spelling and the code goes, which the freeze is the last moment
|
than a code: give the flavour the spelling and the code goes, which the freeze is the last moment
|
||||||
for. A cause the carry answers gets no code: a mark no
|
for — `unspellable-link` went at `0.2.0`, the directive link spelling the `href` and `title` it
|
||||||
|
refused and the attributes the carry held (the maintainer, 2026-09-13). A cause the carry answers gets no code: a mark no
|
||||||
spelling writes rides the carry with its node. A directive whose name reads back to no node is
|
spelling writes rides the carry with its node. A directive whose name reads back to no node is
|
||||||
`unknown-directive-name` rather than a claim code — the spelling is well formed, and telling that
|
`unknown-directive-name` rather than a claim code — the spelling is well formed, and telling that
|
||||||
apart from a typo is what a consumer switches on when a later MINOR gives the name meaning. A
|
apart from a typo is what a consumer switches on when a later MINOR gives the name meaning. A
|
||||||
@@ -298,8 +304,10 @@ someone spells it or pins it.
|
|||||||
directions must answer alike — whether a list marker interrupts a paragraph — is one function
|
directions must answer alike — whether a list marker interrupts a paragraph — is one function
|
||||||
there too, never a copy per direction, however conservative the copy would be. Where the rule is
|
there too, never a copy per direction, however conservative the copy would be. Where the rule is
|
||||||
the emitter's own choice, input consults it rather than restating it: the parser asks
|
the emitter's own choice, input consults it rather than restating it: the parser asks
|
||||||
`commonMarkSpelling` which form the emitter picks, so no fixture the emitter writes can be
|
`commonMarkSpelling` which form the emitter picks, and `openingLinkTakesDirective` whether the
|
||||||
refused, and a spelling the emitter refuses gives its own error rather than a second name for it.
|
line a paragraph's opening link starts forces the directive link, so no fixture the emitter
|
||||||
|
writes can be refused, and a spelling the emitter refuses gives its own error rather than a
|
||||||
|
second name for it.
|
||||||
- The attribute vocabulary is ADF's: `adf/` walks it and narrows each value to its kind, and a
|
- The attribute vocabulary is ADF's: `adf/` walks it and narrows each value to its kind, and a
|
||||||
format spells the narrowed value. A spelling that re-checks the type is the check's second copy.
|
format spells the narrowed value. A spelling that re-checks the type is the check's second copy.
|
||||||
Reading a spelling back is the format's own: the reader sits beside the spelling it inverts, so
|
Reading a spelling back is the format's own: the reader sits beside the spelling it inverts, so
|
||||||
|
|||||||
@@ -42,14 +42,19 @@ function migrateMarkdown(stored: string) {
|
|||||||
| `:mention[@Mikael]{id=5b10a2}` | `!adf:mention[@Mikael]{id=5b10a2}` |
|
| `:mention[@Mikael]{id=5b10a2}` | `!adf:mention[@Mikael]{id=5b10a2}` |
|
||||||
| the `adf` code fence and `:adf{json="…"}` | the `carry` code fence and `!adf:carry{json="…"}` |
|
| the `adf` code fence and `:adf{json="…"}` | the `carry` code fence and `!adf:carry{json="…"}` |
|
||||||
| `\:` keeps a directive literal | `\!adf:` keeps a directive literal |
|
| `\:` keeps a directive literal | `\!adf:` keeps a directive literal |
|
||||||
|
| `:adf{json="…"}` carrying a link for its `collection`, `id` or `occurrenceKey` | `!adf:link[text]{attrs}` |
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
### Error codes
|
### Error codes
|
||||||
|
|
||||||
|
`unspellable-link` leaves `ConvertErrorCode`: a `switch` naming it stops compiling, and the link
|
||||||
|
it named converts.
|
||||||
|
|
||||||
| Input | `0.1.0` | `0.2.0` |
|
| Input | `0.1.0` | `0.2.0` |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
| a link whose `href` or `title` no CommonMark escape spells, on emit | `unspellable-link` | spells `!adf:link[text]{attrs}` |
|
||||||
| a leaf node given a body (`media`, `listBreak`) | `unsupported-node-shape` | `malformed-directive` |
|
| a leaf node given a body (`media`, `listBreak`) | `unsupported-node-shape` | `malformed-directive` |
|
||||||
| a node with a block body written as a leaf (`panel`) | `unsupported-node-shape` | `malformed-directive` |
|
| a node with a block body written as a leaf (`panel`) | `unsupported-node-shape` | `malformed-directive` |
|
||||||
| an empty node the `::taskItem` spelling row names, written as a leaf | parses | `malformed-directive` |
|
| an empty node the `::taskItem` spelling row names, written as a leaf | parses | `malformed-directive` |
|
||||||
|
|||||||
@@ -100,7 +100,6 @@ emit refuses:
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `unspellable-character` | text or a code block holds a carriage return or a null character, which CommonMark rewrites wherever it sits | strip or replace the character; no escape carries it through the round-trip |
|
| `unspellable-character` | text or a code block holds a carriage return or a null character, which CommonMark rewrites wherever it sits | strip or replace the character; no escape carries it through the round-trip |
|
||||||
| `unspellable-line-start` | a paragraph line begins with a code span whose backticks would read back as a code fence | put any text before the code span |
|
| `unspellable-line-start` | a paragraph line begins with a code span whose backticks would read back as a code fence | put any text before the code span |
|
||||||
| `unspellable-link` | a link `href` or `title` holds what no canonical escape spells — a backslash, a newline, a control character, an entity reference, an angle bracket beside a space | percent-encode the destination (`%5C` for the backslash, `%26` for the `&` that opens the entity), or drop the title |
|
|
||||||
| `unspellable-whitespace` | an `emoji`, `mention` or `status` holds a newline in the text its inline directive spells in the content slot | replace it with a space — an inline directive never spans lines |
|
| `unspellable-whitespace` | an `emoji`, `mention` or `status` holds a newline in the text its inline directive spells in the content slot | replace it with a space — an inline directive never spans lines |
|
||||||
| `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it |
|
| `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it |
|
||||||
| `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take — or markdown writes as a directive a node the flavour spells as CommonMark | write the shape the message names; `spec/flavour.md` lists every type's attributes and body |
|
| `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take — or markdown writes as a directive a node the flavour spells as CommonMark | write the shape the message names; `spec/flavour.md` lists every type's attributes and body |
|
||||||
@@ -115,7 +114,7 @@ emit refuses:
|
|||||||
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. 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.
|
||||||
`[a](/a\b)`, `<http://x?a=1&b=2>` and `[a](/x y)` read cleanly and then refuse.
|
`` ` `` ` `` reads cleanly and then refuses.
|
||||||
- Three CommonMark spellings parse without an error and build a document the reference renders
|
- Three CommonMark spellings parse without an error and build a document the reference renders
|
||||||
differently: `[](/url)` and `[]()` stay literal text against CommonMark's empty link, a list
|
differently: `[](/url)` and `[]()` stay literal text against CommonMark's empty link, a list
|
||||||
continuing past a marker change stays one list against CommonMark's two, and a shortcut
|
continuing past a marker change stays one list against CommonMark's two, and a shortcut
|
||||||
@@ -124,10 +123,9 @@ emit refuses:
|
|||||||
- 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.3.0`.
|
processing instruction alike. ADF holds no raw-HTML node; the element mapping ships at `0.3.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
|
||||||
a carriage return, a link destination or title no canonical escape spells, a paragraph line
|
a carriage return, or a paragraph line beginning with a code span whose backticks read back as a
|
||||||
beginning with a code span whose backticks read back as a fence. Show the refusal and keep the
|
fence. Show the refusal and keep the document read-only; saving markdown you could not produce
|
||||||
document read-only; saving markdown you could not produce is the loss the round-trip exists to
|
is the loss the round-trip exists to stop.
|
||||||
stop.
|
|
||||||
- The pipe table narrows GFM's twice: every row opens with a pipe, so GFM's bare form is an error
|
- The pipe table narrows GFM's twice: every row opens with a pipe, so GFM's bare form is an error
|
||||||
result rather than the prose it reads as, and an alignment colon in the delimiter row is an
|
result rather than the prose it reads as, and an alignment colon in the delimiter row is an
|
||||||
error too — ADF holds no column alignment. The trailing pipe is canonical output, optional in
|
error too — ADF holds no column alignment. The trailing pipe is canonical output, optional in
|
||||||
|
|||||||
@@ -1,18 +1,4 @@
|
|||||||
[
|
[
|
||||||
{
|
|
||||||
"check": "fixpoint",
|
|
||||||
"divergence": "unspellable-link",
|
|
||||||
"example": 196,
|
|
||||||
"kind": "unspellable",
|
|
||||||
"reason": "The link title holds literal newlines no escape spells."
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"check": "fixpoint",
|
|
||||||
"divergence": "unspellable-link",
|
|
||||||
"example": 202,
|
|
||||||
"kind": "unspellable",
|
|
||||||
"reason": "The link destination holds a backslash the flavour cannot escape."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"check": "count",
|
"check": "count",
|
||||||
"divergence": "ul 2/1",
|
"divergence": "ul 2/1",
|
||||||
@@ -349,13 +335,6 @@
|
|||||||
"kind": "pending",
|
"kind": "pending",
|
||||||
"reason": "An empty link text stays literal text; CommonMark renders an empty link."
|
"reason": "An empty link text stays literal text; CommonMark renders an empty link."
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"check": "fixpoint",
|
|
||||||
"divergence": "unspellable-link",
|
|
||||||
"example": 502,
|
|
||||||
"kind": "unspellable",
|
|
||||||
"reason": "The link destination holds a backslash the flavour cannot escape."
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"check": "count",
|
"check": "count",
|
||||||
"divergence": "a 1/5 em 1/4",
|
"divergence": "a 1/5 em 1/4",
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
unsupported-node-shape
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
See !adf:link[the docs]{href="https://example.com/docs"} here.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
unsupported-node-shape
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
!adf:link[the docs]{href="https://example.com/docs"}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "/a\\b"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "a",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "http://x?a=1&b=2"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "http://x?a=1&b=2",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "/x\ny"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "a",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[a](/a\b)
|
||||||
|
|
||||||
|
<http://x?a=1&b=2>
|
||||||
|
|
||||||
|
[a](/x y)
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
!adf:carry{json="{\"marks\":[{\"attrs\":{\"href\":\"/u\"},\"type\":\"link\"},{\"type\":\"code\"}],\"text\":\"]: a\",\"type\":\"text\"}"}
|
|
||||||
|
|
||||||
!adf:carry{json="{\"attrs\":{\"timestamp\":\"]:a\"},\"marks\":[{\"attrs\":{\"href\":\"/u\"},\"type\":\"link\"}],\"type\":\"date\"}"}
|
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
!adf:link[`]: a`]{href="/u"}
|
||||||
|
|
||||||
|
!adf:link[!adf:date{timestamp="]:a"}]{href="/u"}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"color": "#ae2e24"
|
||||||
|
},
|
||||||
|
"type": "textColor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "ab:\\!adf:name{"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "ab:\\!adf:name{",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
!adf:textColor[!adf:link[ab:\\\!adf:name{]{href="ab:\\!adf:name{"}]{color="#ae2e24"}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"collection": "contentId-98237",
|
||||||
|
"href": "https://example.com/docs"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "the docs",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " explain it.",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
!adf:link[the docs]{collection=contentId-98237 href="https://example.com/docs"} explain it.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "See ",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "https://example.com/a b>c"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "the docs",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": ".",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
See !adf:link[the docs]{href="https://example.com/a b>c"}.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "See ",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "<https://example.com/"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "the docs",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": ".",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
See !adf:link[the docs]{href="\u003chttps://example.com/"}.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "See ",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "https://example.com/a\\b"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "the docs",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": ".",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
See !adf:link[the docs]{href="https://example.com/a\\b"}.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "See ",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "https://example.com/a\u0001b"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "the docs",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": ".",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
See !adf:link[the docs]{href="https://example.com/a\u0001b"}.
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "See ",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "https://example.com/?a=1&b=2"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "the docs",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": ".",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
See !adf:link[the docs]{href="https://example.com/?a=1\u0026amp;b=2"}.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "https://example.com/docs",
|
||||||
|
"id": "01a032c3-7a90-70c9-88f6-c60f710eda07"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "the docs",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " explain it.",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
!adf:link[the docs]{href="https://example.com/docs" id=01a032c3-7a90-70c9-88f6-c60f710eda07} explain it.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "https://example.com/docs",
|
||||||
|
"occurrenceKey": "01a032c3-7a90-70c9-88f6-c60f710eda07"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "the docs",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": " explain it.",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
!adf:link[the docs]{href="https://example.com/docs" occurrenceKey=01a032c3-7a90-70c9-88f6-c60f710eda07} explain it.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "See ",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "https://example.com/docs",
|
||||||
|
"title": "Setup\\guide"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "the docs",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": ".",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
See !adf:link[the docs]{href="https://example.com/docs" title="Setup\\guide"}.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "See ",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "https://example.com/docs",
|
||||||
|
"title": "Setup & guide"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "the docs",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": ".",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
See !adf:link[the docs]{href="https://example.com/docs" title="Setup \u0026amp; guide"}.
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "See ",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"marks": [
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"href": "https://example.com/docs",
|
||||||
|
"title": "Setup\nguide"
|
||||||
|
},
|
||||||
|
"type": "link"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"text": "the docs",
|
||||||
|
"type": "text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": ".",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
See !adf:link[the docs]{href="https://example.com/docs" title="Setup\nguide"}.
|
||||||
+16
-8
@@ -465,13 +465,20 @@ is a named error.
|
|||||||
## Marks
|
## Marks
|
||||||
|
|
||||||
An inline node's marks ride the spelling wrapped around them, never the block sections' reserved
|
An inline node's marks ride the spelling wrapped around them, never the block sections' reserved
|
||||||
`marks` key. `code`, `em`, `link`, `strike` and `strong` keep their markdown spellings, and are
|
`marks` key. `code`, `em`, `strike` and `strong` keep their markdown spellings, and are not
|
||||||
not directive names: `!adf:em[x]` is a named error. `border`, `subsup`, `textColor` and `underline`
|
directive names: `!adf:em[x]` is a named error. `border`, `subsup`, `textColor` and `underline`
|
||||||
are inline directives, content required non-empty.
|
are inline directives, content required non-empty. `link` keeps its markdown spelling wherever
|
||||||
|
CommonMark holds it — `<url>` for a bare autolink-shaped text, else `[text](url "title")` — and is
|
||||||
|
the inline directive `!adf:link[text]{attrs}` only where CommonMark does not: an attribute besides
|
||||||
|
`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
|
||||||
|
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.
|
||||||
|
|
||||||
- `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.
|
||||||
- `link` — Attributes: `href` (string), `title` (string).
|
- `link` — Attributes: `collection` (string), `href` (string), `id` (string), `occurrenceKey`
|
||||||
|
(string), `title` (string).
|
||||||
- `subsup` — Attributes: `type` (`sub` `sup`).
|
- `subsup` — Attributes: `type` (`sub` `sup`).
|
||||||
- `textColor` — Attributes: `color` (string, `#rrggbb`).
|
- `textColor` — Attributes: `color` (string, `#rrggbb`).
|
||||||
- `underline` — Attributes: none.
|
- `underline` — Attributes: none.
|
||||||
@@ -490,13 +497,14 @@ and the mark lacks, an order putting a code span outside another mark, `code` ov
|
|||||||
text node or over text holding a newline, or a spelling CommonMark's flanking rules cannot open or
|
text node or over text holding a newline, or a spelling CommonMark's flanking rules cannot open or
|
||||||
close where the run sits (`un**-real**istic`), or one CommonMark's matching pairs elsewhere — the
|
close where the run sits (`un**-real**istic`), or one CommonMark's matching pairs elsewhere — the
|
||||||
intra-word `*` runs together with a neighbouring `**`, and the multiple-of-3 rule can leave the
|
intra-word `*` runs together with a neighbouring `**`, and the multiple-of-3 rule can leave the
|
||||||
merged run's pairing to another delimiter — rides the inline carry whole. A value the spelling
|
merged run's pairing to another delimiter — rides the inline carry whole. An opaque carry inside a
|
||||||
holds but CommonMark cannot write — a link destination or title — is a named error instead. An
|
mark spelling is a named error in input: the carry restores its node exactly, marks included
|
||||||
opaque carry inside a mark spelling is a named error in input: the carry restores its node
|
(AGENTS.md §3).
|
||||||
exactly, marks included (AGENTS.md §3).
|
|
||||||
|
|
||||||
```
|
```
|
||||||
!adf:textColor[**Overdue**]{color="#ae2e24"}, H!adf:subsup[2]{type=sub}O, !adf:underline[signed].
|
!adf:textColor[**Overdue**]{color="#ae2e24"}, H!adf:subsup[2]{type=sub}O, !adf:underline[signed].
|
||||||
|
|
||||||
|
!adf:link[the release]{collection=contentId-98237 href="https://example.com/release notes"}
|
||||||
|
|
||||||
!adf:border[!adf:mediaInline{collection=contentId-98237 id=01a032c3-7a90-70c9-88f6-c60f710eda07}]{color="#091e42" size=2}
|
!adf:border[!adf:mediaInline{collection=contentId-98237 id=01a032c3-7a90-70c9-88f6-c60f710eda07}]{color="#091e42" size=2}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ type Spelled = [string, Map<string, AttributeKind>]
|
|||||||
|
|
||||||
const carried = ['alignment', 'annotation', 'backgroundColor', 'blockCard', 'bodiedRule', 'breakout', 'dataConsumer', 'embedCard', 'fontSize', 'fragment', 'indentation', 'inlineExtension', 'placeholder']
|
const carried = ['alignment', 'annotation', 'backgroundColor', 'blockCard', 'bodiedRule', 'breakout', 'dataConsumer', 'embedCard', 'fontSize', 'fragment', 'indentation', 'inlineExtension', 'placeholder']
|
||||||
const definitionReference = '#/definitions/'
|
const definitionReference = '#/definitions/'
|
||||||
const gaps = ['link.collection', 'link.id', 'link.occurrenceKey']
|
const gaps: string[] = []
|
||||||
const grammarOwn = ['doc', 'text']
|
const grammarOwn = ['doc', 'text']
|
||||||
const readKeywords = ['$ref', 'additionalProperties', 'allOf', 'anyOf', 'enum', 'items', 'maxItems', 'maximum', 'minItems', 'minLength', 'minimum', 'pattern', 'properties', 'required', 'type']
|
const readKeywords = ['$ref', 'additionalProperties', 'allOf', 'anyOf', 'enum', 'items', 'maxItems', 'maximum', 'minItems', 'minLength', 'minimum', 'pattern', 'properties', 'required', 'type']
|
||||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..', 'spec', 'adf-schema')
|
const root = join(dirname(fileURLToPath(import.meta.url)), '..', 'spec', 'adf-schema')
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ export const markAttributes = {
|
|||||||
border: { color: 'string', size: 'number' },
|
border: { color: 'string', size: 'number' },
|
||||||
code: {},
|
code: {},
|
||||||
em: {},
|
em: {},
|
||||||
link: { href: 'string', title: 'string' },
|
link: { collection: 'string', href: 'string', id: 'string', occurrenceKey: 'string', title: 'string' },
|
||||||
strike: {},
|
strike: {},
|
||||||
strong: {},
|
strong: {},
|
||||||
subsup: { type: 'string' },
|
subsup: { type: 'string' },
|
||||||
|
|||||||
@@ -82,15 +82,25 @@ test('spells a code block language no info string holds as an attribute', () =>
|
|||||||
assert.equal(language('a\tb'), '!adf:codeBlock {language="a\\tb"}\n```\n```\n!adf:/codeBlock\n')
|
assert.equal(language('a\tb'), '!adf:codeBlock {language="a\\tb"}\n```\n```\n!adf:/codeBlock\n')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('refuses a link destination CommonMark cannot spell', () => {
|
test('spells a link destination CommonMark cannot as the directive link', () => {
|
||||||
const link = (href: string): AdfDocument => document(paragraph({ marks: [{ attrs: { href }, type: 'link' }], text: 't', type: 'text' }))
|
const link = (href: string): string => markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs: { href }, type: 'link' }], text: 't', type: 'text' }))))
|
||||||
assert.equal(code(adfToMarkdown(link('https://example.com/a b>c'))), 'unspellable-link')
|
assert.equal(link('https://example.com/a b>c'), '!adf:link[t]{href="https://example.com/a b>c"}\n')
|
||||||
assert.equal(code(adfToMarkdown(link('<https://example.com/'))), 'unspellable-link')
|
assert.equal(link('<https://example.com/'), '!adf:link[t]{href="\\u003chttps://example.com/"}\n')
|
||||||
assert.equal(code(adfToMarkdown(link('https://example.com/a\\b'))), 'unspellable-link')
|
assert.equal(link('https://example.com/a\\b'), '!adf:link[t]{href="https://example.com/a\\\\b"}\n')
|
||||||
assert.equal(code(adfToMarkdown(link('https://example.com/?a=1&b=2'))), 'unspellable-link')
|
assert.equal(link('https://example.com/?a=1&b=2'), '!adf:link[t]{href="https://example.com/?a=1\\u0026amp;b=2"}\n')
|
||||||
assert.equal(code(adfToMarkdown(link('https://example.com/a\nb'))), 'unspellable-link')
|
assert.equal(link('https://example.com/a\nb'), '!adf:link[t]{href="https://example.com/a\\nb"}\n')
|
||||||
const entity = 'https://example.com/?a=1&b=2'
|
const entity = 'https://example.com/?a=1&b=2'
|
||||||
assert.equal(code(adfToMarkdown(document(paragraph({ marks: [{ attrs: { href: entity }, type: 'link' }], text: entity, type: 'text' })))), 'unspellable-link')
|
const autolinkShaped = document(paragraph({ marks: [{ attrs: { href: entity }, type: 'link' }], text: entity, type: 'text' }))
|
||||||
|
assert.equal(markdown(adfToMarkdown(autolinkShaped)), '!adf:link[https://example.com/?a=1\\&b=2]{href="https://example.com/?a=1\\u0026amp;b=2"}\n')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('spells a link carrying an attribute CommonMark cannot as the directive link, and refuses none', () => {
|
||||||
|
const link = (attrs: Record<string, string>): string =>
|
||||||
|
markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs, type: 'link' }], text: 't', type: 'text' }))))
|
||||||
|
assert.equal(link({ collection: 'c', href: 'https://example.com/' }), '!adf:link[t]{collection=c href="https://example.com/"}\n')
|
||||||
|
assert.equal(link({ href: 'https://example.com/', id: 'i' }), '!adf:link[t]{href="https://example.com/" id=i}\n')
|
||||||
|
assert.equal(link({ href: 'https://example.com/', occurrenceKey: 'k' }), '!adf:link[t]{href="https://example.com/" occurrenceKey=k}\n')
|
||||||
|
assert.equal(link({ href: 'https://example.com/', id: 'i', title: 'a\nb' }), '!adf:link[t]{href="https://example.com/" id=i title="a\\nb"}\n')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('escapes the parenthesis a link destination leaves unbalanced, and no other', () => {
|
test('escapes the parenthesis a link destination leaves unbalanced, and no other', () => {
|
||||||
@@ -102,19 +112,20 @@ test('escapes the parenthesis a link destination leaves unbalanced, and no other
|
|||||||
assert.equal(link('https://example.com/a (b'), '[t](<https://example.com/a (b>)\n')
|
assert.equal(link('https://example.com/a (b'), '[t](<https://example.com/a (b>)\n')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('escapes the quote a link title holds, and refuses the rest', () => {
|
test('escapes the quote a link title holds, and spells the rest as the directive link', () => {
|
||||||
const titled = (title: string): AdfDocument =>
|
const titled = (title: string): string =>
|
||||||
document(paragraph({ marks: [{ attrs: { href: 'https://example.com/', title }, type: 'link' }], text: 't', type: 'text' }))
|
markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs: { href: 'https://example.com/', title }, type: 'link' }], text: 't', type: 'text' }))))
|
||||||
assert.equal(markdown(adfToMarkdown(titled('He said "hi"'))), '[t](https://example.com/ "He said \\"hi\\"")\n')
|
assert.equal(titled('He said "hi"'), '[t](https://example.com/ "He said \\"hi\\"")\n')
|
||||||
assert.equal(code(adfToMarkdown(titled('a\nb'))), 'unspellable-link')
|
assert.equal(titled('a\nb'), '!adf:link[t]{href="https://example.com/" title="a\\nb"}\n')
|
||||||
assert.equal(code(adfToMarkdown(titled('a\\b'))), 'unspellable-link')
|
assert.equal(titled('a\\b'), '!adf:link[t]{href="https://example.com/" title="a\\\\b"}\n')
|
||||||
|
assert.equal(titled('a & b'), '!adf:link[t]{href="https://example.com/" title="a \\u0026amp; b"}\n')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('carries a link mark the link spelling cannot write', () => {
|
test('carries a link mark the link spelling cannot write', () => {
|
||||||
const carried = (mark: AdfMark): string => markdown(adfToMarkdown(document(paragraph({ marks: [mark], text: 't', type: 'text' }))))
|
const carried = (mark: AdfMark): string => markdown(adfToMarkdown(document(paragraph({ marks: [mark], text: 't', type: 'text' }))))
|
||||||
assert.equal(
|
assert.equal(
|
||||||
carried({ attrs: { href: 'x', id: 'y' }, type: 'link' }),
|
carried({ attrs: { href: 'x', rel: 'y' }, type: 'link' }),
|
||||||
'!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"href\\":\\"x\\",\\"id\\":\\"y\\"},\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n',
|
'!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"href\\":\\"x\\",\\"rel\\":\\"y\\"},\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n',
|
||||||
)
|
)
|
||||||
assert.equal(carried({ attrs: { href: 4 }, type: 'link' }), '!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"href\\":4},\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n')
|
assert.equal(carried({ attrs: { href: 4 }, type: 'link' }), '!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"href\\":4},\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n')
|
||||||
assert.equal(carried({ type: 'link' }), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n')
|
assert.equal(carried({ type: 'link' }), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n')
|
||||||
|
|||||||
@@ -2,21 +2,20 @@ import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
|||||||
import type { InlineDirective } from '../../adf/inline-directives.ts'
|
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, isAutolink } from '../commonmark-grammar.ts'
|
import { claimsLine, holdsNullCharacter } from '../commonmark-grammar.ts'
|
||||||
import { escapeUnbalanced, spellDestination, spellLinkTarget } from '../link-syntax.ts'
|
import { commonMarkLink, markSpelling, spellMarkAttributes, type CommonMarkLink } from '../mark-spellings.ts'
|
||||||
|
import { escapeUnbalanced, spellDestination } from '../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 { holdsDirectivePrefix, slotLineEndingFault, spellInlineDirectiveOpener, spellInlineLeafDirective } from '../directive-syntax.ts'
|
|
||||||
import { holdsEntityReference } from '../entity-references.ts'
|
|
||||||
import { inlineDirective } from '../../adf/inline-directives.ts'
|
import { inlineDirective } from '../../adf/inline-directives.ts'
|
||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
import { longestBacktickRun } from '../backtick-runs.ts'
|
import { longestBacktickRun } from '../backtick-runs.ts'
|
||||||
import { markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
|
||||||
import { nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
import { nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||||
import { sameMark } from '../../adf/editor-normal.ts'
|
import { sameMark } from '../../adf/editor-normal.ts'
|
||||||
|
import { slotLineEndingFault, spellInlineDirectiveOpener, spellInlineLeafDirective } from '../directive-syntax.ts'
|
||||||
import { spellInlineNodeAttributes } from './inline-directive-spelling.ts'
|
import { spellInlineNodeAttributes } from './inline-directive-spelling.ts'
|
||||||
import { spellTextDirective } from '../text-directive.ts'
|
import { spellTextDirective } from '../text-directive.ts'
|
||||||
|
|
||||||
type EmittedLine = { line: string; segments: InlineSegment[] }
|
type EmittedLine = { line: string; openingLinkAsDirective: boolean; segments: InlineSegment[] }
|
||||||
|
|
||||||
type Emission = { carry: NodeRange; segments?: undefined } | { carry?: undefined; segments: InlineSegment[] }
|
type Emission = { carry: NodeRange; segments?: undefined } | { carry?: undefined; segments: InlineSegment[] }
|
||||||
|
|
||||||
@@ -24,13 +23,17 @@ type InlineContext = {
|
|||||||
atBlockEnd: boolean
|
atBlockEnd: boolean
|
||||||
bracketed: boolean
|
bracketed: boolean
|
||||||
carried: ReadonlySet<number>
|
carried: ReadonlySet<number>
|
||||||
|
openingLinkAsDirective: boolean
|
||||||
path: ConvertErrorPath
|
path: ConvertErrorPath
|
||||||
spansLines: boolean
|
spansLines: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type InlineRun = { index: number; kind: 'marked'; mark: AdfMark; nodes: AdfNode[] } | { index: number; kind: 'plain'; node: AdfNode }
|
type InlineRun = { index: number; kind: 'marked'; mark: AdfMark; nodes: AdfNode[] } | { index: number; kind: 'plain'; node: AdfNode }
|
||||||
|
|
||||||
type LineAttempt = { carry: NodeRange; line?: undefined } | { carry?: undefined; line: string }
|
type LineAttempt =
|
||||||
|
| { carry: NodeRange; line?: undefined; openingLinkAsDirective?: undefined }
|
||||||
|
| { carry?: undefined; line?: undefined; openingLinkAsDirective: true }
|
||||||
|
| { carry?: undefined; line: string; openingLinkAsDirective?: undefined }
|
||||||
|
|
||||||
export function emitInlineLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<string> {
|
export function emitInlineLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<string> {
|
||||||
const emitted = emitLine(nodes, container, path)
|
const emitted = emitLine(nodes, container, path)
|
||||||
@@ -38,6 +41,13 @@ export function emitInlineLine(nodes: readonly AdfNode[], container: LineContain
|
|||||||
return success(emitted.value.line)
|
return success(emitted.value.line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The parser asks which form a paragraph's opening link takes rather than restating the line rule (AGENTS.md §11).
|
||||||
|
export function openingLinkTakesDirective(nodes: readonly AdfNode[], path: ConvertErrorPath): Result<boolean> {
|
||||||
|
const emitted = emitLine(nodes, 'paragraph', path)
|
||||||
|
if (!emitted.ok) return emitted
|
||||||
|
return success(emitted.value.openingLinkAsDirective)
|
||||||
|
}
|
||||||
|
|
||||||
export function tryPipeCell(nodes: readonly AdfNode[], path: ConvertErrorPath): string | undefined {
|
export function tryPipeCell(nodes: readonly AdfNode[], path: ConvertErrorPath): string | undefined {
|
||||||
const emitted = emitLine(nodes, 'table-cell', path)
|
const emitted = emitLine(nodes, 'table-cell', path)
|
||||||
if (!emitted.ok) return undefined
|
if (!emitted.ok) return undefined
|
||||||
@@ -47,18 +57,19 @@ export function tryPipeCell(nodes: readonly AdfNode[], path: ConvertErrorPath):
|
|||||||
|
|
||||||
export function tryImageLine(alt: string | undefined, href: string, path: ConvertErrorPath): string | undefined {
|
export function tryImageLine(alt: string | undefined, href: string, path: ConvertErrorPath): string | undefined {
|
||||||
if (alt !== undefined && (/^[ \t]|[ \t]$|[\n\r]/.test(alt) || holdsNullCharacter(alt))) return undefined
|
if (alt !== undefined && (/^[ \t]|[ \t]$|[\n\r]/.test(alt) || holdsNullCharacter(alt))) return undefined
|
||||||
const destination = spellDestination(href, path)
|
const destination = spellDestination(href)
|
||||||
if (!destination.ok) return undefined
|
if (destination === undefined) return undefined
|
||||||
const description: InlineSegment[] = alt === undefined ? [] : [{ escaping: 'bracketed', text: alt }]
|
const description: InlineSegment[] = alt === undefined ? [] : [{ escaping: 'bracketed', text: alt }]
|
||||||
const attempt = attemptLine([syntax('`)], 'paragraph', path)
|
const attempt = attemptLine([syntax('`)], 'paragraph', path)
|
||||||
return attempt.ok ? attempt.value.line : undefined
|
return attempt.ok ? attempt.value.line : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
// A carried node joins no run, so every pass carries at least one more node.
|
// A carried node joins no run and the opening link turns directive once, so every pass carries at least one more node.
|
||||||
function emitLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<EmittedLine> {
|
function emitLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<EmittedLine> {
|
||||||
const carried = new Set<number>()
|
const carried = new Set<number>()
|
||||||
|
let openingLinkAsDirective = false
|
||||||
for (;;) {
|
for (;;) {
|
||||||
const emission = lineSegments(nodes, container, path, carried)
|
const emission = lineSegments(nodes, container, path, carried, openingLinkAsDirective)
|
||||||
if (!emission.ok) return emission
|
if (!emission.ok) return emission
|
||||||
if (emission.value.carry !== undefined) {
|
if (emission.value.carry !== undefined) {
|
||||||
carryRange(carried, emission.value.carry)
|
carryRange(carried, emission.value.carry)
|
||||||
@@ -66,8 +77,9 @@ function emitLine(nodes: readonly AdfNode[], container: LineContainer, path: Con
|
|||||||
}
|
}
|
||||||
const attempt = attemptLine(emission.value.segments, container, path)
|
const attempt = attemptLine(emission.value.segments, container, path)
|
||||||
if (!attempt.ok) return attempt
|
if (!attempt.ok) return attempt
|
||||||
if (attempt.value.carry === undefined) return success({ line: attempt.value.line, segments: emission.value.segments })
|
if (attempt.value.line !== undefined) return success({ line: attempt.value.line, openingLinkAsDirective, segments: emission.value.segments })
|
||||||
carryRange(carried, attempt.value.carry)
|
if (attempt.value.carry !== undefined) carryRange(carried, attempt.value.carry)
|
||||||
|
else openingLinkAsDirective = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,8 +87,15 @@ function carryRange(carried: Set<number>, range: NodeRange): void {
|
|||||||
for (let index = range.first; index <= range.last; index += 1) carried.add(index)
|
for (let index = range.first; index <= range.last; index += 1) carried.add(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
function lineSegments(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, carried: ReadonlySet<number>): Result<Emission> {
|
function lineSegments(
|
||||||
const emission = emitRun(nodes, 0, 0, { atBlockEnd: true, bracketed: false, carried, path, spansLines: container === 'paragraph' })
|
nodes: readonly AdfNode[],
|
||||||
|
container: LineContainer,
|
||||||
|
path: ConvertErrorPath,
|
||||||
|
carried: ReadonlySet<number>,
|
||||||
|
openingLinkAsDirective: boolean,
|
||||||
|
): Result<Emission> {
|
||||||
|
const context: InlineContext = { atBlockEnd: true, bracketed: false, carried, openingLinkAsDirective, path, spansLines: container === 'paragraph' }
|
||||||
|
const emission = emitRun(nodes, 0, 0, context)
|
||||||
if (!emission.ok) return emission
|
if (!emission.ok) return emission
|
||||||
if (emission.value.carry !== undefined) return emission
|
if (emission.value.carry !== undefined) return emission
|
||||||
return success({ segments: carryStrippedWhitespace(emission.value.segments) })
|
return success({ segments: carryStrippedWhitespace(emission.value.segments) })
|
||||||
@@ -84,6 +103,7 @@ function lineSegments(nodes: readonly AdfNode[], container: LineContainer, path:
|
|||||||
|
|
||||||
function attemptLine(segments: readonly InlineSegment[], container: LineContainer, path: ConvertErrorPath): Result<LineAttempt> {
|
function attemptLine(segments: readonly InlineSegment[], container: LineContainer, path: ConvertErrorPath): Result<LineAttempt> {
|
||||||
const assembled = assembleInlineLine(segments, container)
|
const assembled = assembleInlineLine(segments, container)
|
||||||
|
if (assembled.openingLinkAsDirective) return success({ openingLinkAsDirective: true })
|
||||||
if (assembled.unspellableRun !== undefined) return success({ carry: assembled.unspellableRun })
|
if (assembled.unspellableRun !== undefined) return success({ carry: assembled.unspellableRun })
|
||||||
for (const [index, single] of assembled.line.split('\n').entries()) {
|
for (const [index, single] of assembled.line.split('\n').entries()) {
|
||||||
if (container === 'paragraph' && claimsLine(single, index === 0 ? 'first' : 'later')) {
|
if (container === 'paragraph' && claimsLine(single, index === 0 ? 'first' : 'later')) {
|
||||||
@@ -237,7 +257,13 @@ function emitMarkedRun(nodes: readonly AdfNode[], mark: AdfMark, depth: number,
|
|||||||
if (attributes === undefined) return success({ carry: range })
|
if (attributes === undefined) return success({ carry: range })
|
||||||
if (spelling.kind === 'code') return emitCodeSpan(nodes, depth, range, path)
|
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 === 'emphasis') return emitEmphasis(nodes, spelling.spelling, depth, range, context)
|
||||||
if (spelling.kind === 'link') return emitLink(nodes, mark, depth, range, context, path)
|
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 inner = emitRun(nodes, depth + 1, index, { ...context, bracketed: true, spansLines: false })
|
const inner = emitRun(nodes, depth + 1, index, { ...context, bracketed: true, spansLines: false })
|
||||||
if (!inner.ok) return inner
|
if (!inner.ok) return inner
|
||||||
if (inner.value.carry !== undefined) return inner
|
if (inner.value.carry !== undefined) return inner
|
||||||
@@ -278,20 +304,17 @@ function needsPadding(text: string): boolean {
|
|||||||
return text.startsWith(' ') && text.endsWith(' ') && /[^ ]/.test(text)
|
return text.startsWith(' ') && text.endsWith(' ') && /[^ ]/.test(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range: NodeRange, context: InlineContext, path: ConvertErrorPath): Result<Emission> {
|
function isBareLink(nodes: readonly AdfNode[], href: string, depth: number): boolean {
|
||||||
const href = nodeAttrs(mark)['href']
|
|
||||||
const title = nodeAttrs(mark)['title']
|
|
||||||
if (typeof href !== 'string') return success({ carry: range })
|
|
||||||
const node = nodes[0]
|
const node = nodes[0]
|
||||||
const bare = nodes.length === 1 && node !== undefined && node.type === 'text' && node.text === href && nodeMarks(node).length === depth + 1
|
return nodes.length === 1 && node !== undefined && node.type === 'text' && node.text === href && nodeMarks(node).length === depth + 1
|
||||||
const autolinkHolds = !context.bracketed || (!href.includes('`') && !holdsDirectivePrefix(href) && escapeUnbalanced(href, '[', ']') === href)
|
}
|
||||||
if (bare && autolinkHolds && title === undefined && isAutolink(href) && !holdsEntityReference(href)) return success({ segments: [syntax(`<${href}>`)] })
|
|
||||||
const target = spellLinkTarget(href, typeof title === 'string' ? title : undefined, path)
|
function emitLink(nodes: readonly AdfNode[], href: string, commonMark: CommonMarkLink, depth: number, range: NodeRange, context: InlineContext): Result<Emission> {
|
||||||
if (!target.ok) return target
|
if (commonMark.form === 'autolink') return success({ segments: [syntax(`<${href}>`)] })
|
||||||
const inner = emitRun(nodes, depth + 1, range.first, { ...context, bracketed: true })
|
const inner = emitRun(nodes, depth + 1, range.first, { ...context, bracketed: true })
|
||||||
if (!inner.ok) return inner
|
if (!inner.ok) return inner
|
||||||
if (inner.value.carry !== undefined) return inner
|
if (inner.value.carry !== undefined) return inner
|
||||||
const spelledTarget: InlineSegment = context.bracketed ? { escaping: 'bracketed-link-target', text: escapeUnbalanced(target.value, '[', ']') } : syntax(target.value)
|
const target: InlineSegment = context.bracketed ? { escaping: 'bracketed-link-target', text: escapeUnbalanced(commonMark.target, '[', ']') } : syntax(commonMark.target)
|
||||||
return success({ segments: [{ escaping: 'none', nodes: range, text: '[' }, ...inner.value.segments, syntax(']('), spelledTarget, syntax(')')] })
|
return success({ segments: [{ escaping: 'none', nodes: range, text: '[' }, ...inner.value.segments, syntax(']('), target, syntax(')')] })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ export type InlineSegment =
|
|||||||
| { emphasis?: undefined; escaping: 'none'; nodes: NodeRange; text: string }
|
| { emphasis?: undefined; escaping: 'none'; nodes: NodeRange; text: string }
|
||||||
| { emphasis?: undefined; escaping: InlineEscaping; nodes?: undefined; text: string }
|
| { emphasis?: undefined; escaping: InlineEscaping; nodes?: undefined; text: string }
|
||||||
|
|
||||||
export type AssembledLine = { line: string; unspellableRun: NodeRange | undefined }
|
export type AssembledLine = { line: string; openingLinkAsDirective?: true; unspellableRun: NodeRange | undefined }
|
||||||
|
|
||||||
type ScanLine = { position: LinePosition; start: number; text: string }
|
type ScanLine = { position: LinePosition; start: number; text: string }
|
||||||
|
|
||||||
@@ -77,8 +77,7 @@ function escape(segments: readonly InlineSegment[], container: LineContainer): A
|
|||||||
output += scan.charAt(index)
|
output += scan.charAt(index)
|
||||||
}
|
}
|
||||||
if (container === 'paragraph' && opensLinkDefinition(output)) {
|
if (container === 'paragraph' && opensLinkDefinition(output)) {
|
||||||
const opener = segments[0]?.nodes
|
if (segments[0]?.nodes !== undefined) return { line: output, openingLinkAsDirective: true, unspellableRun: undefined }
|
||||||
if (opener !== undefined) return { line: output, unspellableRun: opener }
|
|
||||||
return { line: `\\${output}`, unspellableRun: unspellableRun(segments, output, placements) }
|
return { line: `\\${output}`, unspellableRun: unspellableRun(segments, output, placements) }
|
||||||
}
|
}
|
||||||
return { line: output, unspellableRun: unspellableRun(segments, output, placements) }
|
return { line: output, unspellableRun: unspellableRun(segments, output, placements) }
|
||||||
|
|||||||
+11
-27
@@ -1,5 +1,4 @@
|
|||||||
import { backslashEscape, decodeTextEscapes, holdsControlCharacter } from './commonmark-grammar.ts'
|
import { backslashEscape, decodeTextEscapes, holdsControlCharacter } from './commonmark-grammar.ts'
|
||||||
import { failure, success, type ConvertErrorPath, type Result } from '../result.ts'
|
|
||||||
import { holdsEntityReference } from './entity-references.ts'
|
import { holdsEntityReference } from './entity-references.ts'
|
||||||
|
|
||||||
export type LinkDefinition = { destination: string; title?: string }
|
export type LinkDefinition = { destination: string; title?: string }
|
||||||
@@ -90,28 +89,17 @@ export function skipLinkWhitespace(text: string, offset: number): number {
|
|||||||
return offset + rest.length - rest.replace(/^[ \t]*\n?[ \t]*/, '').length
|
return offset + rest.length - rest.replace(/^[ \t]*\n?[ \t]*/, '').length
|
||||||
}
|
}
|
||||||
|
|
||||||
export function spellDestination(href: string, path: ConvertErrorPath): Result<string> {
|
export function spellDestination(href: string): string | undefined {
|
||||||
if (holdsControlCharacter(href)) return failure('unspellable-link', 'a link destination holds a control character', path)
|
if (holdsControlCharacter(href) || href.includes('\\') || holdsEntityReference(href)) return undefined
|
||||||
if (href.includes('\\')) return failure('unspellable-link', 'no canonical escape spells a backslash in a link destination', path)
|
if (href.includes(' ')) return /[<>]/.test(href) ? undefined : `<${href}>`
|
||||||
if (holdsEntityReference(href)) {
|
return href.startsWith('<') ? undefined : escapeUnbalanced(href, '(', ')')
|
||||||
return failure('unspellable-link', 'a link destination holds an entity reference that decodes on the way back', path)
|
|
||||||
}
|
|
||||||
if (href.includes(' ')) {
|
|
||||||
if (/[<>]/.test(href)) {
|
|
||||||
return failure('unspellable-link', 'no canonical escape spells an angle bracket beside a space in a link destination', path)
|
|
||||||
}
|
|
||||||
return success(`<${href}>`)
|
|
||||||
}
|
|
||||||
if (href.startsWith('<')) return failure('unspellable-link', 'a bare link destination cannot begin with an angle bracket', path)
|
|
||||||
return success(escapeUnbalanced(href, '(', ')'))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function spellLinkTarget(href: string, title: string | undefined, path: ConvertErrorPath): Result<string> {
|
export function spellLinkTarget(href: string, title: string | undefined): string | undefined {
|
||||||
const destination = spellDestination(href, path)
|
const destination = spellDestination(href)
|
||||||
if (!destination.ok || title === undefined) return destination
|
if (destination === undefined || title === undefined) return destination
|
||||||
const spelledTitle = spellTitle(title, path)
|
const spelledTitle = spellTitle(title)
|
||||||
if (!spelledTitle.ok) return spelledTitle
|
return spelledTitle === undefined ? undefined : `${destination === '' ? '<>' : destination}${spelledTitle}`
|
||||||
return success(`${destination.value === '' ? '<>' : destination.value}${spelledTitle.value}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function escapeUnbalanced(spelling: string, opener: string, closer: string): string {
|
export function escapeUnbalanced(spelling: string, opener: string, closer: string): string {
|
||||||
@@ -128,10 +116,6 @@ export function escapeUnbalanced(spelling: string, opener: string, closer: strin
|
|||||||
return spelled
|
return spelled
|
||||||
}
|
}
|
||||||
|
|
||||||
function spellTitle(title: string, path: ConvertErrorPath): Result<string> {
|
function spellTitle(title: string): string | undefined {
|
||||||
if (/[\n\r\\]/.test(title)) {
|
return /[\n\r\\]/.test(title) || holdsEntityReference(title) ? undefined : ` "${title.replaceAll('"', '\\"')}"`
|
||||||
return failure('unspellable-link', 'no canonical escape spells a backslash or newline in a link title', path)
|
|
||||||
}
|
|
||||||
if (holdsEntityReference(title)) return failure('unspellable-link', 'a link title holds an entity reference that decodes on the way back', path)
|
|
||||||
return success(` "${title.replaceAll('"', '\\"')}"`)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,20 @@
|
|||||||
import type { AdfMark } from '../adf/document.ts'
|
import type { AdfAttributes, AdfMark } from '../adf/document.ts'
|
||||||
import type { AttributeVocabulary } from '../adf/attribute-vocabulary.ts'
|
import type { AttributeVocabulary } from '../adf/attribute-vocabulary.ts'
|
||||||
import type { MarkType } from '../adf/mark-attributes.ts'
|
import type { MarkType } from '../adf/mark-attributes.ts'
|
||||||
|
import { escapeUnbalanced, spellLinkTarget } from './link-syntax.ts'
|
||||||
|
import { holdsDirectivePrefix, spellAttributes, spellVocabulary } from './directive-syntax.ts'
|
||||||
|
import { holdsEntityReference } from './entity-references.ts'
|
||||||
|
import { isAutolink } from './commonmark-grammar.ts'
|
||||||
import { isMarkType, markAttributes } from '../adf/mark-attributes.ts'
|
import { isMarkType, markAttributes } from '../adf/mark-attributes.ts'
|
||||||
import { nodeAttrs } from '../adf/document.ts'
|
import { nodeAttrs } from '../adf/document.ts'
|
||||||
import { spellAttributes, spellVocabulary } from './directive-syntax.ts'
|
|
||||||
import { vocabularyPairs } from '../adf/attribute-vocabulary.ts'
|
import { vocabularyPairs } from '../adf/attribute-vocabulary.ts'
|
||||||
|
|
||||||
type Spelling = { kind: 'code' | 'directive' | 'link'; spelling?: undefined } | { kind: 'emphasis'; spelling: string }
|
type Spelling = { kind: 'code' | 'directive' | 'link'; spelling?: undefined } | { kind: 'emphasis'; spelling: string }
|
||||||
|
|
||||||
export type MarkSpelling = Spelling & { attributes: AttributeVocabulary }
|
export type MarkSpelling = Spelling & { attributes: AttributeVocabulary }
|
||||||
|
|
||||||
|
export type CommonMarkLink = { form: 'autolink'; target?: undefined } | { form: 'inline'; target: string }
|
||||||
|
|
||||||
const markSpellings: Readonly<Record<MarkType, Spelling>> = {
|
const markSpellings: Readonly<Record<MarkType, Spelling>> = {
|
||||||
border: { kind: 'directive' },
|
border: { kind: 'directive' },
|
||||||
code: { kind: 'code' },
|
code: { kind: 'code' },
|
||||||
@@ -30,6 +35,16 @@ export function markSpelling(type: string): MarkSpelling | undefined {
|
|||||||
return { attributes, kind: spelling.kind }
|
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 {
|
||||||
|
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' }
|
||||||
|
const target = spellLinkTarget(href, typeof title === 'string' ? title : undefined)
|
||||||
|
return target === undefined ? undefined : { form: 'inline', target }
|
||||||
|
}
|
||||||
|
|
||||||
export function spellMarkAttributes(mark: AdfMark, vocabulary: AttributeVocabulary): string | undefined {
|
export function spellMarkAttributes(mark: AdfMark, vocabulary: AttributeVocabulary): string | undefined {
|
||||||
const pairs = vocabularyPairs(nodeAttrs(mark), vocabulary, [])
|
const pairs = vocabularyPairs(nodeAttrs(mark), vocabulary, [])
|
||||||
return pairs === undefined ? undefined : spellAttributes(spellVocabulary(pairs))
|
return pairs === undefined ? undefined : spellAttributes(spellVocabulary(pairs))
|
||||||
|
|||||||
@@ -28,10 +28,9 @@ function markdownForm(spelling: MarkSpelling): string | undefined {
|
|||||||
case 'code':
|
case 'code':
|
||||||
return '`x`'
|
return '`x`'
|
||||||
case 'directive':
|
case 'directive':
|
||||||
|
case 'link':
|
||||||
return undefined
|
return undefined
|
||||||
case 'emphasis':
|
case 'emphasis':
|
||||||
return `${spelling.spelling}x${spelling.spelling}`
|
return `${spelling.spelling}x${spelling.spelling}`
|
||||||
case 'link':
|
|
||||||
return '[x](url)'
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
||||||
import type { DirectiveSpan } from '../directive-syntax.ts'
|
import type { DirectiveSpan } from '../directive-syntax.ts'
|
||||||
import type { EmphasisPairing } from '../emphasis-matching.ts'
|
import type { EmphasisPairing } from '../emphasis-matching.ts'
|
||||||
|
import type { LineContainer } from '../emit/line-escaping.ts'
|
||||||
import type { LinkDefinition } from '../link-syntax.ts'
|
import type { LinkDefinition } from '../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 '../backtick-runs.ts'
|
import { backtickRun, closingBacktickRun } from '../backtick-runs.ts'
|
||||||
|
import { commonMarkLink } from '../mark-spellings.ts'
|
||||||
import { delimiterFlags, matchEmphasis, runLength } from '../emphasis-matching.ts'
|
import { delimiterFlags, matchEmphasis, runLength } from '../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'
|
||||||
import { mergeAdjacentText } from '../../adf/editor-normal.ts'
|
import { mergeAdjacentText } from '../../adf/editor-normal.ts'
|
||||||
import { nodeAttrs, nodeMarks } from '../../adf/document.ts'
|
import { nodeAttrs, nodeMarks } from '../../adf/document.ts'
|
||||||
import { normalizeLabel, readInlineTarget, readLabel } from '../link-syntax.ts'
|
import { normalizeLabel, readInlineTarget, readLabel } from '../link-syntax.ts'
|
||||||
|
import { openingLinkTakesDirective } from '../emit/inline-line.ts'
|
||||||
import { readCarriedInline } from '../opaque-carry.ts'
|
import { readCarriedInline } from '../opaque-carry.ts'
|
||||||
import { readDirectiveMark } from './directive-marks.ts'
|
import { readDirectiveMark } from './directive-marks.ts'
|
||||||
import { readInlineDirective } from '../directive-syntax.ts'
|
import { readInlineDirective } from '../directive-syntax.ts'
|
||||||
@@ -33,19 +36,29 @@ type Piece =
|
|||||||
|
|
||||||
type Run = { canClose: boolean; canOpen: boolean; character: string; index: number; length: number }
|
type Run = { canClose: boolean; canOpen: boolean; character: string; index: number; length: number }
|
||||||
|
|
||||||
type Scan = { definitions: LinkDefinitions; path: ConvertErrorPath; pending: string; pieces: Piece[]; source: string }
|
// `container` is the emitter's line container, `undefined` inside a directive's content slot; `openingSpellableLink` defers a directive link at offset 0 to `assemble`.
|
||||||
|
type Scan = {
|
||||||
|
container: LineContainer | undefined
|
||||||
|
definitions: LinkDefinitions
|
||||||
|
openingSpellableLink: boolean
|
||||||
|
path: ConvertErrorPath
|
||||||
|
pending: string
|
||||||
|
pieces: Piece[]
|
||||||
|
source: string
|
||||||
|
}
|
||||||
|
|
||||||
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 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'
|
||||||
|
|
||||||
export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath): Result<InlineContent> {
|
export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer): Result<InlineContent> {
|
||||||
return parseInline(source, definitions, path, true)
|
return parseInline(source, definitions, path, container)
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseInline(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, strip: boolean): Result<InlineContent> {
|
function parseInline(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer | undefined): Result<InlineContent> {
|
||||||
const scan: Scan = { definitions, path, pending: '', pieces: [], source }
|
const scan: Scan = { container, definitions, openingSpellableLink: false, path, pending: '', pieces: [], source }
|
||||||
let index = 0
|
let index = 0
|
||||||
while (index < source.length) {
|
while (index < source.length) {
|
||||||
switch (source.charAt(index)) {
|
switch (source.charAt(index)) {
|
||||||
@@ -93,7 +106,7 @@ function parseInline(source: string, definitions: LinkDefinitions, path: Convert
|
|||||||
index += 1
|
index += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
flush(scan, strip)
|
flush(scan, container !== undefined)
|
||||||
return assemble(scan)
|
return assemble(scan)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,14 +171,14 @@ function readDirective(scan: Scan, index: number): Result<number> | undefined {
|
|||||||
const directive = readInlineDirective(scan.source, index)
|
const directive = readInlineDirective(scan.source, index)
|
||||||
if (directive === undefined) return undefined
|
if (directive === undefined) return undefined
|
||||||
if (directive.fault !== undefined) return faulted(directive.fault, scan.path)
|
if (directive.fault !== undefined) return faulted(directive.fault, scan.path)
|
||||||
const piece = directivePiece(scan, directive.value)
|
const piece = directivePiece(scan, directive.value, index)
|
||||||
if (!piece.ok) return piece
|
if (!piece.ok) return piece
|
||||||
flush(scan, false)
|
flush(scan, false)
|
||||||
scan.pieces.push(piece.value)
|
scan.pieces.push(piece.value)
|
||||||
return success(index + directive.value.length)
|
return success(index + directive.value.length)
|
||||||
}
|
}
|
||||||
|
|
||||||
function directivePiece(scan: Scan, span: DirectiveSpan): Result<Piece> {
|
function directivePiece(scan: Scan, span: DirectiveSpan, index: number): Result<Piece> {
|
||||||
const carried = readCarriedInline(span)
|
const carried = readCarriedInline(span)
|
||||||
if (carried !== undefined) {
|
if (carried !== undefined) {
|
||||||
if (carried.fault !== undefined) return faulted(carried.fault, scan.path)
|
if (carried.fault !== undefined) return faulted(carried.fault, scan.path)
|
||||||
@@ -183,6 +196,8 @@ function directivePiece(scan: Scan, span: DirectiveSpan): Result<Piece> {
|
|||||||
return failure('unsupported-node-shape', `the ${span.name} mark wraps the [content] it marks: this one wraps none`, scan.path)
|
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)
|
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) })
|
return success({ kind: 'nodes', nodes: applyMark(slot.value.nodes, mark.value) })
|
||||||
}
|
}
|
||||||
const node = readInlineDirectiveNode(span.name, span.attributes, slot.value?.nodes, scan.path)
|
const node = readInlineDirectiveNode(span.name, span.attributes, slot.value?.nodes, scan.path)
|
||||||
@@ -190,9 +205,21 @@ function directivePiece(scan: Scan, span: DirectiveSpan): Result<Piece> {
|
|||||||
return success({ kind: 'nodes', nodes: [node.value] })
|
return success({ kind: 'nodes', nodes: [node.value] })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 (index !== 0 || scan.container !== 'paragraph') return failure('unsupported-node-shape', spellableLink, scan.path)
|
||||||
|
scan.openingSpellableLink = true
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
function slotContent(scan: Scan, content: string | undefined): Result<SlotContent | undefined> {
|
function slotContent(scan: Scan, content: string | undefined): Result<SlotContent | undefined> {
|
||||||
if (content === undefined) return success(undefined)
|
if (content === undefined) return success(undefined)
|
||||||
const parsed = parseInline(content, scan.definitions, scan.path, false)
|
const parsed = parseInline(content, scan.definitions, scan.path, undefined)
|
||||||
if (!parsed.ok) return parsed
|
if (!parsed.ok) return parsed
|
||||||
if (parsed.value.image !== undefined) return failure('unmappable-image', imageAlone, scan.path)
|
if (parsed.value.image !== undefined) return failure('unmappable-image', imageAlone, scan.path)
|
||||||
return success(parsed.value)
|
return success(parsed.value)
|
||||||
@@ -214,6 +241,11 @@ function assemble(scan: Scan): Result<InlineContent> {
|
|||||||
if (holdsImage(scan.pieces)) return failure('unmappable-image', imageAlone, scan.path)
|
if (holdsImage(scan.pieces)) return failure('unmappable-image', imageAlone, scan.path)
|
||||||
const nodes = resolveNodes(scan.pieces, scan.path)
|
const nodes = resolveNodes(scan.pieces, scan.path)
|
||||||
if (!nodes.ok) return nodes
|
if (!nodes.ok) return nodes
|
||||||
|
if (scan.openingSpellableLink) {
|
||||||
|
const takesDirective = openingLinkTakesDirective(nodes.value, scan.path)
|
||||||
|
if (!takesDirective.ok) return takesDirective
|
||||||
|
if (!takesDirective.value) return failure('unsupported-node-shape', spellableLink, scan.path)
|
||||||
|
}
|
||||||
return success({ carry: holdsCarry(scan.pieces), nodes: nodes.value })
|
return success({ carry: holdsCarry(scan.pieces), nodes: nodes.value })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -287,8 +287,8 @@ test('names the directive form a node CommonMark spells refuses', () => {
|
|||||||
|
|
||||||
// The spelling the emitter refuses gives the emitter's own error, never a second name for it.
|
// The spelling the emitter refuses gives the emitter's own error, never a second name for it.
|
||||||
test('gives back the refusal the CommonMark spelling itself raises', () => {
|
test('gives back the refusal the CommonMark spelling itself raises', () => {
|
||||||
const destination = '!adf:blockquote\n[t](https://example.com/a\\b)\n!adf:/blockquote\n'
|
const lineStart = '!adf:blockquote\n` `` `\n!adf:/blockquote\n'
|
||||||
assert.equal(content(markdownToAdf(destination)), 'unspellable-link: no canonical escape spells a backslash in a link destination')
|
assert.equal(content(markdownToAdf(lineStart)), 'unspellable-line-start: block parsing would claim the emitted line "``` `` ```"')
|
||||||
})
|
})
|
||||||
|
|
||||||
test('names the directive name no node reads back to', () => {
|
test('names the directive name no node reads back to', () => {
|
||||||
@@ -966,7 +966,25 @@ test('names the mark markdown spells, never a directive', () => {
|
|||||||
assert.equal(content(markdownToAdf('!adf:strong[a]\n')), 'unsupported-node-shape: strong is spelled **x**, never as a directive')
|
assert.equal(content(markdownToAdf('!adf:strong[a]\n')), 'unsupported-node-shape: strong is spelled **x**, never as a directive')
|
||||||
assert.equal(content(markdownToAdf('!adf:strike[a]\n')), 'unsupported-node-shape: strike is spelled ~~x~~, never as a directive')
|
assert.equal(content(markdownToAdf('!adf:strike[a]\n')), 'unsupported-node-shape: strike is spelled ~~x~~, never as a directive')
|
||||||
assert.equal(content(markdownToAdf('!adf:code[a]\n')), 'unsupported-node-shape: code is spelled `x`, never as a directive')
|
assert.equal(content(markdownToAdf('!adf:code[a]\n')), 'unsupported-node-shape: code is spelled `x`, never as a directive')
|
||||||
assert.equal(content(markdownToAdf('!adf:link[a]{href="/u"}\n')), 'unsupported-node-shape: link is spelled [x](url), never as 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'
|
||||||
|
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)
|
||||||
|
assert.equal(content(markdownToAdf('# !adf:link[`]: a`]{href="/u"}\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.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' }
|
||||||
|
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')), [
|
||||||
|
{ attrs: { level: 1, localId: 'h' }, content: [opening], type: 'heading' },
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('names the directive mark left without the content it wraps', () => {
|
test('names the directive mark left without the content it wraps', () => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
|||||||
import type { Block, DirectiveBlock } from './blocks.ts'
|
import type { Block, DirectiveBlock } from './blocks.ts'
|
||||||
import type { BlockDirectiveNode } from './directive-nodes.ts'
|
import type { BlockDirectiveNode } from './directive-nodes.ts'
|
||||||
import type { ConvertFault } from '../../result.ts'
|
import type { ConvertFault } from '../../result.ts'
|
||||||
|
import type { LineContainer } from '../emit/line-escaping.ts'
|
||||||
import type { LinkDefinitions } from './inline-content.ts'
|
import type { LinkDefinitions } from './inline-content.ts'
|
||||||
import { carryName, readCarriedBlock } from '../opaque-carry.ts'
|
import { carryName, readCarriedBlock } from '../opaque-carry.ts'
|
||||||
import { commonMarkSpelling } from '../emit/adf-to-markdown.ts'
|
import { commonMarkSpelling } from '../emit/adf-to-markdown.ts'
|
||||||
@@ -67,7 +68,7 @@ function blockNode(block: Block, definitions: LinkDefinitions, path: ConvertErro
|
|||||||
case 'fault':
|
case 'fault':
|
||||||
return faulted(block.fault, path)
|
return faulted(block.fault, path)
|
||||||
case 'heading':
|
case 'heading':
|
||||||
return contentNode({ attrs: { level: block.level }, type: 'heading' }, block.text, definitions, path)
|
return contentNode({ attrs: { level: block.level }, type: 'heading' }, block.text, definitions, path, 'heading')
|
||||||
case 'html':
|
case 'html':
|
||||||
return failure('unmappable-html', `no raw HTML converts at this version: ${block.construct}`, path)
|
return failure('unmappable-html', `no raw HTML converts at this version: ${block.construct}`, path)
|
||||||
case 'orderedList':
|
case 'orderedList':
|
||||||
@@ -119,7 +120,7 @@ function tableNode(rows: readonly string[][], definitions: LinkDefinitions, path
|
|||||||
const type = rowIndex === 0 ? 'tableHeader' : 'tableCell'
|
const type = rowIndex === 0 ? 'tableHeader' : 'tableCell'
|
||||||
const row: AdfNode[] = []
|
const row: AdfNode[] = []
|
||||||
for (const [cellIndex, cell] of cells.entries()) {
|
for (const [cellIndex, cell] of cells.entries()) {
|
||||||
const paragraph = contentNode({ type: 'paragraph' }, cell, definitions, [...path, 'content', rowIndex, 'content', cellIndex, 'content', 0])
|
const paragraph = contentNode({ type: 'paragraph' }, cell, definitions, [...path, 'content', rowIndex, 'content', cellIndex, 'content', 0], 'table-cell')
|
||||||
if (!paragraph.ok) return paragraph
|
if (!paragraph.ok) return paragraph
|
||||||
row.push({ content: [paragraph.value], type })
|
row.push({ content: [paragraph.value], type })
|
||||||
}
|
}
|
||||||
@@ -133,7 +134,7 @@ function inlineBodyNode(node: AdfNode, blocks: readonly Block[], definitions: Li
|
|||||||
const only = blocks.length === 1 ? blocks[0] : undefined
|
const only = blocks.length === 1 ? blocks[0] : undefined
|
||||||
if (only?.kind === 'fault') return positioned(faulted(only.fault, path), only.position)
|
if (only?.kind === 'fault') return positioned(faulted(only.fault, path), only.position)
|
||||||
if (only?.kind !== 'paragraph') return failure('unsupported-node-shape', `${node.type} takes one paragraph as its body: this body is not one`, path)
|
if (only?.kind !== 'paragraph') return failure('unsupported-node-shape', `${node.type} takes one paragraph as its body: this body is not one`, path)
|
||||||
return positioned(contentNode(node, only.text, definitions, path), only.position)
|
return positioned(contentNode(node, only.text, definitions, path, 'paragraph'), only.position)
|
||||||
}
|
}
|
||||||
|
|
||||||
function containerNode(node: AdfNode, blocks: readonly Block[], definitions: LinkDefinitions, path: ConvertErrorPath, depth: number): Result<AdfNode> {
|
function containerNode(node: AdfNode, blocks: readonly Block[], definitions: LinkDefinitions, path: ConvertErrorPath, depth: number): Result<AdfNode> {
|
||||||
@@ -168,14 +169,14 @@ function codeBlockNode(language: string, text: string, path: ConvertErrorPath, d
|
|||||||
|
|
||||||
// spec/flavour.md, The CommonMark image: only a plain paragraph gives an image the block it needs.
|
// spec/flavour.md, The CommonMark image: only a plain paragraph gives an image the block it needs.
|
||||||
function paragraphNode(text: string, definitions: LinkDefinitions, path: ConvertErrorPath): Result<AdfNode> {
|
function paragraphNode(text: string, definitions: LinkDefinitions, path: ConvertErrorPath): Result<AdfNode> {
|
||||||
const content = parseInlineContent(text, definitions, path)
|
const content = parseInlineContent(text, definitions, path, 'paragraph')
|
||||||
if (!content.ok) return content
|
if (!content.ok) return content
|
||||||
const image = content.value.image
|
const image = content.value.image
|
||||||
return success(image === undefined ? withContent({ type: 'paragraph' }, content.value.nodes) : image)
|
return success(image === undefined ? withContent({ type: 'paragraph' }, content.value.nodes) : image)
|
||||||
}
|
}
|
||||||
|
|
||||||
function contentNode(node: AdfNode, text: string, definitions: LinkDefinitions, path: ConvertErrorPath): Result<AdfNode> {
|
function contentNode(node: AdfNode, text: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer): Result<AdfNode> {
|
||||||
const content = parseInlineContent(text, definitions, path)
|
const content = parseInlineContent(text, definitions, path, container)
|
||||||
if (!content.ok) return content
|
if (!content.ok) return content
|
||||||
if (content.value.image !== undefined) return failure('unmappable-image', `no ADF node carries an image inside a ${node.type}`, path)
|
if (content.value.image !== undefined) return failure('unmappable-image', `no ADF node carries an image inside a ${node.type}`, path)
|
||||||
return success(withContent(node, content.value.nodes))
|
return success(withContent(node, content.value.nodes))
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export type ConvertErrorCode =
|
|||||||
| 'unmappable-image'
|
| 'unmappable-image'
|
||||||
| 'unspellable-character'
|
| 'unspellable-character'
|
||||||
| 'unspellable-line-start'
|
| 'unspellable-line-start'
|
||||||
| 'unspellable-link'
|
|
||||||
| 'unspellable-whitespace'
|
| 'unspellable-whitespace'
|
||||||
| 'unsupported-document-version'
|
| 'unsupported-document-version'
|
||||||
| 'unsupported-nesting-depth'
|
| 'unsupported-nesting-depth'
|
||||||
|
|||||||
Reference in New Issue
Block a user