diff --git a/corpus/errors/carry-inside-mark.md b/corpus/errors/carry-inside-mark.md
index 10bec4d..7da4509 100644
--- a/corpus/errors/carry-inside-mark.md
+++ b/corpus/errors/carry-inside-mark.md
@@ -1 +1 @@
-_a :adf{json="{\"type\":\"blockCard\"}"} b_
+_a !adf:carry{json="{\"type\":\"blockCard\"}"} b_
diff --git a/corpus/errors/carry-invalid-json-inline.md b/corpus/errors/carry-invalid-json-inline.md
index db12419..5dacb6b 100644
--- a/corpus/errors/carry-invalid-json-inline.md
+++ b/corpus/errors/carry-invalid-json-inline.md
@@ -1 +1 @@
-:adf{json="{"}
+!adf:carry{json="{"}
diff --git a/corpus/errors/carry-nesting-depth.md b/corpus/errors/carry-nesting-depth.md
index ec39271..5f5183e 100644
--- a/corpus/errors/carry-nesting-depth.md
+++ b/corpus/errors/carry-nesting-depth.md
@@ -1 +1 @@
-:adf{json="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"}
+!adf:carry{json="[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]"}
diff --git a/corpus/errors/directive-adf-reserved.error b/corpus/errors/directive-carry-reserved.error
similarity index 100%
rename from corpus/errors/directive-adf-reserved.error
rename to corpus/errors/directive-carry-reserved.error
diff --git a/corpus/errors/directive-adf-reserved.md b/corpus/errors/directive-carry-reserved.md
similarity index 68%
rename from corpus/errors/directive-adf-reserved.md
rename to corpus/errors/directive-carry-reserved.md
index 867275e..2ab969b 100644
--- a/corpus/errors/directive-adf-reserved.md
+++ b/corpus/errors/directive-carry-reserved.md
@@ -1,3 +1,3 @@
-:::adf
+:::carry
{"type":"rule"}
:::
diff --git a/corpus/errors/directive-content-slot.md b/corpus/errors/directive-content-slot.md
index 75bff4f..9b3be0a 100644
--- a/corpus/errors/directive-content-slot.md
+++ b/corpus/errors/directive-content-slot.md
@@ -1 +1 @@
-Part :mention[**@Mikael**]{id=b1c2}.
+Part !adf:mention[**@Mikael**]{id=b1c2}.
diff --git a/corpus/errors/directive-inline-unclosed.md b/corpus/errors/directive-inline-unclosed.md
index e035c81..29d756b 100644
--- a/corpus/errors/directive-inline-unclosed.md
+++ b/corpus/errors/directive-inline-unclosed.md
@@ -1 +1 @@
-Part :mention[@A and more.
+Part !adf:mention[@A and more.
diff --git a/corpus/errors/directive-mark-markdown.md b/corpus/errors/directive-mark-markdown.md
index 20221a3..7894675 100644
--- a/corpus/errors/directive-mark-markdown.md
+++ b/corpus/errors/directive-mark-markdown.md
@@ -1 +1 @@
-Part :em[emphasised] here.
+Part !adf:em[emphasised] here.
diff --git a/corpus/errors/directive-prefix-alone.error b/corpus/errors/directive-prefix-alone.error
new file mode 100644
index 0000000..4f35411
--- /dev/null
+++ b/corpus/errors/directive-prefix-alone.error
@@ -0,0 +1 @@
+malformed-directive
diff --git a/corpus/errors/directive-prefix-alone.md b/corpus/errors/directive-prefix-alone.md
new file mode 100644
index 0000000..6079223
--- /dev/null
+++ b/corpus/errors/directive-prefix-alone.md
@@ -0,0 +1 @@
+Part !adf: here.
diff --git a/corpus/errors/directive-slot-attributes.error b/corpus/errors/directive-slot-attributes.error
new file mode 100644
index 0000000..a811539
--- /dev/null
+++ b/corpus/errors/directive-slot-attributes.error
@@ -0,0 +1 @@
+unsupported-node-shape
diff --git a/corpus/errors/directive-slot-attributes.md b/corpus/errors/directive-slot-attributes.md
new file mode 100644
index 0000000..3753dc7
--- /dev/null
+++ b/corpus/errors/directive-slot-attributes.md
@@ -0,0 +1 @@
+!adf:status[!adf:carry{json="{\"attrs\":{\"localId\":\"x\"},\"text\":\"ok\",\"type\":\"text\"}"}]{color=red}
diff --git a/corpus/errors/directive-text-plain.md b/corpus/errors/directive-text-plain.md
index b52b563..4de8110 100644
--- a/corpus/errors/directive-text-plain.md
+++ b/corpus/errors/directive-text-plain.md
@@ -1 +1 @@
-Part :text{text=plain} here.
+Part !adf:text{text=plain} here.
diff --git a/corpus/errors/nesting-depth-composed.md b/corpus/errors/nesting-depth-composed.md
index dd0d6be..ccb645f 100644
--- a/corpus/errors/nesting-depth-composed.md
+++ b/corpus/errors/nesting-depth-composed.md
@@ -1 +1 @@
-- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - :a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[:a[x]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
+- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - !adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[!adf:a[x]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
diff --git a/corpus/round-trip/block-nodes/table-pipe.md b/corpus/round-trip/block-nodes/table-pipe.md
index 3935489..87217e6 100644
--- a/corpus/round-trip/block-nodes/table-pipe.md
+++ b/corpus/round-trip/block-nodes/table-pipe.md
@@ -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 | |
diff --git a/corpus/round-trip/combinations/attribute-escapes.md b/corpus/round-trip/combinations/attribute-escapes.md
index 534efe2..afc54de 100644
--- a/corpus/round-trip/combinations/attribute-escapes.md
+++ b/corpus/round-trip/combinations/attribute-escapes.md
@@ -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)"}
diff --git a/corpus/round-trip/combinations/carry-attributes.md b/corpus/round-trip/combinations/carry-attributes.md
index 0f5a99b..981a7ab 100644
--- a/corpus/round-trip/combinations/carry-attributes.md
+++ b/corpus/round-trip/combinations/carry-attributes.md
@@ -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.
diff --git a/corpus/round-trip/combinations/carry-marks.md b/corpus/round-trip/combinations/carry-marks.md
index bcd5077..3ce9c8c 100644
--- a/corpus/round-trip/combinations/carry-marks.md
+++ b/corpus/round-trip/combinations/carry-marks.md
@@ -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
diff --git a/corpus/round-trip/combinations/carry-text-attributes.md b/corpus/round-trip/combinations/carry-text-attributes.md
index dad9c07..2fd338d 100644
--- a/corpus/round-trip/combinations/carry-text-attributes.md
+++ b/corpus/round-trip/combinations/carry-text-attributes.md
@@ -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\"}"}
diff --git a/corpus/round-trip/combinations/carve-out-directive.json b/corpus/round-trip/combinations/carve-out-directive.json
index 55135a3..736b8b9 100644
--- a/corpus/round-trip/combinations/carve-out-directive.json
+++ b/corpus/round-trip/combinations/carve-out-directive.json
@@ -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"
},
{
diff --git a/corpus/round-trip/combinations/carve-out-directive.md b/corpus/round-trip/combinations/carve-out-directive.md
index e3c46cf..12a0e69 100644
--- a/corpus/round-trip/combinations/carve-out-directive.md
+++ b/corpus/round-trip/combinations/carve-out-directive.md
@@ -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}
diff --git a/corpus/round-trip/combinations/code-span-joined-run.md b/corpus/round-trip/combinations/code-span-joined-run.md
index a89d1f6..092dc3c 100644
--- a/corpus/round-trip/combinations/code-span-joined-run.md
+++ b/corpus/round-trip/combinations/code-span-joined-run.md
@@ -1,5 +1,5 @@
\`\` \`\``a`
-:underline[\`\`]\`\``a`
+!adf:underline[\`\`]\`\``a`
\`\`\\\``` ` ``\`
diff --git a/corpus/round-trip/combinations/combined-page.md b/corpus/round-trip/combinations/combined-page.md
index 17439fd..7a8ddd4 100644
--- a/corpus/round-trip/combinations/combined-page.md
+++ b/corpus/round-trip/combinations/combined-page.md
@@ -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].
---
diff --git a/corpus/round-trip/combinations/directive-autolink-backtick.md b/corpus/round-trip/combinations/directive-autolink-backtick.md
index 163f313..4d744f7 100644
--- a/corpus/round-trip/combinations/directive-autolink-backtick.md
+++ b/corpus/round-trip/combinations/directive-autolink-backtick.md
@@ -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)]
diff --git a/corpus/round-trip/combinations/directive-link-backticks.md b/corpus/round-trip/combinations/directive-link-backticks.md
index 85d19f6..7bdc3e3 100644
--- a/corpus/round-trip/combinations/directive-link-backticks.md
+++ b/corpus/round-trip/combinations/directive-link-backticks.md
@@ -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)]
diff --git a/corpus/round-trip/combinations/directive-link-brackets.md b/corpus/round-trip/combinations/directive-link-brackets.md
index e677943..3ef541c 100644
--- a/corpus/round-trip/combinations/directive-link-brackets.md
+++ b/corpus/round-trip/combinations/directive-link-brackets.md
@@ -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)]
diff --git a/corpus/round-trip/combinations/directive-link-colon.md b/corpus/round-trip/combinations/directive-link-colon.md
deleted file mode 100644
index ecf5427..0000000
--- a/corpus/round-trip/combinations/directive-link-colon.md
+++ /dev/null
@@ -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{)]
diff --git a/corpus/round-trip/combinations/directive-link-colon.json b/corpus/round-trip/combinations/directive-link-prefix.json
similarity index 73%
rename from corpus/round-trip/combinations/directive-link-colon.json
rename to corpus/round-trip/combinations/directive-link-prefix.json
index 2cc9c86..eb1c0ea 100644
--- a/corpus/round-trip/combinations/directive-link-colon.json
+++ b/corpus/round-trip/combinations/directive-link-prefix.json
@@ -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"
}
],
diff --git a/corpus/round-trip/combinations/directive-link-prefix.md b/corpus/round-trip/combinations/directive-link-prefix.md
new file mode 100644
index 0000000..d584e7d
--- /dev/null
+++ b/corpus/round-trip/combinations/directive-link-prefix.md
@@ -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)]
diff --git a/corpus/round-trip/combinations/link-definition-carry.md b/corpus/round-trip/combinations/link-definition-carry.md
index a06706f..66f8434 100644
--- a/corpus/round-trip/combinations/link-definition-carry.md
+++ b/corpus/round-trip/combinations/link-definition-carry.md
@@ -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\"}"}
diff --git a/corpus/round-trip/combinations/link-definition-line.md b/corpus/round-trip/combinations/link-definition-line.md
index b5c5a28..0f41766 100644
--- a/corpus/round-trip/combinations/link-definition-line.md
+++ b/corpus/round-trip/combinations/link-definition-line.md
@@ -1,6 +1,6 @@
-\[:date{timestamp="]:a"}
+\[!adf:date{timestamp="]:a"}
> \[`]: a`
-- \[:date{timestamp="]:a"}\
+- \[!adf:date{timestamp="]:a"}\
b
diff --git a/corpus/round-trip/combinations/mark-inside-word.md b/corpus/round-trip/combinations/mark-inside-word.md
index 6d15c53..1224510 100644
--- a/corpus/round-trip/combinations/mark-inside-word.md
+++ b/corpus/round-trip/combinations/mark-inside-word.md
@@ -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
diff --git a/corpus/round-trip/combinations/mark-run-carry.md b/corpus/round-trip/combinations/mark-run-carry.md
index 9114578..be4accc 100644
--- a/corpus/round-trip/combinations/mark-run-carry.md
+++ b/corpus/round-trip/combinations/mark-run-carry.md
@@ -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**_
diff --git a/corpus/round-trip/combinations/mark-run-matching.md b/corpus/round-trip/combinations/mark-run-matching.md
index bc30a65..56c4fa7 100644
--- a/corpus/round-trip/combinations/mark-run-matching.md
+++ b/corpus/round-trip/combinations/mark-run-matching.md
@@ -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~~
diff --git a/corpus/round-trip/combinations/mark-run.md b/corpus/round-trip/combinations/mark-run.md
index 863dc71..051f819 100644
--- a/corpus/round-trip/combinations/mark-run.md
+++ b/corpus/round-trip/combinations/mark-run.md
@@ -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)
diff --git a/corpus/round-trip/combinations/pipe-cell-whitespace.md b/corpus/round-trip/combinations/pipe-cell-whitespace.md
index 05c77e5..6d01590 100644
--- a/corpus/round-trip/combinations/pipe-cell-whitespace.md
+++ b/corpus/round-trip/combinations/pipe-cell-whitespace.md
@@ -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=" "} |
diff --git a/corpus/round-trip/commonmark-subset/hard-break.md b/corpus/round-trip/commonmark-subset/hard-break.md
index ebede47..e7bdab5 100644
--- a/corpus/round-trip/commonmark-subset/hard-break.md
+++ b/corpus/round-trip/commonmark-subset/hard-break.md
@@ -1,6 +1,6 @@
Line one\
Line two
-Trailing:hardBreak{}
+Trailing!adf:hardBreak{}
-## Two:hardBreak{}lines
+## Two!adf:hardBreak{}lines
diff --git a/corpus/round-trip/inline-nodes/content-slot.md b/corpus/round-trip/inline-nodes/content-slot.md
index 2d247db..289f4e2 100644
--- a/corpus/round-trip/inline-nodes/content-slot.md
+++ b/corpus/round-trip/inline-nodes/content-slot.md
@@ -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.
diff --git a/corpus/round-trip/inline-nodes/date.md b/corpus/round-trip/inline-nodes/date.md
index ef5707d..5d225ed 100644
--- a/corpus/round-trip/inline-nodes/date.md
+++ b/corpus/round-trip/inline-nodes/date.md
@@ -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}.
diff --git a/corpus/round-trip/inline-nodes/emoji.md b/corpus/round-trip/inline-nodes/emoji.md
index d3f6533..3bc060d 100644
--- a/corpus/round-trip/inline-nodes/emoji.md
+++ b/corpus/round-trip/inline-nodes/emoji.md
@@ -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.
diff --git a/corpus/round-trip/inline-nodes/hard-break-attributes.md b/corpus/round-trip/inline-nodes/hard-break-attributes.md
index 1bcc6f6..8fad5a1 100644
--- a/corpus/round-trip/inline-nodes/hard-break-attributes.md
+++ b/corpus/round-trip/inline-nodes/hard-break-attributes.md
@@ -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
diff --git a/corpus/round-trip/inline-nodes/inline-card.md b/corpus/round-trip/inline-nodes/inline-card.md
index 2ef9f06..0da29fd 100644
--- a/corpus/round-trip/inline-nodes/inline-card.md
+++ b/corpus/round-trip/inline-nodes/inline-card.md
@@ -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}
diff --git a/corpus/round-trip/inline-nodes/mark-content-slot.md b/corpus/round-trip/inline-nodes/mark-content-slot.md
index 83ee860..d4c1882 100644
--- a/corpus/round-trip/inline-nodes/mark-content-slot.md
+++ b/corpus/round-trip/inline-nodes/mark-content-slot.md
@@ -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.
diff --git a/corpus/round-trip/inline-nodes/mark-nesting.md b/corpus/round-trip/inline-nodes/mark-nesting.md
index e0a2959..41c2451 100644
--- a/corpus/round-trip/inline-nodes/mark-nesting.md
+++ b/corpus/round-trip/inline-nodes/mark-nesting.md
@@ -1,3 +1,3 @@
-_:underline[Rewired]_ at the mains.
+_!adf:underline[Rewired]_ at the mains.
-:underline[_Rewound_] by hand.
+!adf:underline[_Rewound_] by hand.
diff --git a/corpus/round-trip/inline-nodes/marks.md b/corpus/round-trip/inline-nodes/marks.md
index c69b440..4a1c203 100644
--- a/corpus/round-trip/inline-nodes/marks.md
+++ b/corpus/round-trip/inline-nodes/marks.md
@@ -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}
diff --git a/corpus/round-trip/inline-nodes/media-inline.md b/corpus/round-trip/inline-nodes/media-inline.md
index 64cd79b..781d25d 100644
--- a/corpus/round-trip/inline-nodes/media-inline.md
+++ b/corpus/round-trip/inline-nodes/media-inline.md
@@ -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.
diff --git a/corpus/round-trip/inline-nodes/mention.md b/corpus/round-trip/inline-nodes/mention.md
index a39976c..973db6e 100644
--- a/corpus/round-trip/inline-nodes/mention.md
+++ b/corpus/round-trip/inline-nodes/mention.md
@@ -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.
diff --git a/corpus/round-trip/inline-nodes/status.md b/corpus/round-trip/inline-nodes/status.md
index ed01471..ab8f278 100644
--- a/corpus/round-trip/inline-nodes/status.md
+++ b/corpus/round-trip/inline-nodes/status.md
@@ -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.
diff --git a/corpus/round-trip/inline-nodes/text-whitespace.md b/corpus/round-trip/inline-nodes/text-whitespace.md
index 89a815f..ecdf6b9 100644
--- a/corpus/round-trip/inline-nodes/text-whitespace.md
+++ b/corpus/round-trip/inline-nodes/text-whitespace.md
@@ -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
diff --git a/corpus/round-trip/opaque-carry/unknown-inline.json b/corpus/round-trip/opaque-carry/unknown-inline.json
index 0dd91b4..280d7bc 100644
--- a/corpus/round-trip/opaque-carry/unknown-inline.json
+++ b/corpus/round-trip/opaque-carry/unknown-inline.json
@@ -56,7 +56,7 @@
{
"content": [
{
- "text": ":",
+ "text": "!adf:",
"type": "text"
},
{
diff --git a/corpus/round-trip/opaque-carry/unknown-inline.md b/corpus/round-trip/opaque-carry/unknown-inline.md
index 22343ff..a5d8b95 100644
--- a/corpus/round-trip/opaque-carry/unknown-inline.md
+++ b/corpus/round-trip/opaque-carry/unknown-inline.md
@@ -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**
diff --git a/spec/flavour.md b/spec/flavour.md
index 4ced431..5328870 100644
--- a/spec/flavour.md
+++ b/spec/flavour.md
@@ -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`
diff --git a/src/markdown-property.test.ts b/src/markdown-property.test.ts
index 1778594..ee2e885 100644
--- a/src/markdown-property.test.ts
+++ b/src/markdown-property.test.ts
@@ -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('', '', '', "", '', '', '', '', '', '
', '')
-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) => 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,
},
diff --git a/src/markdown/code-language.ts b/src/markdown/code-language.ts
index 27f3a07..6d4ef4e 100644
--- a/src/markdown/code-language.ts
+++ b/src/markdown/code-language.ts
@@ -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' }
}
diff --git a/src/markdown/directive-syntax.test.ts b/src/markdown/directive-syntax.test.ts
index ae9f20f..b005f83 100644
--- a/src/markdown/directive-syntax.test.ts
+++ b/src/markdown/directive-syntax.test.ts
@@ -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`)
})
diff --git a/src/markdown/directive-syntax.ts b/src/markdown/directive-syntax.ts
index 3c0ec9e..cc418c6 100644
--- a/src/markdown/directive-syntax.ts
+++ b/src/markdown/directive-syntax.ts
@@ -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 | 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 | 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
diff --git a/src/markdown/emit/adf-to-markdown.test.ts b/src/markdown/emit/adf-to-markdown.test.ts
index 5995088..69d5c0d 100644
--- a/src/markdown/emit/adf-to-markdown.test.ts
+++ b/src/markdown/emit/adf-to-markdown.test.ts
@@ -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\\ \\