12b - The inline directive forms read and spell the !adf: prefix #84

Merged
lilleman merged 3 commits from inline-prefix-12b into main 2026-09-16 10:27:44 +02:00
67 changed files with 418 additions and 335 deletions
+1 -1
View File
@@ -1 +1 @@
_a :adf{json="{\"type\":\"blockCard\"}"} b_
_a !adf:carry{json="{\"type\":\"blockCard\"}"} b_
+1 -1
View File
@@ -1 +1 @@
:adf{json="{"}
!adf:carry{json="{"}
+1 -1
View File
@@ -1 +1 @@
:adf{json="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"}
!adf:carry{json="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"}
@@ -1,3 +1,3 @@
:::adf
:::carry
{"type":"rule"}
:::
+1 -1
View File
@@ -1 +1 @@
Part :mention[**@Mikael**]{id=b1c2}.
Part !adf:mention[**@Mikael**]{id=b1c2}.
+1 -1
View File
@@ -1 +1 @@
Part :mention[@A and more.
Part !adf:mention[@A and more.
+1 -1
View File
@@ -1 +1 @@
Part :em[emphasised] here.
Part !adf:em[emphasised] here.
@@ -0,0 +1 @@
malformed-directive
+1
View File
@@ -0,0 +1 @@
Part !adf: here.
@@ -0,0 +1 @@
unsupported-node-shape
@@ -0,0 +1 @@
!adf:status[!adf:carry{json="{\"attrs\":{\"localId\":\"x\"},\"text\":\"ok\",\"type\":\"text\"}"}]{color=red}
+1 -1
View File
@@ -1 +1 @@
Part :text{text=plain} here.
Part !adf:text{text=plain} here.
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -2,5 +2,5 @@
| --- | --- |
| Bolt M8 | Grade `8.8` |
| Nut \| washer | Sold as a pair |
| Washer M8 | 100 pcs:hardBreak{}zinc-plated |
| :text{text=" "}Spare | |
| Washer M8 | 100 pcs!adf:hardBreak{}zinc-plated |
| !adf:text{text=" "}Spare | |
@@ -1,7 +1,7 @@
a `b :inlineCard{url="c\u0060d"}
a `b !adf:inlineCard{url="c\u0060d"}
:inlineCard{url="https://example.com/s?q=a\u0026amp;b\u003cc"}
!adf:inlineCard{url="https://example.com/s?q=a\u0026amp;b\u003cc"}
::extension {extensionKey=jira parameters="{\"jql\":\"a\u007cb \u0060c\u0060 \u0026 d \u003c e\"}"}
[a:inlineCard{url="b](c)"}
[a!adf:inlineCard{url="b](c)"}
@@ -38,4 +38,4 @@
}
```
See :adf{json="{\"attrs\":{\"localId\":5,\"text\":\"In review\"},\"type\":\"status\"}"} now.
See !adf:carry{json="{\"attrs\":{\"localId\":5,\"text\":\"In review\"},\"type\":\"status\"}"} now.
@@ -1,11 +1,11 @@
Reviewed :adf{json="{\"marks\":[{\"attrs\":{\"annotationType\":\"inlineComment\",\"id\":\"0198f3a2-7c41-7f2e-9b3a-4d8e2c1a6b90\"},\"type\":\"annotation\"}],\"text\":\"audited\",\"type\":\"text\"}"} twice.
Reviewed !adf:carry{json="{\"marks\":[{\"attrs\":{\"annotationType\":\"inlineComment\",\"id\":\"0198f3a2-7c41-7f2e-9b3a-4d8e2c1a6b90\"},\"type\":\"annotation\"}],\"text\":\"audited\",\"type\":\"text\"}"} twice.
:adf{json="{\"marks\":[{\"attrs\":{\"colour\":\"red\"},\"type\":\"em\"}],\"text\":\"urgent\",\"type\":\"text\"}"}
!adf:carry{json="{\"marks\":[{\"attrs\":{\"colour\":\"red\"},\"type\":\"em\"}],\"text\":\"urgent\",\"type\":\"text\"}"}
:adf{json="{\"marks\":[{\"type\":\"code\"},{\"type\":\"strong\"}],\"text\":\"flag\",\"type\":\"text\"}"}
!adf:carry{json="{\"marks\":[{\"type\":\"code\"},{\"type\":\"strong\"}],\"text\":\"flag\",\"type\":\"text\"}"}
:adf{json="{\"marks\":[{\"type\":\"code\"}],\"text\":\"a\\nb\",\"type\":\"text\"}"}
!adf:carry{json="{\"marks\":[{\"type\":\"code\"}],\"text\":\"a\\nb\",\"type\":\"text\"}"}
:adf{json="{\"marks\":[{\"type\":\"link\"}],\"text\":\"Release notes\",\"type\":\"text\"}"}
!adf:carry{json="{\"marks\":[{\"type\":\"link\"}],\"text\":\"Release notes\",\"type\":\"text\"}"}
a:adf{json="{\"marks\":[{\"type\":\"em\"}],\"text\":\".\",\"type\":\"text\"}"}b:adf{json="{\"marks\":[{\"type\":\"strong\"}],\"text\":\",\",\"type\":\"text\"}"}c
a!adf:carry{json="{\"marks\":[{\"type\":\"em\"}],\"text\":\".\",\"type\":\"text\"}"}b!adf:carry{json="{\"marks\":[{\"type\":\"strong\"}],\"text\":\",\",\"type\":\"text\"}"}c
@@ -1,5 +1,5 @@
:adf{json="{\"attrs\":{\"localId\":\"01a0a067-68cf-78af-abd6-c660ec0d189b\"},\"text\":\"Owner\",\"type\":\"text\"}"} signs off.
!adf:carry{json="{\"attrs\":{\"localId\":\"01a0a067-68cf-78af-abd6-c660ec0d189b\"},\"text\":\"Owner\",\"type\":\"text\"}"} signs off.
Signed by :adf{json="{\"attrs\":{\"localId\":\"01a0a067-68d2-787e-afc5-2459776aa029\"},\"text\":\"the owner\",\"type\":\"text\"}"}
Signed by !adf:carry{json="{\"attrs\":{\"localId\":\"01a0a067-68d2-787e-afc5-2459776aa029\"},\"text\":\"the owner\",\"type\":\"text\"}"}
:adf{json="{\"attrs\":{\"localId\":\"01a0a067-68d5-7372-9962-29a039654056\"},\"text\":\"One \",\"type\":\"text\"}"}:adf{json="{\"attrs\":{\"localId\":\"01a0a067-68d5-7372-9962-29a039654056\"},\"text\":\"anchor\",\"type\":\"text\"}"}
!adf:carry{json="{\"attrs\":{\"localId\":\"01a0a067-68d5-7372-9962-29a039654056\"},\"text\":\"One \",\"type\":\"text\"}"}!adf:carry{json="{\"attrs\":{\"localId\":\"01a0a067-68d5-7372-9962-29a039654056\"},\"text\":\"anchor\",\"type\":\"text\"}"}
@@ -3,7 +3,16 @@
{
"content": [
{
"text": "Write :mention[@Mikael]{id=5b10a2} to tag someone.",
"text": "Write !adf:mention[@Mikael]{id=5b10a2} to tag someone.",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"text": "Namespaced under !adf: alone.",
"type": "text"
}
],
@@ -54,7 +63,7 @@
{
"content": [
{
"text": ":status[nope]{color=red} vs ",
"text": "!adf:status[nope]{color=red} vs ",
"type": "text"
},
{
@@ -1,4 +1,6 @@
Write \:mention[@Mikael]{id=5b10a2} to tag someone.
Write \!adf:mention[@Mikael]{id=5b10a2} to tag someone.
Namespaced under \!adf: alone.
\::syncBlock {localId=x}
@@ -6,6 +8,6 @@ Write \:mention[@Mikael]{id=5b10a2} to tag someone.
Stand-up at 10:30, shipped :tada:, {maxLevel} deep, a ::syncBlock mid-line.
:status[In review]\{color} decides.
!adf:status[In review]\{color} decides.
\:status[nope]{color=red} vs :status[yep]{color=red}
\!adf:status[nope]{color=red} vs !adf:status[yep]{color=red}
@@ -1,5 +1,5 @@
\`\` \`\``a`
:underline[\`\`]\`\``a`
!adf:underline[\`\`]\`\``a`
\`\`\\\``` ` ``\`
@@ -1,6 +1,6 @@
# Release 2.4
Shipped :emoji[🚀]{shortName=":rocket:"} on :date{timestamp=1756080000000} — :mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4} owns the rollout, status :status[In review]{color=yellow}.
Shipped !adf:emoji[🚀]{shortName=":rocket:"} on !adf:date{timestamp=1756080000000} — !adf:mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4} owns the rollout, status !adf:status[In review]{color=yellow}.
| Part | Qty |
| --- | --- |
@@ -11,7 +11,7 @@ Shipped :emoji[🚀]{shortName=":rocket:"} on :date{timestamp=1756080000000} —
- Check the collation:
- `mysqldump --default-character-set=utf8mb4`
> Rolled back once, see the :underline[postmortem].
> Rolled back once, see the !adf:underline[postmortem].
---
@@ -1,3 +1,3 @@
:underline[[http://a\`b](http://a\`b)]`c
!adf:underline[[http://a\`b](http://a\`b)]`c
:underline[[http://d\`e](http://d`e)]
!adf:underline[[http://d\`e](http://d`e)]
@@ -1,5 +1,5 @@
:textColor[[ ](\`)]{color=""}`a
!adf:textColor[[ ](\`)]{color=""}`a
:underline[[b](c "\`")]`c`
!adf:underline[[b](c "\`")]`c`
:underline[[d](e`f)]
!adf:underline[[d](e`f)]
@@ -1,7 +1,7 @@
:subsup[[a](\[)]
!adf:subsup[[a](\[)]
:underline[[b](\]) [c](x "\[")]
!adf:underline[[b](\]) [c](x "\[")]
:underline[[d](e[f])]
!adf:underline[[d](e[f])]
:underline[[http://a\[b](http://a\[b)]
!adf:underline[[http://a\[b](http://a\[b)]
@@ -1,9 +0,0 @@
:underline[[x](\:a{)]
:underline[[x](y "\:a{")]
:underline[[x](\:a[]{)]
:underline[[x](https://example.com/?q=\:emoji{)]
:underline[[ab\:c{](ab\:c{)]
@@ -9,7 +9,7 @@
},
{
"attrs": {
"href": ":a{"
"href": "!adf:a{"
},
"type": "link"
}
@@ -30,7 +30,7 @@
{
"attrs": {
"href": "y",
"title": ":a{"
"title": "!adf:a{"
},
"type": "link"
}
@@ -50,7 +50,7 @@
},
{
"attrs": {
"href": ":a[]{"
"href": "!adf:a[]{"
},
"type": "link"
}
@@ -70,7 +70,7 @@
},
{
"attrs": {
"href": "https://example.com/?q=:emoji{"
"href": "https://example.com/?q=!adf:emoji{"
},
"type": "link"
}
@@ -90,12 +90,32 @@
},
{
"attrs": {
"href": "ab:c{"
"href": "ab:!adf:c{"
},
"type": "link"
}
],
"text": "ab:c{",
"text": "ab:!adf:c{",
"type": "text"
}
],
"type": "paragraph"
},
{
"content": [
{
"marks": [
{
"type": "underline"
},
{
"attrs": {
"href": "ab:!adf:d"
},
"type": "link"
}
],
"text": "ab:!adf:d",
"type": "text"
}
],
@@ -0,0 +1,11 @@
!adf:underline[[x](\!adf:a{)]
!adf:underline[[x](y "\!adf:a{")]
!adf:underline[[x](\!adf:a[]{)]
!adf:underline[[x](https://example.com/?q=\!adf:emoji{)]
!adf:underline[[ab:\!adf:c{](ab:\!adf:c{)]
!adf:underline[[ab:\!adf:d](ab:\!adf:d)]
@@ -1,3 +1,3 @@
:adf{json="{\"marks\":[{\"attrs\":{\"href\":\"/u\"},\"type\":\"link\"},{\"type\":\"code\"}],\"text\":\"]: a\",\"type\":\"text\"}"}
!adf:carry{json="{\"marks\":[{\"attrs\":{\"href\":\"/u\"},\"type\":\"link\"},{\"type\":\"code\"}],\"text\":\"]: a\",\"type\":\"text\"}"}
:adf{json="{\"attrs\":{\"timestamp\":\"]:a\"},\"marks\":[{\"attrs\":{\"href\":\"/u\"},\"type\":\"link\"}],\"type\":\"date\"}"}
!adf:carry{json="{\"attrs\":{\"timestamp\":\"]:a\"},\"marks\":[{\"attrs\":{\"href\":\"/u\"},\"type\":\"link\"}],\"type\":\"date\"}"}
@@ -1,6 +1,6 @@
\[:date{timestamp="]:a"}
\[!adf:date{timestamp="]:a"}
> \[`]: a`
- \[:date{timestamp="]:a"}\
- \[!adf:date{timestamp="]:a"}\
b
@@ -1 +1 @@
un:adf{json="{\"marks\":[{\"type\":\"strong\"}],\"text\":\"-real\",\"type\":\"text\"}"}istic
un!adf:carry{json="{\"marks\":[{\"type\":\"strong\"}],\"text\":\"-real\",\"type\":\"text\"}"}istic
@@ -1 +1 @@
_**Ready**_:adf{json="{\"attrs\":{\"extensionKey\":\"status-lozenge\",\"extensionType\":\"com.atlassian.confluence.macro.core\"},\"marks\":[{\"type\":\"em\"},{\"type\":\"strong\"}],\"type\":\"inlineExtension\"}"}_**to ship**_
_**Ready**_!adf:carry{json="{\"attrs\":{\"extensionKey\":\"status-lozenge\",\"extensionType\":\"com.atlassian.confluence.macro.core\"},\"marks\":[{\"type\":\"em\"},{\"type\":\"strong\"}],\"type\":\"inlineExtension\"}"}_**to ship**_
@@ -1,5 +1,5 @@
un:adf{json="{\"marks\":[{\"type\":\"em\"}],\"text\":\"a\",\"type\":\"text\"}"}:adf{json="{\"marks\":[{\"type\":\"em\"},{\"type\":\"strong\"}],\"text\":\"b\",\"type\":\"text\"}"}**c**istic
un!adf:carry{json="{\"marks\":[{\"type\":\"em\"}],\"text\":\"a\",\"type\":\"text\"}"}!adf:carry{json="{\"marks\":[{\"type\":\"em\"},{\"type\":\"strong\"}],\"text\":\"b\",\"type\":\"text\"}"}**c**istic
***a*b**:adf{json="{\"marks\":[{\"type\":\"strong\"},{\"type\":\"em\"}],\"text\":\"c\",\"type\":\"text\"}"}
***a*b**!adf:carry{json="{\"marks\":[{\"type\":\"strong\"},{\"type\":\"em\"}],\"text\":\"c\",\"type\":\"text\"}"}
~~un~~:adf{json="{\"marks\":[{\"type\":\"strike\"},{\"type\":\"em\"}],\"text\":\"a\",\"type\":\"text\"}"}:adf{json="{\"marks\":[{\"type\":\"strike\"},{\"type\":\"em\"},{\"type\":\"strong\"}],\"text\":\"b\",\"type\":\"text\"}"}~~**c**istic~~
~~un~~!adf:carry{json="{\"marks\":[{\"type\":\"strike\"},{\"type\":\"em\"}],\"text\":\"a\",\"type\":\"text\"}"}!adf:carry{json="{\"marks\":[{\"type\":\"strike\"},{\"type\":\"em\"},{\"type\":\"strong\"}],\"text\":\"b\",\"type\":\"text\"}"}~~**c**istic~~
+2 -2
View File
@@ -1,5 +1,5 @@
:textColor[Overdue since :status[Blocked]{color=red} on Tuesday]{color="#ae2e24"}
!adf:textColor[Overdue since !adf:status[Blocked]{color=red} on Tuesday]{color="#ae2e24"}
_Torque the **M8 bolt** to **25 Nm**_
[Release notes :emoji[🚀]{shortName=":rocket:"}](https://example.com/release)
[Release notes !adf:emoji[🚀]{shortName=":rocket:"}](https://example.com/release)
@@ -1,4 +1,4 @@
| Cell | Note |
| --- | --- |
| :text{text=" "}lead | |
| :status[ok]{style="a\u007cb"} | trail:text{text=" "} |
| !adf:text{text=" "}lead | |
| !adf:status[ok]{style="a\u007cb"} | trail!adf:text{text=" "} |
@@ -1,6 +1,6 @@
Line one\
Line two
Trailing:hardBreak{}
Trailing!adf:hardBreak{}
## Two:hardBreak{}lines
## Two!adf:hardBreak{}lines
@@ -1,5 +1,5 @@
:status[In review]{color=yellow} carries the text in the slot.
!adf:status[In review]{color=yellow} carries the text in the slot.
:status{color=neutral} carries no text attribute.
!adf:status{color=neutral} carries no text attribute.
:status[]{color=neutral} carries the empty string.
!adf:status[]{color=neutral} carries the empty string.
+1 -1
View File
@@ -1 +1 @@
Due :date{timestamp=1756080000000}, reviewed :date{localId=01a033ab-781c-7f79-9c8a-fb63356c1cad timestamp=1735689600000}.
Due !adf:date{timestamp=1756080000000}, reviewed !adf:date{localId=01a033ab-781c-7f79-9c8a-fb63356c1cad timestamp=1735689600000}.
+1 -1
View File
@@ -1 +1 @@
Shipped :emoji[🎉]{id=1f389 localId=01a033ad-f00e-7acb-a009-37ddd25d7949 shortName=":tada:"} on Friday.
Shipped !adf:emoji[🎉]{id=1f389 localId=01a033ad-f00e-7acb-a009-37ddd25d7949 shortName=":tada:"} on Friday.
@@ -1,3 +1,3 @@
One:hardBreak{localId=01a0431d-201e-7400-a175-9c04d3628f95}Two
One!adf:hardBreak{localId=01a0431d-201e-7400-a175-9c04d3628f95}Two
Three:hardBreak{text="\n"}Four
Three!adf:hardBreak{text="\n"}Four
@@ -1,3 +1,3 @@
Tracking :inlineCard{url="https://example.com/browse/ABC-123"} this week.
Tracking !adf:inlineCard{url="https://example.com/browse/ABC-123"} this week.
:inlineCard{data="{\"@type\":\"Link\",\"name\":\"Torque spec\",\"url\":\"https://example.com/torque\"}" localId=01a033ad-1fa5-76fa-aad0-2e56a17d76aa}
!adf:inlineCard{data="{\"@type\":\"Link\",\"name\":\"Torque spec\",\"url\":\"https://example.com/torque\"}" localId=01a033ad-1fa5-76fa-aad0-2e56a17d76aa}
@@ -1 +1 @@
**:mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4}** owns the rebuild.
**!adf:mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4}** owns the rebuild.
@@ -1,3 +1,3 @@
_:underline[Rewired]_ at the mains.
_!adf:underline[Rewired]_ at the mains.
:underline[_Rewound_] by hand.
!adf:underline[_Rewound_] by hand.
+2 -2
View File
@@ -1,3 +1,3 @@
:textColor[Over]{color="#ae2e24"}:textColor[due]{color="#a54800"} since the H:subsup[2]{type=sub}O test, :underline[signed by :mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4}] on the 2:subsup[nd]{type=sup} of May.
!adf:textColor[Over]{color="#ae2e24"}!adf:textColor[due]{color="#a54800"} since the H!adf:subsup[2]{type=sub}O test, !adf:underline[signed by !adf:mention[@Mikael]{id=01a032c3-7a7c-775f-a730-2d79351338b4}] on the 2!adf:subsup[nd]{type=sup} of May.
:border[: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}
@@ -1,3 +1,3 @@
See :mediaInline{alt="Torque chart" collection=contentId-98237 height=24 id=01a033ae-69a3-7411-b388-a37f9d103ded type=file width=24} mid-sentence.
See !adf:mediaInline{alt="Torque chart" collection=contentId-98237 height=24 id=01a033ae-69a3-7411-b388-a37f9d103ded type=file width=24} mid-sentence.
Or :mediaInline{data="{\"fileName\":\"budget.pdf\",\"fileSize\":18432}" localId=01a033b1-b5a0-7a98-9e9c-48192bd30ac0 occurrenceKey=01a033ab-bab8-74b0-9ec8-b3419b000189 type=image} beside the text.
Or !adf:mediaInline{data="{\"fileName\":\"budget.pdf\",\"fileSize\":18432}" localId=01a033b1-b5a0-7a98-9e9c-48192bd30ac0 occurrenceKey=01a033ab-bab8-74b0-9ec8-b3419b000189 type=image} beside the text.
+1 -1
View File
@@ -1 +1 @@
Ping :mention[@Mikael]{accessLevel=CONTAINER id=01a032c3-7a7c-775f-a730-2d79351338b4 localId=01a033ad-ecb7-7530-aa1c-ff25031e96e2 userType=DEFAULT} before merging.
Ping !adf:mention[@Mikael]{accessLevel=CONTAINER id=01a032c3-7a7c-775f-a730-2d79351338b4 localId=01a033ad-ecb7-7530-aa1c-ff25031e96e2 userType=DEFAULT} before merging.
+1 -1
View File
@@ -1 +1 @@
The rebuild is :status[In review]{color=yellow localId=01a033a7-d68a-71a9-a58a-edf5757ad176 style=bold} until Friday.
The rebuild is !adf:status[In review]{color=yellow localId=01a033a7-d68a-71a9-a58a-edf5757ad176 style=bold} until Friday.
@@ -1,15 +1,15 @@
:text{text=" "}Two leading spaces held, and one text node split:text{text="\n"}over two lines.
!adf:text{text=" "}Two leading spaces held, and one text node split!adf:text{text="\n"}over two lines.
Trailing space held:text{text=" "}
Trailing space held!adf:text{text=" "}
:text{text="\t"}Tabbed at the block edge.
!adf:text{text="\t"}Tabbed at the block edge.
Space before the break:text{text=" "}\
Space before the break!adf:text{text=" "}\
survives.
Space after the break\
:text{text=" "}survives too.
!adf:text{text=" "}survives too.
Kept _:text{text=" "}inside:text{text=" "}_, exactly.
Kept _!adf:text{text=" "}inside!adf:text{text=" "}_, exactly.
Edged with the whitespace CommonMark keeps
@@ -56,7 +56,7 @@
{
"content": [
{
"text": ":",
"text": "!adf:",
"type": "text"
},
{
@@ -1,7 +1,7 @@
Draft: :adf{json="{\"attrs\":{\"text\":\"Add a summary\"},\"type\":\"placeholder\"}"} — expected Friday.
Draft: !adf:carry{json="{\"attrs\":{\"text\":\"Add a summary\"},\"type\":\"placeholder\"}"} — expected Friday.
**Blocked** on :adf{json="{\"attrs\":{\"extensionKey\":\"jira-issue\",\"extensionType\":\"com.atlassian.jira.macro\"},\"marks\":[{\"type\":\"strong\"}],\"type\":\"inlineExtension\"}"} until the rebuild lands.
**Blocked** on !adf:carry{json="{\"attrs\":{\"extensionKey\":\"jira-issue\",\"extensionType\":\"com.atlassian.jira.macro\"},\"marks\":[{\"type\":\"strong\"}],\"type\":\"inlineExtension\"}"} until the rebuild lands.
\::adf{json="{\"type\":\"blockCard\"}"}
\!adf:!adf:carry{json="{\"type\":\"blockCard\"}"}
**a**:adf{json="{\"marks\":[{\"type\":\"strong\"}],\"type\":\"placeholder\"}"}**b**
**a**!adf:carry{json="{\"marks\":[{\"type\":\"strong\"}],\"type\":\"placeholder\"}"}**b**
+11 -8
View File
@@ -87,8 +87,11 @@ info string, and `listBreak` for the leaf that parts two adjacent lists (Canonic
**Claiming**: an unescaped `!adf:` claims wherever it stands. What follows picks the form: `/name`
closes a container, and a name picks by what follows it in turn — a space or the line's end a block
line, `[` or `{` an inline directive. A `!adf:` completing none of the three is a named error, and
`\!adf:` is the literal, block and inline alike. A claimed block line also ends a lazy continuation:
the blockquote or list item whose paragraph CommonMark would fold it into closes instead.
`\!adf:` is the literal, block and inline alike. A construct CommonMark binds first — a code span,
an autolink URI, a link destination or title — holds its `!adf:` as content; a directive's
`[content]`, which binds ahead of bracket matching, does not. A claimed block line also ends a lazy
continuation: the blockquote or list item whose paragraph CommonMark would fold it into closes
instead.
**Inline**: `!adf:name[content]{attrs}`, on one line — an inline directive never spans lines.
`[content]` is inline markdown; brackets inside balance as in CommonMark link text, `\]` for a
@@ -416,12 +419,12 @@ Right.
## Inline nodes
Attributes and the carry fallback read as in the block sections, the carry in its inline form. Of
the nodes below, `emoji`, `mention` and `status` spell their `text` attribute in the content slot
as plain text: `[]` is the empty string, absent content is the absent attribute, non-empty content
parsing to anything but one unmarked text node — adjacent text nodes with identical marks and no
attributes merged first — is a named error, and so is a `text` key in `{attrs}`. An enclosing mark
spelling does not reach into the slot. The rest take no content, `!adf:text` included; content on a
node that takes none is a named error.
the nodes below, `emoji`, `mention` and `status` spell their `text` attribute in the content slot as
plain text: `[]` is the empty string, absent content is the absent attribute, non-empty content
parsing to anything but one text node carrying neither marks, attributes nor content — adjacent text
nodes with identical marks and no attributes merged first — is a named error, and so is a `text` key
in `{attrs}`. An enclosing mark spelling does not reach into the slot. The rest take no content,
`!adf:text` included; content on a node that takes none is a named error.
- `date` — Attributes: `localId` (string), `timestamp` (string, epoch milliseconds).
- `emoji` — Attributes: `id` (string), `localId` (string), `shortName` (string, `:name:`), `text`
+16 -12
View File
@@ -11,7 +11,8 @@ import { adfDocument, attributes, jsonKey, jsonValue, markdownPieces, propertyRu
import { adfToMarkdown } from './markdown/emit/adf-to-markdown.ts'
import { blockArgument } from './markdown/block-directive-arguments.ts'
import { blockDirectives } from './adf/block-directives.ts'
import { carryName } from './markdown/opaque-carry.ts'
import { carryFence, carryName } from './markdown/opaque-carry.ts'
import { directivePrefix, spellAttributes, spellInlineDirectiveOpener, spellJsonAttribute, spellLeafDirective, spellStringAttribute, spellVocabulary } from './markdown/directive-syntax.ts'
import { fencedCodeBlock } from './markdown/backtick-runs.ts'
import { inlineDirectives } from './adf/inline-directives.ts'
import { listBreakName } from './markdown/list-break.ts'
@@ -21,7 +22,6 @@ import { markdownToAdf } from './markdown/parse/markdown-to-adf.ts'
import { marksAttribute } from './markdown/block-directive-marks.ts'
import { nodeContent, nodeMarks } from './adf/document.ts'
import { serializeCanonicalJson } from './canonical-json.ts'
import { spellAttributes, spellJsonAttribute, spellLeafDirective, spellStringAttribute, spellVocabulary } from './markdown/directive-syntax.ts'
import { textDirectiveName } from './markdown/text-directive.ts'
import { toEditorNormal } from './adf/editor-normal.ts'
import { vocabularyPairs } from './adf/attribute-vocabulary.ts'
@@ -107,9 +107,10 @@ const autolink = fc.oneof(
)
const hostileAutolink = fc.tuple(fc.constantFrom('http://', 'ab:', 'a:'), textOf(0)).map(([scheme, rest]) => `<${scheme}${rest}>`)
const inlineHtml = fc.constantFrom('<span>', '</span>', '<a href="x">', "<b class='y'/>", '<!-- c -->', '<!---->', '<?x?>', '<![CDATA[x]]>', '<!X y>', '<br/>', '<b', '<3', '< a>')
const hardBreak = fc.constantFrom('\\\n', ' \n', '\n', ':hardBreak{}')
const textDirective = fc.constantFrom(' ', ' ', '\t', '\n', '\n\n').map((held) => `:${textDirectiveName}{${textDirectiveName}=${spellStringAttribute(held)}}`)
const hostileTextDirective = fc.constantFrom(' \n', 'a', '').map((held) => `:${textDirectiveName}{${textDirectiveName}=${spellStringAttribute(held)}}`)
const hardBreak = fc.constantFrom('\\\n', ' \n', '\n', spellLeafDirective('hardBreak', ''))
const spellTextDirective = (held: string) => spellLeafDirective(textDirectiveName, `{${textDirectiveName}=${spellStringAttribute(held)}}`)
const textDirective = fc.constantFrom(' ', ' ', '\t', '\n', '\n\n').map(spellTextDirective)
const hostileTextDirective = fc.constantFrom(' \n', 'a', '').map(spellTextDirective)
const url = fc.stringMatching(/^https?:\/\/[a-z]{1,6}\.[a-z]{2,3}(?:\/[a-z0-9()]{0,5})?$/)
const title = (text: Arbitrary<string>) => fc.oneof(fc.constant(''), text.map((held) => ` "${held}"`), text.map((held) => ` '${held}'`), text.map((held) => ` (${held})`))
@@ -163,10 +164,11 @@ const carriedNode = fc.oneof(
fc.tuple(fc.constantFrom('mention', 'paragraph', 'status', 'widget'), fc.dictionary(jsonKey, jsonValue, { maxKeys: 2, noNullPrototype: true })).map(([type, attrs]): JsonValue => ({ attrs, type })),
textOf(1).map((held): JsonValue => ({ text: held, type: 'text' })),
)
const inlineCarry = carriedNode.map((node) => `:${carryName}{json=${spellStringAttribute(serializeCanonicalJson(node, 'compact'))}}`)
const hostileInlineCarry = fc.oneof(carriedNode, jsonValue).map((node) => `:${carryName}{json=${spellStringAttribute(JSON.stringify(node, null, 1))}}`)
const blockCarry = carriedNode.map((node) => fencedCodeBlock(carryName, serializeCanonicalJson(node, 'two-space')))
const hostileBlockCarry = fc.oneof(carriedNode, jsonValue).map((node) => fencedCodeBlock(carryName, JSON.stringify(node)))
const spellCarry = (json: string) => spellLeafDirective(carryName, `{json=${spellStringAttribute(json)}}`)
const inlineCarry = carriedNode.map((node) => spellCarry(serializeCanonicalJson(node, 'compact')))
const hostileInlineCarry = fc.oneof(carriedNode, jsonValue).map((node) => spellCarry(JSON.stringify(node, null, 1)))
const blockCarry = carriedNode.map((node) => fencedCodeBlock(carryFence, serializeCanonicalJson(node, 'two-space')))
const hostileBlockCarry = fc.oneof(carriedNode, jsonValue).map((node) => fencedCodeBlock(carryFence, JSON.stringify(node)))
function prefixLines(body: string, first: string, rest: (index: number) => string): string {
return body
@@ -225,16 +227,18 @@ function inlineMarkdown(hostile: boolean): InlineMarkdown {
...Object.entries(inlineDirectives).map(([name, directive]) =>
fc
.tuple(directive.textAttribute === undefined ? fc.constant(null) : fc.option(hostile ? word : prose), tableAttributes(directive.attributes, directive.textAttribute))
.map(([slot, attrs]) => (slot === null ? spellLeafDirective(name, attrs) : `:${name}[${slot}]${attrs}`)),
.map(([slot, attrs]) => (slot === null ? spellLeafDirective(name, attrs) : `${spellInlineDirectiveOpener(name)}${slot}]${attrs}`)),
),
...Object.entries(markAttributes)
.filter(([name]) => hostile || markSpelling(name)?.kind === 'directive')
.map(([name, vocabulary]) => fc.tuple(tie('inlines'), tableAttributes(vocabulary)).map(([content, attrs]) => `:${name}[${content}]${attrs}`)),
.map(([name, vocabulary]) => fc.tuple(tie('inlines'), tableAttributes(vocabulary)).map(([content, attrs]) => `${spellInlineDirectiveOpener(name)}${content}]${attrs}`)),
),
weight: 2,
},
{
arbitrary: fc.tuple(directiveName, fc.option(tie('inlines'), { freq: 3 }), hostileAttributes).map(([name, content, attrs]) => `:${name}${content === null ? '' : `[${content}]`}${attrs}`),
arbitrary: fc
.tuple(directiveName, fc.option(tie('inlines'), { freq: 3 }), hostileAttributes)
.map(([name, content, attrs]) => `${directivePrefix}${name}${content === null ? '' : `[${content}]`}${attrs}`),
hostile: true,
weight: 2,
},
+2 -2
View File
@@ -1,5 +1,5 @@
import type { JsonValue } from '../json-value.ts'
import { carryName } from './opaque-carry.ts'
import { carryFence } from './opaque-carry.ts'
import { holdsControlCharacter } from './commonmark-grammar.ts'
import { holdsEntityReference } from './entity-references.ts'
@@ -8,7 +8,7 @@ export type LanguageSlot = { info: string; kind: 'fence' } | { kind: 'attribute'
// spec/flavour.md, The CommonMark blocks: the one slot a codeBlock's language rides.
export function languageSlot(language: JsonValue | undefined): LanguageSlot {
if (language === undefined) return { kind: 'none' }
if (typeof language !== 'string' || language === '' || language === carryName) return { kind: 'attribute' }
if (typeof language !== 'string' || language === '' || language === carryFence) return { kind: 'attribute' }
if (/[`\\]/.test(language) || holdsControlCharacter(language) || language !== language.trim() || holdsEntityReference(language)) return { kind: 'attribute' }
return { info: language, kind: 'fence' }
}
+30 -28
View File
@@ -73,8 +73,8 @@ test('spells an empty {attrs} only where the brace itself claims the directive',
const omitted = 'an empty {attrs} is omitted unless the { itself claims the directive: this one spells {}'
assert.equal(fault('::rule {}'), omitted)
assert.equal(fault(':::panel info {}'), omitted)
assert.equal(inline(':underline[a]{}'), omitted)
spans(':hardBreak{}', 'hardBreak', undefined)
assert.equal(inline('!adf:underline[a]{}'), omitted)
spans('!adf:hardBreak{}', 'hardBreak', undefined)
})
test('names the directive line no spelling reads', () => {
@@ -98,7 +98,7 @@ test('names the attributes no spelling reads', () => {
assert.equal(fault('::panel { a=1}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\::: keeps the line literal text')
assert.equal(fault('::panel {a=1 }'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\::: keeps the line literal text')
assert.equal(fault('::panel {a=1 a=2}'), 'the attribute key a is spelled twice')
assert.equal(inline(':mention[@A]{id}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\: keeps the colon literal')
assert.equal(inline('!adf:mention[@A]{id}'), 'an attribute reads key=value, the value bare or double-quoted: this one does not; \\!adf: keeps the prefix literal')
})
test('breaks the directive on the raw characters a quoted value spells as escapes', () => {
@@ -108,41 +108,43 @@ test('breaks the directive on the raw characters a quoted value spells as escape
assert.equal(fault('::panel {a="x|y"}'), 'a raw | inside {attrs} breaks the directive: spell it \\u007c')
})
test('reads an inline directive only where a bracket or a brace follows the name', () => {
test('claims at the prefix, and reads a directive only where a bracket or a brace follows the name', () => {
const completes = 'an unescaped !adf: completes no directive; \\!adf: keeps the prefix literal'
assert.equal(inline('Part.'), 'unclaimed')
assert.equal(inline(':10:30'), 'unclaimed')
assert.equal(inline(':smile:'), 'unclaimed')
assert.equal(inline(':Mention[@A]'), 'unclaimed')
assert.equal(inline(':mention @A'), 'unclaimed')
spans(':mention[@A]', 'mention', '@A')
spans(':date{timestamp=1756080000000}', 'date', undefined, ['timestamp', '1756080000000'])
spans(':emoji[]{shortName=":tada:"}', 'emoji', '', ['shortName', ':tada:', '":tada:"'])
spans(':underline[ a ]', 'underline', ' a ')
assert.equal(inline(':mention[@A]'), 'unclaimed')
assert.equal(inline('!adfx:mention[@A]'), 'unclaimed')
assert.equal(inline('!adf:'), completes)
assert.equal(inline('!adf:Mention[@A]'), completes)
assert.equal(inline('!adf:mention @A'), completes)
spans('!adf:mention[@A]', 'mention', '@A')
spans('!adf:date{timestamp=1756080000000}', 'date', undefined, ['timestamp', '1756080000000'])
spans('!adf:emoji[]{shortName=":tada:"}', 'emoji', '', ['shortName', ':tada:', '":tada:"'])
spans('!adf:underline[ a ]', 'underline', ' a ')
})
test('binds an inline directive as a unit, its content balancing brackets like link text', () => {
spans(':underline[a [b] c]', 'underline', 'a [b] c')
spans(':underline[a \\] b]', 'underline', 'a \\] b')
spans(':underline[a `]` b]', 'underline', 'a `]` b')
spans(':underline[a `b c]', 'underline', 'a `b c')
spans(':underline[:status[x]{color=red}]', 'underline', ':status[x]{color=red}')
spans(':status[x]{color=red style="bold "}', 'status', 'x', ['color', 'red'], ['style', 'bold ', '"bold "'])
assert.deepEqual(inline(':underline[a] {}'), { attributes: attributes(), content: 'a', length: 13, name: 'underline' })
assert.deepEqual(inline(':text{text=" "} and more'), { attributes: attributes(['text', ' ', '" "']), content: undefined, length: 15, name: 'text' })
spans('!adf:underline[a [b] c]', 'underline', 'a [b] c')
spans('!adf:underline[a \\] b]', 'underline', 'a \\] b')
spans('!adf:underline[a `]` b]', 'underline', 'a `]` b')
spans('!adf:underline[a `b c]', 'underline', 'a `b c')
spans('!adf:underline[!adf:status[x]{color=red}]', 'underline', '!adf:status[x]{color=red}')
spans('!adf:status[x]{color=red style="bold "}', 'status', 'x', ['color', 'red'], ['style', 'bold ', '"bold "'])
assert.deepEqual(inline('!adf:underline[a] {}'), { attributes: attributes(), content: 'a', length: 17, name: 'underline' })
assert.deepEqual(inline('!adf:text{text=" "} and more'), { attributes: attributes(['text', ' ', '" "']), content: undefined, length: 19, name: 'text' })
})
test('names the inline directive left unclosed at the end of its line', () => {
assert.equal(inline(':mention[@A'), 'an inline directive [content] is unclosed; \\: keeps the colon literal')
assert.equal(inline(':mention[@A\nB]'), 'an inline directive [content] is unclosed; \\: keeps the colon literal')
assert.equal(inline(':mention[a `b\nc` d]'), 'an inline directive [content] is unclosed; \\: keeps the colon literal')
assert.equal(inline(':underline[:status[x'), 'an inline directive [content] is unclosed; \\: keeps the colon literal')
assert.equal(inline(':mention[@A]{id=1'), 'the {attrs} closing brace is missing')
assert.equal(inline(':mention{id=1'), 'the {attrs} closing brace is missing')
assert.equal(inline(':text{text="a\nb"}'), 'the {attrs} quoted value is not a JSON string')
assert.equal(inline('!adf:mention[@A'), 'an inline directive [content] is unclosed; \\!adf: keeps the prefix literal')
assert.equal(inline('!adf:mention[@A\nB]'), 'an inline directive [content] is unclosed; \\!adf: keeps the prefix literal')
assert.equal(inline('!adf:mention[a `b\nc` d]'), 'an inline directive [content] is unclosed; \\!adf: keeps the prefix literal')
assert.equal(inline('!adf:underline[!adf:status[x'), 'an inline directive [content] is unclosed; \\!adf: keeps the prefix literal')
assert.equal(inline('!adf:mention[@A]{id=1'), 'the {attrs} closing brace is missing')
assert.equal(inline('!adf:mention{id=1'), 'the {attrs} closing brace is missing')
assert.equal(inline('!adf:text{text="a\nb"}'), 'the {attrs} quoted value is not a JSON string')
})
test('refuses inline directives nested deeper than the parser carries', () => {
const nest = (depth: number): string => `${':underline['.repeat(depth)}x${']'.repeat(depth)}`
const nest = (depth: number): string => `${'!adf:underline['.repeat(depth)}x${']'.repeat(depth)}`
spans(nest(largestNesting), 'underline', nest(largestNesting - 1))
assert.equal(inline(nest(largestNesting + 1)), `the input nests inline directives deeper than the ${largestNesting} levels the parser carries`)
})
+21 -14
View File
@@ -26,11 +26,13 @@ type Attributes = { attributes: DirectiveAttributes; length: number }
type AttributePair = { end: number; key: string; value: DirectiveValue }
export const directivePrefix = '!adf:'
const bareTokenSource = '[A-Za-z0-9_-]+'
const bareRun = new RegExp(bareTokenSource, 'y')
const bareToken = new RegExp(`^${bareTokenSource}$`)
const directiveName = /[a-z][A-Za-z0-9]*/y
const inlineDirectiveOpener = /:[a-z][A-Za-z0-9]*[[{]/y
const inlineDirectiveOpener = new RegExp(`${directivePrefix}[a-z][A-Za-z0-9]*[[{]`, 'y')
const lineEnd = /^[ \t]*$/
// spec/flavour.md, Attributes.
const reservedSource = '[&<`|]'
@@ -39,12 +41,13 @@ const rawReserved = new RegExp(reservedSource)
const noAttributes: DirectiveAttributes = new Map()
export const directiveLineEscape = '\\::: keeps the line literal text'
export const inlineDirectiveEscape = '\\: keeps the colon literal'
export const inlineDirectiveEscape = `\\${directivePrefix} keeps the prefix literal`
const emptyFault = 'an empty {attrs} is omitted unless the { itself claims the directive: this one spells {}'
const nameFault = `a directive name reads [a-z][A-Za-z0-9]*: this one does not; ${directiveLineEscape}`
const orderFault = 'the {attrs} keys read in alphabetical order'
const pairFault = 'an attribute reads key=value, the value bare or double-quoted: this one does not'
const prefixFault = `an unescaped ${directivePrefix} completes no directive; ${inlineDirectiveEscape}`
const shapeFault = `a directive line reads a name, one bare argument and {attrs}, one space apart: this one does not; ${directiveLineEscape}`
export function attributeValue(text: string, kind: AttributeKind): AttributeReading {
@@ -56,9 +59,12 @@ export function attributeValue(text: string, kind: AttributeKind): AttributeRead
return overNested(parsed) ? { refusal: 'nesting' } : { value: { kind, value: parsed } }
}
export function holdsInlineDirectiveOpener(text: string): boolean {
for (let index = text.indexOf(':'); index !== -1; index = text.indexOf(':', index + 1)) if (opensInlineDirective(text, index)) return true
return false
export function claimsDirectivePrefix(text: string, index: number): boolean {
return text.startsWith(directivePrefix, index)
}
export function holdsDirectivePrefix(text: string): boolean {
return text.includes(directivePrefix)
}
export function isBareToken(text: string): boolean {
@@ -69,10 +75,6 @@ export function malformedDirective(message: string): ConvertFault {
return { code: 'malformed-directive', message }
}
export function opensInlineDirective(text: string, index: number): boolean {
return inlineDirectiveName(text, index) !== undefined
}
export function readDirectiveLine(line: string): Read<DirectiveLine> | undefined {
if (!claimsDirectiveLine(line)) return undefined
const colons = runLength(line, 0)
@@ -112,8 +114,12 @@ export function spellJsonAttribute(value: JsonValue): string {
return quote(serializeCanonicalJson(value, 'compact'))
}
export function spellInlineDirectiveOpener(name: string): string {
return `${directivePrefix}${name}[`
}
export function spellLeafDirective(name: string, attributes: string): string {
return `:${name}${attributes === '' ? '{}' : attributes}`
return `${directivePrefix}${name}${attributes === '' ? '{}' : attributes}`
}
export function spellStringAttribute(text: string): string {
@@ -159,7 +165,7 @@ function escapeDigits(character: string): string {
function inlineDirectiveName(text: string, index: number): string | undefined {
inlineDirectiveOpener.lastIndex = index
const opened = inlineDirectiveOpener.exec(text)?.[0]
return opened === undefined ? undefined : opened.slice(1, -1)
return opened === undefined ? undefined : opened.slice(directivePrefix.length, -1)
}
function readDirectiveHeader(rest: string): Read<{ argument: string | undefined; attributes: DirectiveAttributes; name: string }> {
@@ -187,12 +193,13 @@ function readDirectiveHeader(rest: string): Read<{ argument: string | undefined;
}
function readNestedDirective(text: string, index: number, depth: number): Read<DirectiveSpan> | undefined {
if (!claimsDirectivePrefix(text, index)) return undefined
const name = inlineDirectiveName(text, index)
if (name === undefined) return undefined
if (name === undefined) return { fault: malformedDirective(prefixFault) }
if (depth > largestNesting) {
return { fault: { code: 'unsupported-nesting-depth', message: `the input nests inline directives deeper than the ${largestNesting} levels the parser carries` } }
}
let cursor = index + 1 + name.length
let cursor = index + directivePrefix.length + name.length
let content: string | undefined
if (text.charAt(cursor) === '[') {
const end = readDirectiveContent(text, cursor + 1, depth)
@@ -227,7 +234,7 @@ function readDirectiveContent(text: string, start: number, depth: number): Read<
cursor = span
continue
}
const nested = character === ':' ? readNestedDirective(text, cursor, depth + 1) : undefined
const nested = character === '!' ? readNestedDirective(text, cursor, depth + 1) : undefined
if (nested !== undefined) {
if (nested.fault !== undefined) return { fault: nested.fault }
cursor += nested.value.length
+58 -56
View File
@@ -52,14 +52,14 @@ test('refuses a document version the markdown cannot carry', () => {
test('carries a text node attribute no spelling holds', () => {
assert.equal(
markdown(adfToMarkdown(document(paragraph({ attrs: { localId: 'a' }, text: 'x', type: 'text' })))),
':adf{json="{\\"attrs\\":{\\"localId\\":\\"a\\"},\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
'!adf:carry{json="{\\"attrs\\":{\\"localId\\":\\"a\\"},\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
)
})
test('spells a CommonMark block as a directive where its own spelling holds neither attribute nor mark', () => {
assert.equal(markdown(adfToMarkdown(document({ attrs: { localId: 'a' }, type: 'paragraph' }))), '::paragraph {localId=a}\n')
assert.equal(markdown(adfToMarkdown(document({ attrs: { wrap: true }, type: 'codeBlock' }))), ':::codeBlock {wrap=true}\n```\n```\n:::\n')
assert.equal(markdown(adfToMarkdown(document(paragraph({ attrs: { localId: 'a' }, type: 'hardBreak' }, { text: 'x', type: 'text' })))), ':hardBreak{localId=a}x\n')
assert.equal(markdown(adfToMarkdown(document(paragraph({ attrs: { localId: 'a' }, type: 'hardBreak' }, { text: 'x', type: 'text' })))), '!adf:hardBreak{localId=a}x\n')
assert.equal(markdown(adfToMarkdown(document({ marks: [{ type: 'border' }], type: 'blockquote' }))), ':::blockquote {marks="[{\\"type\\":\\"border\\"}]"}\n:::\n')
assert.equal(markdown(adfToMarkdown(document({ type: 'listItem' }))), ':::listItem\n:::\n')
})
@@ -114,26 +114,26 @@ 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' }))))
assert.equal(
carried({ attrs: { href: 'x', id: 'y' }, type: 'link' }),
':adf{json="{\\"marks\\":[{\\"attrs\\":{\\"href\\":\\"x\\",\\"id\\":\\"y\\"},\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n',
'!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"href\\":\\"x\\",\\"id\\":\\"y\\"},\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n',
)
assert.equal(carried({ attrs: { href: 4 }, type: 'link' }), ':adf{json="{\\"marks\\":[{\\"attrs\\":{\\"href\\":4},\\"type\\":\\"link\\"}],\\"text\\":\\"t\\",\\"type\\":\\"text\\"}"}\n')
assert.equal(carried({ type: 'link' }), ':adf{json="{\\"marks\\":[{\\"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')
})
test('carries a mark the canonical spellings cannot nest', () => {
const carried = (...marks: AdfMark[]): string => markdown(adfToMarkdown(document(paragraph({ marks, text: 'x', type: 'text' }))))
assert.equal(carried({ type: 'annotation' }), ':adf{json="{\\"marks\\":[{\\"type\\":\\"annotation\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n')
assert.equal(carried({ type: 'annotation' }), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"annotation\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n')
assert.equal(
carried({ type: 'code' }, { type: 'strong' }),
':adf{json="{\\"marks\\":[{\\"type\\":\\"code\\"},{\\"type\\":\\"strong\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
'!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"code\\"},{\\"type\\":\\"strong\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
)
assert.equal(
carried({ attrs: { colour: 'red' }, type: 'em' }),
':adf{json="{\\"marks\\":[{\\"attrs\\":{\\"colour\\":\\"red\\"},\\"type\\":\\"em\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
'!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"colour\\":\\"red\\"},\\"type\\":\\"em\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
)
assert.equal(
carried({ attrs: { localId: 'x' }, type: 'code' }),
':adf{json="{\\"marks\\":[{\\"attrs\\":{\\"localId\\":\\"x\\"},\\"type\\":\\"code\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
'!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"localId\\":\\"x\\"},\\"type\\":\\"code\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
)
})
@@ -165,7 +165,7 @@ test('parts two adjacent lists of the same kind, the marker spelling being what
test('carries a node type no section spells', () => {
assert.equal(markdown(adfToMarkdown(document({ type: 'blockCard' }))), '```adf\n{\n "type": "blockCard"\n}\n```\n')
assert.equal(markdown(adfToMarkdown(document({ type: 'toString' }))), '```adf\n{\n "type": "toString"\n}\n```\n')
assert.equal(markdown(adfToMarkdown(document(paragraph({ type: 'blockCard' })))), ':adf{json="{\\"type\\":\\"blockCard\\"}"}\n')
assert.equal(markdown(adfToMarkdown(document(paragraph({ type: 'blockCard' })))), '!adf:carry{json="{\\"type\\":\\"blockCard\\"}"}\n')
assert.equal(markdown(adfToMarkdown(document({ text: 'x', type: 'text' }))), '```adf\n{\n "text": "x",\n "type": "text"\n}\n```\n')
assert.equal(markdown(adfToMarkdown(document({ type: 'hardBreak' }))), '```adf\n{\n "type": "hardBreak"\n}\n```\n')
})
@@ -179,7 +179,7 @@ test('breaks a mark run at the node it carries', () => {
const carried: AdfNode = { marks: [strong], type: 'placeholder' }
assert.equal(
markdown(adfToMarkdown(document(paragraph({ marks: [strong], text: 'a', type: 'text' }, carried, { marks: [strong], text: 'b', type: 'text' })))),
'**a**:adf{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"type\\":\\"placeholder\\"}"}**b**\n',
'**a**!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"type\\":\\"placeholder\\"}"}**b**\n',
)
})
@@ -225,7 +225,7 @@ test('spells an ordered list no marker fits as a directive', () => {
test('carries a code mark over anything but text', () => {
assert.equal(
markdown(adfToMarkdown(document(paragraph({ marks: [{ type: 'code' }], type: 'hardBreak' })))),
':adf{json="{\\"marks\\":[{\\"type\\":\\"code\\"}],\\"type\\":\\"hardBreak\\"}"}\n',
'!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"code\\"}],\\"type\\":\\"hardBreak\\"}"}\n',
)
})
@@ -257,7 +257,9 @@ test('escapes only text that would otherwise open a construct', () => {
assert.equal(markdown(adfToMarkdown(document(later))), 'a\\\n\\<div\n')
assert.equal(markdown(adfToMarkdown(document({ content: [paragraph({ text: '<!-- x', type: 'text' })], type: 'blockquote' }))), '> \\<!-- x\n')
assert.equal(emitted('| a | b |'), '\\| a | b |\n')
assert.equal(emitted(':mention[@x]{id=1}'), '\\:mention[@x]{id=1}\n')
assert.equal(emitted('!adf:mention[@x]{id=1}'), '\\!adf:mention[@x]{id=1}\n')
assert.equal(emitted('!adf: completes nothing'), '\\!adf: completes nothing\n')
assert.equal(emitted('!adfx and a! alone'), '!adfx and a! alone\n')
assert.equal(emitted(':::panel info'), '\\:::panel info\n')
assert.equal(emitted('10:30 tomorrow'), '10:30 tomorrow\n')
assert.equal(emitted('[a](b)'), '\\[a](b)\n')
@@ -291,15 +293,15 @@ test('carries a mark spelling that cannot open or close where it sits', () => {
const strong: AdfMark = { type: 'strong' }
assert.equal(
emitted({ text: 'un', type: 'text' }, marked('-real', strong), { text: 'istic', type: 'text' }),
'un:adf{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"text\\":\\"-real\\",\\"type\\":\\"text\\"}"}istic\n',
'un!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"text\\":\\"-real\\",\\"type\\":\\"text\\"}"}istic\n',
)
assert.equal(
emitted(marked('C++', { type: 'em' }), { text: 'ish', type: 'text' }),
':adf{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\"C++\\",\\"type\\":\\"text\\"}"}ish\n',
'!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\"C++\\",\\"type\\":\\"text\\"}"}ish\n',
)
assert.equal(
emitted({ text: 'x', type: 'text' }, marked('.a', strong)),
'x:adf{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"text\\":\\".a\\",\\"type\\":\\"text\\"}"}\n',
'x!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"text\\":\\".a\\",\\"type\\":\\"text\\"}"}\n',
)
assert.equal(emitted({ text: 'un ', type: 'text' }, marked('-real', strong), { text: ' istic', type: 'text' }), 'un **-real** istic\n')
assert.equal(emitted(marked('a.', strong)), '**a.**\n')
@@ -307,7 +309,7 @@ test('carries a mark spelling that cannot open or close where it sits', () => {
const em: AdfMark = { type: 'em' }
assert.equal(
emitted({ text: 'x', type: 'text' }, marked('a.', em), marked('b', strong)),
'x:adf{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\"a.\\",\\"type\\":\\"text\\"}"}**b**\n',
'x!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\"a.\\",\\"type\\":\\"text\\"}"}**b**\n',
)
assert.equal(emitted({ text: 'x', type: 'text' }, marked('ab', em, strong), { text: 'y', type: 'text' }), 'x***ab***y\n')
})
@@ -376,20 +378,20 @@ test('escapes a literal delimiter that would merge with an emitted one', () => {
assert.equal(emitted(marked('~a', { type: 'strike' })), '~~\\~a~~\n')
assert.equal(
emitted({ text: 'x', type: 'text' }, marked('~a', { type: 'strike' })),
'x:adf{json="{\\"marks\\":[{\\"type\\":\\"strike\\"}],\\"text\\":\\"~a\\",\\"type\\":\\"text\\"}"}\n',
'x!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strike\\"}],\\"text\\":\\"~a\\",\\"type\\":\\"text\\"}"}\n',
)
assert.equal(emitted({ text: '`', type: 'text' }, marked('x', { type: 'code' })), '\\``x`\n')
assert.equal(emitted(marked('x', { type: 'code' }), { text: '`', type: 'text' }), '`x`\\`\n')
assert.equal(emitted({ text: '`a', type: 'text' }, marked('b', { type: 'code' })), '\\`a`b`\n')
assert.equal(
emitted({ text: '<https://example.com/', type: 'text' }, { attrs: { url: 'x>' }, type: 'inlineCard' }),
'\\<https://example.com/:inlineCard{url="x>"}\n',
'\\<https://example.com/!adf:inlineCard{url="x>"}\n',
)
assert.equal(emitted({ text: '!', type: 'text' }, marked('x', { attrs: { href: 'https://example.com/' }, type: 'link' })), '\\![x](https://example.com/)\n')
assert.equal(emitted({ text: '[a', type: 'text' }, marked('x', { attrs: { href: 'https://example.com/' }, type: 'link' })), '[a[x](https://example.com/)\n')
assert.equal(emitted({ text: '[a](b)', type: 'text' }), '\\[a](b)\n')
assert.equal(emitted(marked('x', { type: 'underline' }), { text: '{}', type: 'text' }), ':underline[x]\\{}\n')
assert.equal(emitted({ attrs: { text: '' }, type: 'status' }, { text: '{color=red}', type: 'text' }), ':status[]\\{color=red}\n')
assert.equal(emitted(marked('x', { type: 'underline' }), { text: '{}', type: 'text' }), '!adf:underline[x]\\{}\n')
assert.equal(emitted({ attrs: { text: '' }, type: 'status' }, { text: '{color=red}', type: 'text' }), '!adf:status[]\\{color=red}\n')
assert.equal(emitted(marked('x', { attrs: { href: 'https://example.com/' }, type: 'link' }), { text: '{}', type: 'text' }), '[x](https://example.com/){}\n')
})
@@ -457,11 +459,11 @@ test('carries a mark run whose edge holds whitespace CommonMark flanking counts'
const em = { type: 'em' }
assert.equal(
markdown(adfToMarkdown(document(paragraph({ marks: [em], text: 'a', type: 'text' }, { marks: [em], type: 'hardBreak' }, { text: 'b', type: 'text' })))),
':adf{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\"a\\",\\"type\\":\\"text\\"}"}:adf{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"type\\":\\"hardBreak\\"}"}b\n',
'!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\"a\\",\\"type\\":\\"text\\"}"}!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"type\\":\\"hardBreak\\"}"}b\n',
)
assert.equal(
markdown(adfToMarkdown(document(paragraph({ marks: [em], text: '\u00a0a', type: 'text' })))),
':adf{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\"\u00a0a\\",\\"type\\":\\"text\\"}"}\n',
'!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\"\u00a0a\\",\\"type\\":\\"text\\"}"}\n',
)
})
@@ -599,11 +601,11 @@ test('spells a table as a pipe table only where every row and cell is plain', ()
assert.ok(directive(adfToMarkdown(table(row(cell('tableHeader', { attrs: { localId: 'a' }, type: 'paragraph' }))))))
assert.equal(
markdown(adfToMarkdown(table(row(cell('tableHeader', { content: [{ attrs: { url: 'a|b' }, type: 'blockCard' }], type: 'paragraph' }))))),
'| :adf{json="{\\"attrs\\":{\\"url\\":\\"a\\u007cb\\"},\\"type\\":\\"blockCard\\"}"} |\n| --- |\n',
'| !adf:carry{json="{\\"attrs\\":{\\"url\\":\\"a\\u007cb\\"},\\"type\\":\\"blockCard\\"}"} |\n| --- |\n',
)
assert.equal(markdown(adfToMarkdown(table(row(cell('tableHeader', { content: [{ text: '\fa', type: 'text' }], type: 'paragraph' }))))), '| \fa |\n| --- |\n')
assert.ok(directive(adfToMarkdown(table(row(cell('tableHeader', { attrs: { level: 1 }, type: 'heading' }))))))
assert.equal(markdown(adfToMarkdown(table(row(cell('tableHeader', { content: [{ text: ' a', type: 'text' }], type: 'paragraph' }))))), '| :text{text=" "}a |\n| --- |\n')
assert.equal(markdown(adfToMarkdown(table(row(cell('tableHeader', { content: [{ text: ' a', type: 'text' }], type: 'paragraph' }))))), '| !adf:text{text=" "}a |\n| --- |\n')
const marked = (mark: AdfMark): AdfDocument => table(row(cell('tableHeader', { content: [{ marks: [mark], text: 'l', type: 'text' }], type: 'paragraph' })))
assert.ok(directive(adfToMarkdown(marked({ attrs: { href: 'https://example.com/?x|y' }, type: 'link' }))))
assert.ok(directive(adfToMarkdown(marked({ attrs: { href: 'https://example.com/', title: 'a|b' }, type: 'link' }))))
@@ -614,23 +616,23 @@ test('spells a table as a pipe table only where every row and cell is plain', ()
test('spells an inline node as a directive with its content slot and attributes', () => {
const emitted = (node: AdfNode): string => markdown(adfToMarkdown(document(paragraph(node))))
assert.equal(emitted({ attrs: { timestamp: '1756080000000' }, type: 'date' }), ':date{timestamp=1756080000000}\n')
assert.equal(emitted({ type: 'mention' }), ':mention{}\n')
assert.equal(emitted({ attrs: { text: '' }, type: 'status' }), ':status[]\n')
assert.equal(emitted({ attrs: { color: 'yellow', text: 'In review' }, type: 'status' }), ':status[In review]{color=yellow}\n')
assert.equal(emitted({ attrs: { id: '1f389', text: 'a]b' }, type: 'emoji' }), ':emoji[a\\]b]{id=1f389}\n')
assert.equal(emitted({ attrs: { data: { url: 'https://example.com/' } }, type: 'inlineCard' }), ':inlineCard{data="{\\"url\\":\\"https://example.com/\\"}"}\n')
assert.equal(emitted({ attrs: { height: 24 }, type: 'mediaInline' }), ':mediaInline{height=24}\n')
assert.equal(emitted({ attrs: { url: 'a`b&c<d|e' }, type: 'inlineCard' }), ':inlineCard{url="a\\u0060b\\u0026c\\u003cd\\u007ce"}\n')
assert.equal(emitted({ attrs: { timestamp: '1756080000000' }, type: 'date' }), '!adf:date{timestamp=1756080000000}\n')
assert.equal(emitted({ type: 'mention' }), '!adf:mention{}\n')
assert.equal(emitted({ attrs: { text: '' }, type: 'status' }), '!adf:status[]\n')
assert.equal(emitted({ attrs: { color: 'yellow', text: 'In review' }, type: 'status' }), '!adf:status[In review]{color=yellow}\n')
assert.equal(emitted({ attrs: { id: '1f389', text: 'a]b' }, type: 'emoji' }), '!adf:emoji[a\\]b]{id=1f389}\n')
assert.equal(emitted({ attrs: { data: { url: 'https://example.com/' } }, type: 'inlineCard' }), '!adf:inlineCard{data="{\\"url\\":\\"https://example.com/\\"}"}\n')
assert.equal(emitted({ attrs: { height: 24 }, type: 'mediaInline' }), '!adf:mediaInline{height=24}\n')
assert.equal(emitted({ attrs: { url: 'a`b&c<d|e' }, type: 'inlineCard' }), '!adf:inlineCard{url="a\\u0060b\\u0026c\\u003cd\\u007ce"}\n')
})
test('carries an inline node attribute no section spells', () => {
const carried = (node: AdfNode): string => markdown(adfToMarkdown(document(paragraph(node))))
assert.equal(carried({ attrs: { rounded: true }, type: 'status' }), ':adf{json="{\\"attrs\\":{\\"rounded\\":true},\\"type\\":\\"status\\"}"}\n')
assert.equal(carried({ attrs: { toString: 'x' }, type: 'status' }), ':adf{json="{\\"attrs\\":{\\"toString\\":\\"x\\"},\\"type\\":\\"status\\"}"}\n')
assert.equal(carried({ attrs: { color: 4 }, type: 'status' }), ':adf{json="{\\"attrs\\":{\\"color\\":4},\\"type\\":\\"status\\"}"}\n')
assert.equal(carried({ attrs: { width: '2' }, type: 'mediaInline' }), ':adf{json="{\\"attrs\\":{\\"width\\":\\"2\\"},\\"type\\":\\"mediaInline\\"}"}\n')
assert.equal(carried({ attrs: { text: 4 }, type: 'status' }), ':adf{json="{\\"attrs\\":{\\"text\\":4},\\"type\\":\\"status\\"}"}\n')
assert.equal(carried({ attrs: { rounded: true }, type: 'status' }), '!adf:carry{json="{\\"attrs\\":{\\"rounded\\":true},\\"type\\":\\"status\\"}"}\n')
assert.equal(carried({ attrs: { toString: 'x' }, type: 'status' }), '!adf:carry{json="{\\"attrs\\":{\\"toString\\":\\"x\\"},\\"type\\":\\"status\\"}"}\n')
assert.equal(carried({ attrs: { color: 4 }, type: 'status' }), '!adf:carry{json="{\\"attrs\\":{\\"color\\":4},\\"type\\":\\"status\\"}"}\n')
assert.equal(carried({ attrs: { width: '2' }, type: 'mediaInline' }), '!adf:carry{json="{\\"attrs\\":{\\"width\\":\\"2\\"},\\"type\\":\\"mediaInline\\"}"}\n')
assert.equal(carried({ attrs: { text: 4 }, type: 'status' }), '!adf:carry{json="{\\"attrs\\":{\\"text\\":4},\\"type\\":\\"status\\"}"}\n')
})
test('refuses the content and slot an inline directive has no room for', () => {
@@ -648,18 +650,18 @@ test('spells the directive marks around the longest run they cover', () => {
const marked = (text: string, ...marks: AdfMark[]): AdfNode => ({ marks, text, type: 'text' })
const emitted = (...content: AdfNode[]): string => markdown(adfToMarkdown(document(paragraph(...content))))
const underline: AdfMark = { type: 'underline' }
assert.equal(emitted(marked('x', underline)), ':underline[x]\n')
assert.equal(emitted(marked('a', underline), marked('b', underline)), ':underline[ab]\n')
assert.equal(emitted(marked('x', { attrs: { type: 'sub' }, type: 'subsup' })), ':subsup[x]{type=sub}\n')
assert.equal(emitted(marked('x', { attrs: { color: '#ae2e24' }, type: 'textColor' })), ':textColor[x]{color="#ae2e24"}\n')
assert.equal(emitted(marked('x', { attrs: { color: '#091e42', size: 2 }, type: 'border' })), ':border[x]{color="#091e42" size=2}\n')
assert.equal(emitted(marked('x', { type: 'em' }, underline)), '_:underline[x]_\n')
assert.equal(emitted(marked('x', underline, { type: 'em' })), ':underline[_x_]\n')
assert.equal(emitted(marked('a', underline), { marks: [underline], type: 'hardBreak' }, marked('b', underline)), ':underline[a:hardBreak{}b]\n')
assert.equal(emitted(marked('x', underline)), '!adf:underline[x]\n')
assert.equal(emitted(marked('a', underline), marked('b', underline)), '!adf:underline[ab]\n')
assert.equal(emitted(marked('x', { attrs: { type: 'sub' }, type: 'subsup' })), '!adf:subsup[x]{type=sub}\n')
assert.equal(emitted(marked('x', { attrs: { color: '#ae2e24' }, type: 'textColor' })), '!adf:textColor[x]{color="#ae2e24"}\n')
assert.equal(emitted(marked('x', { attrs: { color: '#091e42', size: 2 }, type: 'border' })), '!adf:border[x]{color="#091e42" size=2}\n')
assert.equal(emitted(marked('x', { type: 'em' }, underline)), '_!adf:underline[x]_\n')
assert.equal(emitted(marked('x', underline, { type: 'em' })), '!adf:underline[_x_]\n')
assert.equal(emitted(marked('a', underline), { marks: [underline], type: 'hardBreak' }, marked('b', underline)), '!adf:underline[a!adf:hardBreak{}b]\n')
const link: AdfMark = { attrs: { href: 'https://example.com/' }, type: 'link' }
assert.equal(
emitted(marked('a', underline, link), { marks: [underline, link], type: 'hardBreak' }, marked('b', underline, link)),
':underline[[a:hardBreak{}b](https://example.com/)]\n',
'!adf:underline[[a!adf:hardBreak{}b](https://example.com/)]\n',
)
assert.equal(
emitted(marked('a', link), { marks: [link], type: 'hardBreak' }, marked('b', link)),
@@ -671,23 +673,23 @@ test('carries a mark directive attribute no spelling holds', () => {
const carried = (mark: AdfMark): string => markdown(adfToMarkdown(document(paragraph({ marks: [mark], text: 'x', type: 'text' }))))
assert.equal(
carried({ attrs: { width: 2 }, type: 'border' }),
':adf{json="{\\"marks\\":[{\\"attrs\\":{\\"width\\":2},\\"type\\":\\"border\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
'!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"width\\":2},\\"type\\":\\"border\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
)
assert.equal(
carried({ attrs: { size: '2' }, type: 'border' }),
':adf{json="{\\"marks\\":[{\\"attrs\\":{\\"size\\":\\"2\\"},\\"type\\":\\"border\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
'!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"size\\":\\"2\\"},\\"type\\":\\"border\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n',
)
})
test('carries whitespace CommonMark strips in the reserved text directive', () => {
const emitted = (...content: AdfNode[]): string => markdown(adfToMarkdown(document(paragraph(...content))))
assert.equal(emitted({ text: ' lead', type: 'text' }), ':text{text=" "}lead\n')
assert.equal(emitted({ text: 'trail ', type: 'text' }), 'trail:text{text=" "}\n')
assert.equal(emitted({ text: 'a\nb', type: 'text' }), 'a:text{text="\\n"}b\n')
assert.equal(emitted({ text: '\t', type: 'text' }), ':text{text="\\t"}\n')
assert.equal(emitted({ text: 'a ', type: 'text' }, { type: 'hardBreak' }, { text: ' b', type: 'text' }), 'a:text{text=" "}\\\n:text{text=" "}b\n')
assert.equal(emitted({ marks: [{ type: 'em' }], text: ' a ', type: 'text' }), '_:text{text=" "}a:text{text=" "}_\n')
assert.equal(markdown(adfToMarkdown(document({ attrs: { level: 1 }, content: [{ text: 'x ', type: 'text' }], type: 'heading' }))), '# x:text{text=" "}\n')
assert.equal(emitted({ text: ' lead', type: 'text' }), '!adf:text{text=" "}lead\n')
assert.equal(emitted({ text: 'trail ', type: 'text' }), 'trail!adf:text{text=" "}\n')
assert.equal(emitted({ text: 'a\nb', type: 'text' }), 'a!adf:text{text="\\n"}b\n')
assert.equal(emitted({ text: '\t', type: 'text' }), '!adf:text{text="\\t"}\n')
assert.equal(emitted({ text: 'a ', type: 'text' }, { type: 'hardBreak' }, { text: ' b', type: 'text' }), 'a!adf:text{text=" "}\\\n!adf:text{text=" "}b\n')
assert.equal(emitted({ marks: [{ type: 'em' }], text: ' a ', type: 'text' }), '_!adf:text{text=" "}a!adf:text{text=" "}_\n')
assert.equal(markdown(adfToMarkdown(document({ attrs: { level: 1 }, content: [{ text: 'x ', type: 'text' }], type: 'heading' }))), '# x!adf:text{text=" "}\n')
// CommonMark strips spaces and tabs alone, so the whitespace beside them is plain text.
assert.equal(emitted({ text: '\va\f', type: 'text' }), '\va\f\n')
})
+4 -4
View File
@@ -5,8 +5,8 @@ import { carriedInline } from '../opaque-carry.ts'
import { claimsLine, holdsNullCharacter, isAutolink } from '../commonmark-grammar.ts'
import { escapeUnbalanced, spellDestination, spellLinkTarget } from '../link-syntax.ts'
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { holdsDirectivePrefix, slotLineEndingFault, spellInlineDirectiveOpener, spellLeafDirective } from '../directive-syntax.ts'
import { holdsEntityReference } from '../entity-references.ts'
import { holdsInlineDirectiveOpener, slotLineEndingFault, spellLeafDirective } from '../directive-syntax.ts'
import { inlineDirective } from '../../adf/inline-directives.ts'
import { largestNesting } from '../../nesting.ts'
import { longestBacktickRun } from '../backtick-runs.ts'
@@ -214,7 +214,7 @@ function emitInlineDirective(node: AdfNode, directive: InlineDirective, index: n
if (spans !== undefined) return faulted(spans, path)
if (holdsNullCharacter(slot)) return failure('unspellable-character', `a ${node.type} content slot holds a null character CommonMark replaces`, path)
const content: InlineSegment[] = slot === '' ? [] : [{ escaping: 'bracketed', text: slot }]
return success({ segments: [syntax(`:${node.type}[`), ...content, syntax(`]${attributes}`)] })
return success({ segments: [syntax(spellInlineDirectiveOpener(node.type)), ...content, syntax(`]${attributes}`)] })
}
function emitText(node: AdfNode, context: InlineContext, index: number, path: ConvertErrorPath): Result<Emission> {
@@ -241,7 +241,7 @@ function emitMarkedRun(nodes: readonly AdfNode[], mark: AdfMark, depth: number,
const inner = emitRun(nodes, depth + 1, index, { ...context, bracketed: true, spansLines: false })
if (!inner.ok) return inner
if (inner.value.carry !== undefined) return inner
return success({ segments: [syntax(`:${mark.type}[`), ...inner.value.segments, syntax(`]${attributes}`)] })
return success({ segments: [syntax(spellInlineDirectiveOpener(mark.type)), ...inner.value.segments, syntax(`]${attributes}`)] })
}
function emitEmphasis(nodes: readonly AdfNode[], spelling: string, depth: number, range: NodeRange, context: InlineContext): Result<Emission> {
@@ -284,7 +284,7 @@ function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range
if (typeof href !== 'string') return success({ carry: range })
const node = nodes[0]
const bare = nodes.length === 1 && node !== undefined && node.type === 'text' && node.text === href && nodeMarks(node).length === depth + 1
const autolinkHolds = !context.bracketed || (!href.includes('`') && !holdsInlineDirectiveOpener(href) && escapeUnbalanced(href, '[', ']') === href)
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)
if (!target.ok) return target
+3 -3
View File
@@ -1,8 +1,8 @@
import { backtickRun, closingBacktickRun } from '../backtick-runs.ts'
import { delimiterFlags, isWordCharacter, matchEmphasis, runLength } from '../emphasis-matching.ts'
import { backslashEscape, escapesLineClaim, inlineHtmlConstruct, opensBracketedAutolink, opensEmailAutolink, type LinePosition } from '../commonmark-grammar.ts'
import { claimsDirectivePrefix } from '../directive-syntax.ts'
import { isBareDelimiterRow } from '../pipe-table-syntax.ts'
import { opensInlineDirective } from '../directive-syntax.ts'
import { opensLinkDefinition } from '../link-reference-definitions.ts'
import { readEntityReference } from '../entity-references.ts'
@@ -98,7 +98,7 @@ function escapedIndexes(scan: string, escapings: readonly InlineEscaping[], cont
mergesWithSyntax(scan, escapings, index) ||
opensConstruct(scan, linkClose, index, escaping === 'bracketed', container, escaped))) ||
(escaping === 'bracketed-link-target' &&
((scan.charAt(index) === '`' && opensCodeSpan(scan, index, escaped)) || (scan.charAt(index) === ':' && opensInlineDirective(scan, index))))
((scan.charAt(index) === '`' && opensCodeSpan(scan, index, escaped)) || (scan.charAt(index) === '!' && claimsDirectivePrefix(scan, index))))
) {
escaped.add(index)
}
@@ -270,7 +270,7 @@ function claimsCharacter(
if (character === '\\') return backslashEscape(scan, index) !== undefined
if (character === '&') return readEntityReference(scan, index) !== undefined
if (character === '<') return opensBracketedAutolink(scan, index) || opensEmailAutolink(scan, index) || inlineHtmlConstruct(scan, index) !== undefined
if (character === ':') return opensInlineDirective(scan, index)
if (character === '!') return claimsDirectivePrefix(scan, index)
if (character === '[') return index < linkClose
if (character === '`') return opensCodeSpan(scan, index, escaped)
if (character === '*' || character === '_' || character === '~') return claimsEmphasis(scan, index, escaped)
+5 -4
View File
@@ -6,23 +6,24 @@ import { isAdfNode } from '../adf/document.ts'
import { isJsonValue, overNested } from '../json-value.ts'
import { fencedCodeBlock } from './backtick-runs.ts'
import { largestNesting } from '../nesting.ts'
import { malformedDirective, readSoleStringAttribute, spellAttributes, spellStringAttribute, unsupportedNodeShape } from './directive-syntax.ts'
import { malformedDirective, readSoleStringAttribute, spellAttributes, spellLeafDirective, spellStringAttribute, unsupportedNodeShape } from './directive-syntax.ts'
import { serializeCanonicalJson } from '../canonical-json.ts'
export const carryName = 'adf'
export const carryFence = 'adf'
export const carryName = 'carry'
const jsonAttribute = 'json'
export function carriedBlock(node: AdfNode, path: ConvertErrorPath, depth: number): Result<string> {
const json = carriedJson(node, 'two-space', path, largestNesting - depth)
if (!json.ok) return json
return success(fencedCodeBlock(carryName, json.value))
return success(fencedCodeBlock(carryFence, json.value))
}
export function carriedInline(node: AdfNode, path: ConvertErrorPath): Result<string> {
const json = carriedJson(node, 'compact', path, largestNesting)
if (!json.ok) return json
return success(`:${carryName}${spellAttributes([[jsonAttribute, spellStringAttribute(json.value)]])}`)
return success(spellLeafDirective(carryName, spellAttributes([[jsonAttribute, spellStringAttribute(json.value)]])))
}
export function readCarriedBlock(body: string, depth: number): Read<AdfNode> {
+2 -1
View File
@@ -2,6 +2,7 @@ import type { AdfMark } from '../../adf/document.ts'
import type { ConvertFault } from '../../result.ts'
import type { DirectiveAttributes } from '../directive-syntax.ts'
import type { MarkSpelling } from '../mark-spellings.ts'
import { directivePrefix } from '../directive-syntax.ts'
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { markSpelling } from '../mark-spellings.ts'
import { readVocabulary } from './directive-attributes.ts'
@@ -19,7 +20,7 @@ export function readDirectiveMark(name: string, attributes: DirectiveAttributes,
export function inlineMarkSpellingFault(name: string): ConvertFault | undefined {
const spelling = markSpelling(name)
if (spelling === undefined) return undefined
return { code: 'unsupported-node-shape', message: `${name} is spelled ${markdownForm(spelling) ?? `:${name}[…]`}, never as a block directive` }
return { code: 'unsupported-node-shape', message: `${name} is spelled ${markdownForm(spelling) ?? `${directivePrefix}${name}[…]`}, never as a block directive` }
}
function markdownForm(spelling: MarkSpelling): string | undefined {
+9 -7
View File
@@ -3,11 +3,11 @@ import type { BlockDirective } from '../../adf/block-directives.ts'
import type { ConvertFault } from '../../result.ts'
import type { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts'
import type { Elsewhere } from './directive-attributes.ts'
import { attributeNestingMessage, nodeMarks } from '../../adf/document.ts'
import { attributeValue, directiveLineEscape, inlineDirectiveEscape, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
import { attributeNestingMessage, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
import { attributeValue, directiveLineEscape, directivePrefix, inlineDirectiveEscape, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
import { blockArgument } from '../block-directive-arguments.ts'
import { blockDirective } from '../../adf/block-directives.ts'
import { carryName } from '../opaque-carry.ts'
import { carryFence, carryName } from '../opaque-carry.ts'
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { inlineDirective } from '../../adf/inline-directives.ts'
import { inlineMarkSpellingFault } from './directive-marks.ts'
@@ -26,7 +26,7 @@ export function readBlockDirectiveNode(
path: ConvertErrorPath,
): Result<BlockDirectiveNode> {
if (name === carryName) {
return failure('malformed-directive', `the name ${carryName} is reserved for the opaque carry, whose block form is the fence`, path)
return failure('malformed-directive', `the name ${carryName} is reserved for the opaque carry, whose block form is the ${carryFence} fence`, path)
}
const directive = blockDirective(name)
if (directive === undefined) return faulted(inlineSpellingFault(name) ?? unknownDirectiveFault(name, directiveLineEscape), path)
@@ -61,7 +61,9 @@ export function readInlineDirectiveNode(
if (!attrs.ok) return attrs
if (slot !== undefined && content !== undefined) {
const text = slotText(content)
if (text === undefined) return failure('unsupported-node-shape', `the ${name} content slot holds one unmarked text node: this one holds something else`, path)
if (text === undefined) {
return failure('unsupported-node-shape', `the ${name} content slot holds one text node carrying neither marks, attributes nor content: this one holds something else`, path)
}
const spans = slotLineEndingFault(name, text)
if (spans !== undefined) return faulted(spans, path)
attrs.value[slot] = text
@@ -74,7 +76,7 @@ function inlineSpellingFault(name: string): ConvertFault | undefined {
const mark = inlineMarkSpellingFault(name)
if (mark !== undefined) return mark
if (inlineDirective(name) === undefined && name !== textDirectiveName) return undefined
return { code: 'unsupported-node-shape', message: `${name} takes the inline form, :${name}{…}, never the block form` }
return { code: 'unsupported-node-shape', message: `${name} takes the inline form, ${directivePrefix}${name}{…}, never the block form` }
}
function blockSpellingFault(name: string): ConvertFault | undefined {
@@ -89,7 +91,7 @@ function blockSpellingFault(name: string): ConvertFault | undefined {
function slotText(content: readonly AdfNode[]): string | undefined {
if (content.length === 0) return ''
const only = content.length === 1 ? content[0] : undefined
if (only?.type !== 'text' || nodeMarks(only).length > 0 || typeof only.text !== 'string') return undefined
if (only?.type !== 'text' || nodeMarks(only).length > 0 || Object.keys(nodeAttrs(only)).length > 0 || nodeContent(only).length > 0 || typeof only.text !== 'string') return undefined
return only.text
}
+7 -7
View File
@@ -64,13 +64,16 @@ function parseInline(source: string, definitions: LinkDefinitions, path: Convert
index = angle.value
break
}
case ':': {
case '!': {
const directive = readDirective(scan, index)
if (directive === undefined) {
index = openBracket(scan, index)
break
}
if (!directive.ok) return directive
index = directive.value
break
}
case '!':
case '[':
index = openBracket(scan, index)
break
@@ -151,12 +154,9 @@ function openBracket(scan: Scan, index: number): number {
return index + width
}
function readDirective(scan: Scan, index: number): Result<number> {
function readDirective(scan: Scan, index: number): Result<number> | undefined {
const directive = readInlineDirective(scan.source, index)
if (directive === undefined) {
scan.pending += ':'
return success(index + 1)
}
if (directive === undefined) return undefined
if (directive.fault !== undefined) return faulted(directive.fault, scan.path)
const piece = directivePiece(scan, directive.value)
if (!piece.ok) return piece
+98 -86
View File
@@ -230,7 +230,7 @@ test('refuses the list separator that parts anything else', () => {
assert.equal(content(markdownToAdf('- a\n\n::listBreak x\n\n- b\n')), bare)
assert.equal(content(markdownToAdf('- a\n\n::listBreak {id=x}\n\n- b\n')), bare)
assert.equal(content(markdownToAdf(':::listBreak\n- a\n:::\n')), bare)
assert.equal(content(markdownToAdf(':listBreak{}\n')), 'unsupported-node-shape: listBreak takes the block form, ::listBreak, never the inline form')
assert.equal(content(markdownToAdf('!adf:listBreak{}\n')), 'unsupported-node-shape: listBreak takes the block form, ::listBreak, never the inline form')
assert.deepEqual(path(markdownToAdf('Part.\n\n::listBreak\n')), ['content', 1])
})
@@ -270,7 +270,7 @@ test('reads the three directive forms into the nodes the tables name', () => {
assert.deepEqual(content(markdownToAdf(':::heading {level=2 localId=a-1}\nPart.\n:::\n')), [
{ attrs: { level: 2, localId: 'a-1' }, content: [text('Part.')], type: 'heading' },
])
assert.deepEqual(content(markdownToAdf('Part:hardBreak{}.\n')), [{ content: [text('Part'), hardBreak(), text('.')], type: 'paragraph' }])
assert.deepEqual(content(markdownToAdf('Part!adf:hardBreak{}.\n')), [{ content: [text('Part'), hardBreak(), text('.')], type: 'paragraph' }])
})
test('names the directive form a node CommonMark spells refuses', () => {
@@ -293,8 +293,8 @@ test('gives back the refusal the CommonMark spelling itself raises', () => {
test('names the directive name no node reads back to', () => {
assert.equal(code(markdownToAdf(':::widget info\nx\n:::\n')), 'unknown-directive-name')
assert.equal(content(markdownToAdf('::widget\n')), 'unknown-directive-name: the directive name widget reads back to no node; \\::: keeps the line literal text')
assert.equal(content(markdownToAdf(':widget[x]\n')), 'unknown-directive-name: the directive name widget reads back to no node; \\: keeps the colon literal')
assert.equal(content(markdownToAdf('ratio a:b[c]{d}\n')), 'malformed-directive: an attribute reads key=value, the value bare or double-quoted: this one does not; \\: keeps the colon literal')
assert.equal(content(markdownToAdf('!adf:widget[x]\n')), 'unknown-directive-name: the directive name widget reads back to no node; \\!adf: keeps the prefix literal')
assert.equal(content(markdownToAdf('ratio a!adf:b[c]{d}\n')), 'malformed-directive: an attribute reads key=value, the value bare or double-quoted: this one does not; \\!adf: keeps the prefix literal')
assert.deepEqual(path(markdownToAdf('Part.\n\n::widget\n')), ['content', 1])
assert.equal(content(markdownToAdf('Part.\n:::x\n')), 'malformed-directive: a container fenced with 3 colons is unclosed')
assert.deepEqual(path(markdownToAdf('Part.\n:::x\n')), ['content', 1])
@@ -302,23 +302,23 @@ test('names the directive name no node reads back to', () => {
test('names the position a directive name the other one spells belongs to', () => {
assert.equal(content(markdownToAdf(':::em\na\n:::\n')), 'unsupported-node-shape: em is spelled _x_, never as a block directive')
assert.equal(content(markdownToAdf('::underline\n')), 'unsupported-node-shape: underline is spelled :underline[…], never as a block directive')
assert.equal(content(markdownToAdf('::text {text=" "}\n')), 'unsupported-node-shape: text takes the inline form, :text{…}, never the block form')
assert.equal(content(markdownToAdf('::date {timestamp=1}\n')), 'unsupported-node-shape: date takes the inline form, :date{…}, never the block form')
assert.equal(content(markdownToAdf(':paragraph[a]\n')), 'unsupported-node-shape: paragraph takes the block form, :::paragraph, never the inline form')
assert.equal(content(markdownToAdf(':rule[a]\n')), 'unsupported-node-shape: rule takes the block form, ::rule, never the inline form')
assert.equal(content(markdownToAdf('::underline\n')), 'unsupported-node-shape: underline is spelled !adf:underline[…], never as a block directive')
assert.equal(content(markdownToAdf('::text {text=" "}\n')), 'unsupported-node-shape: text takes the inline form, !adf:text{…}, never the block form')
assert.equal(content(markdownToAdf('::date {timestamp=1}\n')), 'unsupported-node-shape: date takes the inline form, !adf:date{…}, never the block form')
assert.equal(content(markdownToAdf('!adf:paragraph[a]\n')), 'unsupported-node-shape: paragraph takes the block form, :::paragraph, never the inline form')
assert.equal(content(markdownToAdf('!adf:rule[a]\n')), 'unsupported-node-shape: rule takes the block form, ::rule, never the inline form')
assert.equal(code(markdownToAdf(':::widget\na\n:::\n')), 'unknown-directive-name')
assert.equal(code(markdownToAdf(':widget[a]\n')), 'unknown-directive-name')
assert.equal(code(markdownToAdf('!adf:widget[a]\n')), 'unknown-directive-name')
})
test('names the reserved carry name a block directive spells', () => {
const reserved = 'malformed-directive: the name adf is reserved for the opaque carry, whose block form is the fence'
assert.equal(content(markdownToAdf('::adf\n')), reserved)
assert.equal(content(markdownToAdf(':::adf\nx\n:::\n')), reserved)
const reserved = 'malformed-directive: the name carry is reserved for the opaque carry, whose block form is the adf fence'
assert.equal(content(markdownToAdf('::carry\n')), reserved)
assert.equal(content(markdownToAdf(':::carry\nx\n:::\n')), reserved)
assert.deepEqual(content(markdownToAdf('```adfx\nx\n```\n')), [{ attrs: { language: 'adfx' }, content: [text('x')], type: 'codeBlock' }])
})
const carried = ':adf{json="{\\"type\\":\\"placeholder\\"}"}'
const carried = '!adf:carry{json="{\\"type\\":\\"placeholder\\"}"}'
test('reads the adf fence back to the node its JSON holds', () => {
assert.deepEqual(content(markdownToAdf('```adf\n{\n "attrs": {\n "url": "https://example.com/x"\n },\n "type": "blockCard"\n}\n```\n')), [
@@ -336,36 +336,36 @@ test('names the invalid JSON no opaque carry holds', () => {
const invalid = 'malformed-directive: the opaque carry holds invalid JSON'
assert.equal(content(markdownToAdf('```adf\n{"type":\n```\n')), invalid)
assert.equal(content(markdownToAdf('```adf\n```\n')), invalid)
assert.equal(content(markdownToAdf(':adf{json="{"}\n')), invalid)
assert.equal(content(markdownToAdf(':adf{json=abc}\n')), invalid)
assert.equal(content(markdownToAdf('!adf:carry{json="{"}\n')), invalid)
assert.equal(content(markdownToAdf('!adf:carry{json=abc}\n')), invalid)
})
test('names the canonical spelling a carried JSON reads alone', () => {
const canonically = "unsupported-node-shape: the opaque carry spells its node's JSON canonically: "
assert.equal(content(markdownToAdf('```adf\n{"type":"blockCard"}\n```\n')), `${canonically}two-space indent, keys sorted`)
assert.equal(content(markdownToAdf(':adf{json="{\\"type\\": \\"blockCard\\"}"}\n')), `${canonically}compact, keys sorted`)
assert.equal(content(markdownToAdf(':adf{json="{\\"type\\":\\"blockCard\\",\\"attrs\\":{}}"}\n')), `${canonically}compact, keys sorted`)
assert.equal(content(markdownToAdf('!adf:carry{json="{\\"type\\": \\"blockCard\\"}"}\n')), `${canonically}compact, keys sorted`)
assert.equal(content(markdownToAdf('!adf:carry{json="{\\"type\\":\\"blockCard\\",\\"attrs\\":{}}"}\n')), `${canonically}compact, keys sorted`)
})
test('names the node JSON an opaque carry restores alone', () => {
const node = "unsupported-node-shape: the opaque carry holds one ADF node's JSON: this JSON is no ADF node"
assert.equal(content(markdownToAdf('```adf\n[]\n```\n')), node)
assert.equal(content(markdownToAdf(':adf{json=null}\n')), node)
assert.equal(content(markdownToAdf(':adf{json="{\\"kind\\":\\"x\\"}"}\n')), node)
assert.equal(content(markdownToAdf('!adf:carry{json=null}\n')), node)
assert.equal(content(markdownToAdf('!adf:carry{json="{\\"kind\\":\\"x\\"}"}\n')), node)
})
test('names the shape the inline carry reads alone', () => {
assert.equal(content(markdownToAdf(':adf[x]{json="{}"}\n')), 'unsupported-node-shape: adf takes no content: this one holds some')
assert.equal(content(markdownToAdf(':adf{}\n')), 'unsupported-node-shape: adf holds one json attribute alone: this one does not')
assert.equal(content(markdownToAdf(':adf{json="{}" localId=x}\n')), 'unsupported-node-shape: adf holds one json attribute alone: this one does not')
assert.equal(content(markdownToAdf(':adf{json="null"}\n')), 'unsupported-node-shape: adf spells its json attribute as json=null')
assert.equal(content(markdownToAdf('!adf:carry[x]{json="{}"}\n')), 'unsupported-node-shape: carry takes no content: this one holds some')
assert.equal(content(markdownToAdf('!adf:carry{}\n')), 'unsupported-node-shape: carry holds one json attribute alone: this one does not')
assert.equal(content(markdownToAdf('!adf:carry{json="{}" localId=x}\n')), 'unsupported-node-shape: carry holds one json attribute alone: this one does not')
assert.equal(content(markdownToAdf('!adf:carry{json="null"}\n')), 'unsupported-node-shape: carry spells its json attribute as json=null')
})
test('holds a carried JSON value to the nesting its position leaves', () => {
const nested = (levels: number): string => `${'['.repeat(levels)}${']'.repeat(levels)}`
const fence = (prefix: string, levels: number): string => `${prefix}\`\`\`adf\n${prefix}${nested(levels)}\n${prefix}\`\`\`\n`
const deeper = (levels: number): string => `unsupported-nesting-depth: a carried node's JSON nests deeper than the ${levels} levels its position leaves`
assert.equal(content(markdownToAdf(`:adf{json="${nested(largestNesting + 2)}"}\n`)), deeper(largestNesting))
assert.equal(content(markdownToAdf(`!adf:carry{json="${nested(largestNesting + 2)}"}\n`)), deeper(largestNesting))
assert.equal(
content(markdownToAdf(fence('', largestNesting + 1))),
"unsupported-node-shape: the opaque carry spells its node's JSON canonically: two-space indent, keys sorted",
@@ -375,7 +375,7 @@ test('holds a carried JSON value to the nesting its position leaves', () => {
test('names the number no JSON spelling carries in an opaque carry', () => {
const named = 'unsupported-node-shape: the opaque carry holds a number JSON cannot spell'
assert.equal(content(markdownToAdf(':adf{json="{\\"attrs\\":{\\"width\\":1e999},\\"type\\":\\"blockCard\\"}"}\n')), named)
assert.equal(content(markdownToAdf('!adf:carry{json="{\\"attrs\\":{\\"width\\":1e999},\\"type\\":\\"blockCard\\"}"}\n')), named)
assert.equal(content(markdownToAdf('```adf\n1e999\n```\n')), named)
})
@@ -385,8 +385,8 @@ test('names the mark spelling no opaque carry sits inside', () => {
assert.equal(content(markdownToAdf(`**${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(`:underline[${carried}]\n`)), named)
assert.equal(content(markdownToAdf(`:textColor[a ${carried}]{color="#ae2e24"}\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(`![_a ${carried}_](https://example.com/i)\n`)), named)
})
@@ -432,7 +432,7 @@ test('names the attribute a node holds no reading for', () => {
assert.equal(content(markdownToAdf('::media {width=true}\n')), 'unsupported-node-shape: the width attribute of media is no number')
assert.equal(content(markdownToAdf(':::tableCell {colwidth="[340,"}\n:::\n')), 'unsupported-node-shape: the colwidth attribute of tableCell is no json')
assert.equal(content(markdownToAdf(':::panel info {panelType=note}\nx\n:::\n')), 'unsupported-node-shape: panel spells its panelType attribute as the directive argument, never in {attrs}')
assert.equal(content(markdownToAdf('Part :mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot, never in {attrs}')
assert.equal(content(markdownToAdf('Part !adf:mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot, never in {attrs}')
})
test('names the depth an attribute value nests past, never the kind the JSON reads as', () => {
@@ -460,19 +460,28 @@ test('names the argument and the body a node takes no reading for', () => {
assert.equal(content(markdownToAdf(':::paragraph\n---\n:::\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body: this body is not one')
assert.equal(content(markdownToAdf(':::codeBlock {wrap=true}\nx\n:::\n')), 'unsupported-node-shape: codeBlock takes one code block as its body: this body is not one')
assert.equal(content(markdownToAdf(':::paragraph\n![a](/u)\n:::\n')), 'unmappable-image: no ADF node carries an image inside a paragraph')
assert.equal(content(markdownToAdf('Part :date[now]{timestamp=1}.\n')), 'unsupported-node-shape: date takes no content: this one holds some')
assert.equal(content(markdownToAdf('Part !adf:date[now]{timestamp=1}.\n')), 'unsupported-node-shape: date takes no content: this one holds some')
})
test('leaves the colon that opens no directive the text it is', () => {
test('leaves the text that opens no directive the text it is', () => {
assert.deepEqual(content(markdownToAdf('At 10:30 :smile: today.\n')), [paragraph('At 10:30 :smile: today.')])
assert.deepEqual(content(markdownToAdf('\\:mention[@A]\n')), [paragraph(':mention[@A]')])
assert.deepEqual(content(markdownToAdf('`:mention[@A]`\n')), [{ content: [codeSpan(':mention[@A]')], type: 'paragraph' }])
assert.deepEqual(content(markdownToAdf(':mention[@A] and !adfx and a!\n')), [paragraph(':mention[@A] and !adfx and a!')])
assert.deepEqual(content(markdownToAdf('\\!adf:mention[@A]\n')), [paragraph('!adf:mention[@A]')])
assert.deepEqual(content(markdownToAdf('`!adf:mention[@A]`\n')), [{ content: [codeSpan('!adf:mention[@A]')], type: 'paragraph' }])
})
test('names the unescaped prefix that completes no directive', () => {
const named = 'malformed-directive: an unescaped !adf: completes no directive; \\!adf: keeps the prefix literal'
assert.equal(content(markdownToAdf('Part !adf: here.\n')), named)
assert.equal(content(markdownToAdf('Part !adf:Mention[@A] here.\n')), named)
assert.equal(content(markdownToAdf('Part !adf:mention @A here.\n')), named)
assert.equal(content(markdownToAdf('!adf:underline[a !adf: b]\n')), named)
})
test('names the inline directive left unclosed at the end of its line', () => {
assert.equal(content(markdownToAdf('Part :mention[@A\n')), 'malformed-directive: an inline directive [content] is unclosed; \\: keeps the colon literal')
assert.equal(code(markdownToAdf('Part :mention[@A]{id=\n')), 'malformed-directive')
assert.deepEqual(path(markdownToAdf('> Part :mention[@A\n')), ['content', 0, 'content', 0])
assert.equal(content(markdownToAdf('Part !adf:mention[@A\n')), 'malformed-directive: an inline directive [content] is unclosed; \\!adf: keeps the prefix literal')
assert.equal(code(markdownToAdf('Part !adf:mention[@A]{id=\n')), 'malformed-directive')
assert.deepEqual(path(markdownToAdf('> Part !adf:mention[@A\n')), ['content', 0, 'content', 0])
})
test('claims a block-level pipe with no table to parse it', () => {
@@ -630,7 +639,7 @@ test('names the block the claim inside a container opens', () => {
test('refuses input nested deeper than the parser carries', () => {
assert.equal(code(markdownToAdf('> '.repeat(501))), 'unsupported-nesting-depth')
assert.ok(markdownToAdf('> '.repeat(500)).ok)
const marks = (levels: number): string => `${':underline['.repeat(levels)}a${']'.repeat(levels)}\n`
const marks = (levels: number): string => `${'!adf:underline['.repeat(levels)}a${']'.repeat(levels)}\n`
assert.equal(code(markdownToAdf(marks(largestNesting + 1))), 'unsupported-nesting-depth')
assert.deepEqual(content(markdownToAdf(marks(largestNesting))), [{ content: [marked('a', underline)], type: 'paragraph' }])
})
@@ -820,8 +829,8 @@ test('flattens the description of a lone image to the plain text alt holds', ()
assert.deepEqual(content(markdownToAdf('![a\nb](/u)\n')), [image('/u', 'a b')])
assert.deepEqual(content(markdownToAdf('![a \nb](/u)\n')), [image('/u', 'a b')])
assert.deepEqual(content(markdownToAdf('![a `b`](/u)\n')), [image('/u', 'a b')])
assert.deepEqual(content(markdownToAdf('![a :mention[@A]{id=b1c2} b](/u)\n')), [image('/u', 'a @A b')])
assert.deepEqual(content(markdownToAdf('![:mention[@A]{id=b1c2}](/u)\n')), [image('/u', '@A')])
assert.deepEqual(content(markdownToAdf('![a !adf:mention[@A]{id=b1c2} b](/u)\n')), [image('/u', 'a @A b')])
assert.deepEqual(content(markdownToAdf('![!adf:mention[@A]{id=b1c2}](/u)\n')), [image('/u', '@A')])
})
test('leaves the brackets of an empty link text the text they are', () => {
@@ -855,78 +864,81 @@ test('carries the mark a spelling nested inside its own kind names once', () =>
test('reads the content slot as the text attribute the node spells there', () => {
const status = (attrs: AdfAttributes): AdfNode[] => [{ content: [{ attrs, type: 'status' }], type: 'paragraph' }]
assert.deepEqual(content(markdownToAdf(':status[In review]{color=yellow}\n')), status({ color: 'yellow', text: 'In review' }))
assert.deepEqual(content(markdownToAdf(':status{color=neutral}\n')), status({ color: 'neutral' }))
assert.deepEqual(content(markdownToAdf(':status[]{color=neutral}\n')), status({ color: 'neutral', text: '' }))
assert.deepEqual(content(markdownToAdf(':status[ In review ]{color=yellow}\n')), status({ color: 'yellow', text: ' In review ' }))
assert.deepEqual(content(markdownToAdf(':status[In:text{text=" "}review]{color=yellow}\n')), status({ color: 'yellow', text: 'In review' }))
assert.deepEqual(content(markdownToAdf(':status[a\\]b]{color=yellow}\n')), status({ color: 'yellow', text: 'a]b' }))
assert.deepEqual(content(markdownToAdf('**:mention[@A]{id=b1c2}**\n')), [
assert.deepEqual(content(markdownToAdf('!adf:status[In review]{color=yellow}\n')), status({ color: 'yellow', text: 'In review' }))
assert.deepEqual(content(markdownToAdf('!adf:status{color=neutral}\n')), status({ color: 'neutral' }))
assert.deepEqual(content(markdownToAdf('!adf:status[]{color=neutral}\n')), status({ color: 'neutral', text: '' }))
assert.deepEqual(content(markdownToAdf('!adf:status[ In review ]{color=yellow}\n')), status({ color: 'yellow', text: ' In review ' }))
assert.deepEqual(content(markdownToAdf('!adf:status[In!adf:text{text=" "}review]{color=yellow}\n')), status({ color: 'yellow', text: 'In review' }))
assert.deepEqual(content(markdownToAdf('!adf:status[a\\]b]{color=yellow}\n')), status({ color: 'yellow', text: 'a]b' }))
assert.deepEqual(content(markdownToAdf('**!adf:mention[@A]{id=b1c2}**\n')), [
{ content: [{ attrs: { id: 'b1c2', text: '@A' }, marks: [strong], type: 'mention' }], type: 'paragraph' },
])
})
test('names the content slot no one unmarked text node reads back from', () => {
assert.equal(content(markdownToAdf(':status[**A**]{color=yellow}\n')), 'unsupported-node-shape: the status content slot holds one unmarked text node: this one holds something else')
assert.equal(code(markdownToAdf(':status[a`b`]{color=yellow}\n')), 'unsupported-node-shape')
assert.equal(code(markdownToAdf(':status[:date{timestamp=1}]{color=yellow}\n')), 'unsupported-node-shape')
assert.equal(content(markdownToAdf(':status[![a](/u)]{color=yellow}\n')), 'unmappable-image: an image fits only as a paragraph of its own: this one sits inside other content')
assert.equal(code(markdownToAdf(':status[<div>]{color=yellow}\n')), 'unmappable-html')
assert.equal(code(markdownToAdf(':date[<div>]{timestamp=1}\n')), 'unmappable-html')
assert.equal(code(markdownToAdf(':widget[<div>]\n')), 'unmappable-html')
test('names the content slot no lone plain text node reads back from', () => {
const named = 'unsupported-node-shape: the status content slot holds one text node carrying neither marks, attributes nor content: this one holds something else'
assert.equal(content(markdownToAdf('!adf:status[**A**]{color=yellow}\n')), named)
assert.equal(content(markdownToAdf('!adf:status[!adf:carry{json="{\\"attrs\\":{\\"localId\\":\\"a\\"},\\"text\\":\\"A\\",\\"type\\":\\"text\\"}"}]{color=yellow}\n')), named)
assert.equal(content(markdownToAdf('!adf:status[!adf:carry{json="{\\"content\\":[{\\"text\\":\\"B\\",\\"type\\":\\"text\\"}],\\"text\\":\\"A\\",\\"type\\":\\"text\\"}"}]{color=yellow}\n')), named)
assert.equal(code(markdownToAdf('!adf:status[a`b`]{color=yellow}\n')), 'unsupported-node-shape')
assert.equal(code(markdownToAdf('!adf:status[!adf:date{timestamp=1}]{color=yellow}\n')), 'unsupported-node-shape')
assert.equal(content(markdownToAdf('!adf:status[![a](/u)]{color=yellow}\n')), 'unmappable-image: an image fits only as a paragraph of its own: this one sits inside other content')
assert.equal(code(markdownToAdf('!adf:status[<div>]{color=yellow}\n')), 'unmappable-html')
assert.equal(code(markdownToAdf('!adf:date[<div>]{timestamp=1}\n')), 'unmappable-html')
assert.equal(code(markdownToAdf('!adf:widget[<div>]\n')), 'unmappable-html')
const spans = 'unspellable-whitespace: the status content slot holds a newline no inline directive spans'
assert.equal(content(markdownToAdf(':status[:text{text="\\n"}]{color=yellow}\n')), spans)
assert.equal(content(markdownToAdf(':status[a&#10;b]{color=yellow}\n')), spans)
assert.equal(content(markdownToAdf(':status[a&#13;b]{color=yellow}\n')), spans)
assert.equal(content(markdownToAdf('Part :mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot, never in {attrs}')
assert.equal(content(markdownToAdf('!adf:status[!adf:text{text="\\n"}]{color=yellow}\n')), spans)
assert.equal(content(markdownToAdf('!adf:status[a&#10;b]{color=yellow}\n')), spans)
assert.equal(content(markdownToAdf('!adf:status[a&#13;b]{color=yellow}\n')), spans)
assert.equal(content(markdownToAdf('Part !adf:mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot, never in {attrs}')
})
test('reads the whitespace the reserved text directive carries', () => {
assert.deepEqual(content(markdownToAdf(':text{text=" "}a\n')), [paragraph(' a')])
assert.deepEqual(content(markdownToAdf('a:text{text="\\n"}b\n')), [paragraph('a\nb')])
assert.deepEqual(content(markdownToAdf('a:text{text="\\t"}\n')), [paragraph('a\t')])
assert.deepEqual(content(markdownToAdf('_:text{text=" "}a_\n')), [{ content: [marked(' a', em)], type: 'paragraph' }])
assert.deepEqual(content(markdownToAdf('!adf:text{text=" "}a\n')), [paragraph(' a')])
assert.deepEqual(content(markdownToAdf('a!adf:text{text="\\n"}b\n')), [paragraph('a\nb')])
assert.deepEqual(content(markdownToAdf('a!adf:text{text="\\t"}\n')), [paragraph('a\t')])
assert.deepEqual(content(markdownToAdf('_!adf:text{text=" "}a_\n')), [{ content: [marked(' a', em)], type: 'paragraph' }])
})
test('names the text directive spelling no whitespace run reads back from', () => {
const named = 'unsupported-node-shape: text spells one run of spaces and tabs, or one run of newlines: this one spells neither'
assert.equal(content(markdownToAdf(':text{text=hi}\n')), named)
assert.equal(content(markdownToAdf(':text{text=" \\n"}\n')), named)
assert.equal(content(markdownToAdf(':text{text=""}\n')), named)
assert.equal(content(markdownToAdf(':text{}\n')), 'unsupported-node-shape: text holds one text attribute alone: this one does not')
assert.equal(content(markdownToAdf(':text{localId=a text=" "}\n')), 'unsupported-node-shape: text holds one text attribute alone: this one does not')
assert.equal(content(markdownToAdf(':text[a]{text=" "}\n')), 'unsupported-node-shape: text takes no content: this one holds some')
assert.equal(content(markdownToAdf(':text{text="\\u0020"}\n')), 'unsupported-node-shape: text spells its text attribute as text=" "')
assert.equal(content(markdownToAdf('!adf:text{text=hi}\n')), named)
assert.equal(content(markdownToAdf('!adf:text{text=" \\n"}\n')), named)
assert.equal(content(markdownToAdf('!adf:text{text=""}\n')), named)
assert.equal(content(markdownToAdf('!adf:text{}\n')), 'unsupported-node-shape: text holds one text attribute alone: this one does not')
assert.equal(content(markdownToAdf('!adf:text{localId=a text=" "}\n')), 'unsupported-node-shape: text holds one text attribute alone: this one does not')
assert.equal(content(markdownToAdf('!adf:text[a]{text=" "}\n')), 'unsupported-node-shape: text takes no content: this one holds some')
assert.equal(content(markdownToAdf('!adf:text{text="\\u0020"}\n')), 'unsupported-node-shape: text spells its text attribute as text=" "')
})
test('reads the directive marks, the nesting outermost first', () => {
const wrapped = (...marks: AdfMark[]): AdfNode[] => [{ content: [marked('a', ...marks)], type: 'paragraph' }]
assert.deepEqual(content(markdownToAdf(':underline[a]\n')), wrapped(underline))
assert.deepEqual(content(markdownToAdf('_:underline[a]_\n')), wrapped(em, underline))
assert.deepEqual(content(markdownToAdf(':underline[_a_]\n')), wrapped(underline, em))
assert.deepEqual(content(markdownToAdf(':underline[:underline[a]]\n')), wrapped(underline))
assert.deepEqual(content(markdownToAdf(':textColor[a]{color="#ae2e24"}\n')), wrapped({ attrs: { color: '#ae2e24' }, type: 'textColor' }))
assert.deepEqual(content(markdownToAdf(':subsup[a]{type=sub}\n')), wrapped({ attrs: { type: 'sub' }, type: 'subsup' }))
assert.deepEqual(content(markdownToAdf(':border[a]{color="#091e42" size=2}\n')), wrapped({ attrs: { color: '#091e42', size: 2 }, type: 'border' }))
assert.deepEqual(content(markdownToAdf(':underline[a:date{timestamp=1}]\n')), [
assert.deepEqual(content(markdownToAdf('!adf:underline[a]\n')), wrapped(underline))
assert.deepEqual(content(markdownToAdf('_!adf:underline[a]_\n')), wrapped(em, underline))
assert.deepEqual(content(markdownToAdf('!adf:underline[_a_]\n')), wrapped(underline, em))
assert.deepEqual(content(markdownToAdf('!adf:underline[!adf:underline[a]]\n')), wrapped(underline))
assert.deepEqual(content(markdownToAdf('!adf:textColor[a]{color="#ae2e24"}\n')), wrapped({ attrs: { color: '#ae2e24' }, type: 'textColor' }))
assert.deepEqual(content(markdownToAdf('!adf:subsup[a]{type=sub}\n')), wrapped({ attrs: { type: 'sub' }, type: 'subsup' }))
assert.deepEqual(content(markdownToAdf('!adf:border[a]{color="#091e42" size=2}\n')), wrapped({ attrs: { color: '#091e42', size: 2 }, type: 'border' }))
assert.deepEqual(content(markdownToAdf('!adf:underline[a!adf:date{timestamp=1}]\n')), [
{ content: [marked('a', underline), { attrs: { timestamp: '1' }, marks: [underline], type: 'date' }], type: 'paragraph' },
])
assert.deepEqual(content(markdownToAdf('_:underline[a:date{timestamp=1}]_\n')), [
assert.deepEqual(content(markdownToAdf('_!adf:underline[a!adf:date{timestamp=1}]_\n')), [
{ content: [marked('a', em, underline), { attrs: { timestamp: '1' }, marks: [em, underline], type: 'date' }], type: 'paragraph' },
])
assert.equal(content(markdownToAdf(':border[a]{color="#091e42" size=x}\n')), 'unsupported-node-shape: the size attribute of border is no number')
assert.equal(content(markdownToAdf('!adf:border[a]{color="#091e42" size=x}\n')), 'unsupported-node-shape: the size attribute of border is no number')
})
test('names the mark markdown spells, never a directive', () => {
assert.equal(content(markdownToAdf(':em[a]\n')), 'unsupported-node-shape: em is spelled _x_, never as a directive')
assert.equal(content(markdownToAdf(':strong[a]\n')), 'unsupported-node-shape: strong is spelled **x**, never as a directive')
assert.equal(content(markdownToAdf(':strike[a]\n')), 'unsupported-node-shape: strike is spelled ~~x~~, never as a directive')
assert.equal(content(markdownToAdf(':code[a]\n')), 'unsupported-node-shape: code is spelled `x`, never as a directive')
assert.equal(content(markdownToAdf(':link[a]{href="/u"}\n')), 'unsupported-node-shape: link is spelled [x](url), never as a directive')
assert.equal(content(markdownToAdf('!adf:em[a]\n')), 'unsupported-node-shape: em 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: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')
})
test('names the directive mark left without the content it wraps', () => {
const named = 'unsupported-node-shape: the underline mark wraps the [content] it marks: this one wraps none'
assert.equal(content(markdownToAdf(':underline[]\n')), named)
assert.equal(content(markdownToAdf(':underline{}\n')), named)
assert.equal(content(markdownToAdf('!adf:underline[]\n')), named)
assert.equal(content(markdownToAdf('!adf:underline{}\n')), named)
})
+2 -2
View File
@@ -3,7 +3,7 @@ import type { Block, DirectiveBlock } from './blocks.ts'
import type { BlockDirectiveNode } from './directive-nodes.ts'
import type { ConvertFault } from '../../result.ts'
import type { LinkDefinitions } from './inline-content.ts'
import { carryName, readCarriedBlock } from '../opaque-carry.ts'
import { carryFence, readCarriedBlock } from '../opaque-carry.ts'
import { commonMarkSpelling } from '../emit/adf-to-markdown.ts'
import { failure, faulted, positioned, success, type ConvertErrorPath, type ParseError, type Result, type SourcePosition } from '../../result.ts'
import { languageSlot } from '../code-language.ts'
@@ -161,7 +161,7 @@ function listNode(node: AdfNode, items: readonly Block[][], definitions: LinkDef
}
function codeBlockNode(language: string, text: string, path: ConvertErrorPath, depth: number): Result<AdfNode> {
if (language === carryName) {
if (language === carryFence) {
const carried = readCarriedBlock(text, depth)
if (carried.fault !== undefined) return faulted(carried.fault, path)
return success(carried.value)
+2 -1
View File
@@ -8,6 +8,7 @@ import type { AttributeKind, AttributeVocabulary } from './adf/attribute-vocabul
import type { JsonValue } from './json-value.ts'
import { blockArgument } from './markdown/block-directive-arguments.ts'
import { blockDirectives } from './adf/block-directives.ts'
import { directivePrefix } from './markdown/directive-syntax.ts'
import { inlineDirectives } from './adf/inline-directives.ts'
import { markAttributes } from './adf/mark-attributes.ts'
import { toEditorNormal } from './adf/editor-normal.ts'
@@ -22,7 +23,7 @@ export const propertyTimeout = 600000
const depthIdentifier = fc.createDepthIdentifier()
const emptyCell: AdfNode = { content: [{ type: 'paragraph' }], type: 'tableCell' }
const flatCommonMarkShapeWeight = 4
export const markdownPieces = fc.constantFrom(...'aZ09 \t\n!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~é\xa0🎉', ':a[', ':a{', 'ab:', 'http://')
export const markdownPieces = fc.constantFrom(...'aZ09 \t\n!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~é\xa0🎉', 'ab:', 'http://', directivePrefix, `${directivePrefix}a[`, `${directivePrefix}a{`)
const nestingCommonMarkShapeWeight = 21
const spelledTypes = new Set(['text', ...Object.keys(blockDirectives), ...Object.keys(inlineDirectives), ...Object.keys(markAttributes)])
+11
View File
@@ -247,6 +247,9 @@ proves 12, 13 spells 11's gaps in 12's grammar, and 12 rewrites code 4b and 4c c
container parses.
- Split by construct, each sub-item both directions: 55 of 78 round-trip fixtures feed both
the emit and the read-back test, so an emit-only chunk cannot land green.
- The spec leads the code from 12a to 12d: `spec/flavour.md` and `AGENTS.md` §4 spell the
`!adf:` grammar whole, while code and fixtures reach it one form at a time. A reader landing
in either without `todo.md` sees a gap that is the plan, not a defect.
- [x] **12a — The spec and the decision.**
- [ ] **12b — The inline form.** Inline nodes, directive marks, `text` and the inline carry
`!adf:carry{json=…}` spelled and read as `!adf:name[content]{attrs}`, with the prefix claim
@@ -258,6 +261,14 @@ proves 12, 13 spells 11's gaps in 12's grammar, and 12 rewrites code 4b and 4c c
model, empty pairs, `listBreak` and the `carry` fence, spelled and read; the fence-length
rule and the corpus test's fence nesting check deleted; the remaining fixtures re-spelled
and `errors/` re-derived under the shifted codes, and the gate green.
12b's two temporary seams expire here: `carryFence` folds back into `carryName` once the
fence reads `carry`, and `directiveLineEscape` into `inlineDirectiveEscape` once one escape
serves both forms. `spellLeafDirective` takes the `Inline` its reader-side regex already
carries, and `header` versus `opener` settles as one word in spec and code. While
`readNestedDirective` is open, its `[content]` and `{attrs}` reads lift out as named steps,
and the two `charAt`-against-`!` fast paths ahead of `claimsDirectivePrefix` — in
`readDirectiveContent` and `line-escaping`'s `bracketed-link-target` arm — either earn a
reason or go (the systems-architect, 2026-09-16).
- [ ] **12d — The README, `MIGRATION.md` and the sweep.** The README's examples and error tables
follow, `MIGRATION.md` linked from one README line; docs and fixtures swept for any stale
`::`/`:name` spelling.