Compare commits
5 Commits
79d9b4db53
...
608ef266ed
| Author | SHA1 | Date | |
|---|---|---|---|
| 608ef266ed | |||
| a2c620da27 | |||
| e4eecebeab | |||
| 230d590fb4 | |||
| 7846502a1a |
@@ -174,9 +174,9 @@ allow, rendered, shuffled, with no rationale and nothing saying what is implemen
|
|||||||
settle it; otherwise four more read, five of seven settle it, and less is a missing goal, asked.
|
settle it; otherwise four more read, five of seven settle it, and less is a missing goal, asked.
|
||||||
The verdict lands in `docs/decisions.md`.
|
The verdict lands in `docs/decisions.md`.
|
||||||
|
|
||||||
Every new or changed markdown or HTML spelling goes to such a panel, seated by the people who read
|
A writer panel settles every new or changed markdown or HTML spelling: a reader panel whose readers
|
||||||
and write that format (README `## Audience`); a question about what an app relies on seats the
|
are the people who read and write that format (README `## Audience`). A question about what an app
|
||||||
developer personas.
|
relies on goes to the developer personas instead.
|
||||||
|
|
||||||
### Stated numbers
|
### Stated numbers
|
||||||
|
|
||||||
|
|||||||
+12
-9
@@ -4,18 +4,21 @@
|
|||||||
|
|
||||||
- **Breaking:** directives, the inline opaque carry among them (now `!adf:carry{json="…"}`), are
|
- **Breaking:** directives, the inline opaque carry among them (now `!adf:carry{json="…"}`), are
|
||||||
spelled under an `!adf:` prefix (`!adf:name … !adf:/name`, `!adf:name[content]{attrs}`,
|
spelled under an `!adf:` prefix (`!adf:name … !adf:/name`, `!adf:name[content]{attrs}`,
|
||||||
`!adf:name arg {attrs}`) in place of the `:::`/`::`/`:name` forms, and the block carry is a code
|
`!adf:name arg {attrs}`) in place of the `:::`/`::`/`:name` forms: text holding an unescaped
|
||||||
fence whose info string `adf:<type>` names the node's type, its body the node's JSON without
|
`!adf:` is claimed, and `adf` is an ordinary code block language. Convert stored markdown per
|
||||||
`type`: text holding an unescaped `!adf:` and a code fence whose info string opens `adf:` are
|
`MIGRATION.md`.
|
||||||
claimed, and `adf` is an ordinary code block language. Convert stored markdown per `MIGRATION.md`.
|
- **Breaking:** the block carry is a code fence whose info string `adf:<type>` names the node's
|
||||||
|
type, its body the node's JSON without `type`, and a code fence whose info string opens `adf:` is
|
||||||
|
claimed. Convert stored markdown per `MIGRATION.md`.
|
||||||
- **Breaking:** `markdownToAdf` and `plainMarkdownToAdf` read markdown holding no block as a
|
- **Breaking:** `markdownToAdf` and `plainMarkdownToAdf` read markdown holding no block as a
|
||||||
document whose `content` is empty, as Atlassian's schema requires; `!adf:doc {content=none}`
|
document whose `content` is empty, as Atlassian's schema requires; `!adf:doc {content=none}`
|
||||||
spells a document holding no `content` key.
|
spells a document holding no `content` key.
|
||||||
- `markdownToAdf(adfToMarkdown(doc))` deep-equals `doc`: two adjacent text nodes a reader would
|
- `markdownToAdf(adfToMarkdown(doc))` deep-equals `doc` as JSON, for a document of plain objects as
|
||||||
join are parted by `!adf:textBreak{}`, an empty `attrs`, `content` or `marks` is spelled
|
`JSON.parse` builds them: two adjacent text nodes CommonMark would read back as one are parted by
|
||||||
`{attrs=empty}`, `{content=empty}` or `{marks=empty}`, `-0` is spelled `-0`, and a `codeBlock`
|
`!adf:textBreak{}`, an empty `attrs`, `content` or `marks` is spelled `{attrs=empty}`,
|
||||||
of several text nodes is a fence per node. A `codeBlock` holding other than plain text nodes
|
`{content=empty}` or `{marks=empty}`, `-0` is spelled `-0`, and a `codeBlock` of several text
|
||||||
rides the block carry, where it was refused.
|
nodes is a fence per node. A `codeBlock` holding other than plain text nodes rides the block
|
||||||
|
carry, where it was refused.
|
||||||
- **Breaking:** `unspellable-link` leaves `ConvertErrorCode`; a link whose `href` or `title` no
|
- **Breaking:** `unspellable-link` leaves `ConvertErrorCode`; a link whose `href` or `title` no
|
||||||
CommonMark escape spells is written as `!adf:link[text]{attrs}`.
|
CommonMark escape spells is written as `!adf:link[text]{attrs}`.
|
||||||
- **Breaking:** some directive refusals carry `malformed-directive` where they carried
|
- **Breaking:** some directive refusals carry `malformed-directive` where they carried
|
||||||
|
|||||||
+4
-4
@@ -5,9 +5,9 @@
|
|||||||
Directives moved under the `!adf:` prefix. `0.2.0` reads `0.1.0`'s spelling without an error,
|
Directives moved under the `!adf:` prefix. `0.2.0` reads `0.1.0`'s spelling without an error,
|
||||||
turning each directive into text and each carried node into an `adf` code block. Before `0.2.0`
|
turning each directive into text and each carried node into an `adf` code block. Before `0.2.0`
|
||||||
reads any `0.1.0` markdown, convert what is stored or in flight (an open editor, a queue) with the
|
reads any `0.1.0` markdown, convert what is stored or in flight (an open editor, a queue) with the
|
||||||
recipe below, and rewrite markdown your code writes or matches (templates, prompts, patterns) by
|
recipe below, and rewrite markdown your code writes or matches (templates, prompts, patterns) by the
|
||||||
the tables below. Stored ADF needs one change: a document `0.1.0` read from empty markdown holds
|
tables below. Stored ADF needs one change: give a document holding no `content` key `content: []`.
|
||||||
no `content` key, and gets `content: []`, the empty document it meant.
|
`0.1.0` read empty markdown to that shape, meaning the empty document.
|
||||||
|
|
||||||
### Convert markdown
|
### Convert markdown
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ import { markdownToAdf as markdownToAdf010 } from 'adf-codec-0.1'
|
|||||||
|
|
||||||
function migrateMarkdown(stored: string) {
|
function migrateMarkdown(stored: string) {
|
||||||
const parsed = markdownToAdf010(stored)
|
const parsed = markdownToAdf010(stored)
|
||||||
// 0.1.0's reader was editor-normal, so a document with no content key always meant an empty one.
|
// 0.1.0 dropped an empty content array, so a document with no content key meant an empty one.
|
||||||
return parsed.ok ? adfToMarkdown({ ...parsed.value, content: parsed.value.content ?? [] }) : parsed
|
return parsed.ok ? adfToMarkdown({ ...parsed.value, content: parsed.value.content ?? [] }) : parsed
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ Parsing — `markdownToAdf` and `plainMarkdownToAdf`, and `htmlToAdf` at `0.2.0`
|
|||||||
|
|
||||||
| Code | Fires when | What you can do |
|
| Code | Fires when | What you can do |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| `malformed-directive` | an `!adf:` the grammar cannot read — a prefix completing no directive, an unclosed container, `[content]` or `{attrs}`, a closer with no container of its name open, a leaf given a body, `{attrs}` out of order or duplicated, invalid JSON in an opaque carry | write the spelling the message names, or keep it literal: escape the prefix — `\!adf:`, block and inline alike — or wrap a code fence whose info string opens `adf:` in `!adf:codeBlock {language="adf:…"}` with a bare fence |
|
| `malformed-directive` | an `!adf:` the grammar cannot read — a prefix completing no directive, an unclosed container, `[content]` or `{attrs}`, a closer with no container of its name open, a leaf given a body, `{attrs}` out of order or duplicated, invalid JSON in an opaque carry | write the spelling the message names, or keep it literal: escape the prefix — `\!adf:`, block and inline alike — or, for a code fence whose info string opens `adf:`, drop the info string and wrap the fence in `!adf:codeBlock {language="adf:…"}` |
|
||||||
| `malformed-pipe-table` | a pipe row that is no pipe table — a missing or ragged `---` delimiter row, an alignment colon in it, or a row not opening with a pipe | open every row with a pipe and give the delimiter row the header's cell count; to keep the lines literal text instead, escape the leading pipe of every one — escaping a single row leaves the next to open a fresh table and fail the same way |
|
| `malformed-pipe-table` | a pipe row that is no pipe table — a missing or ragged `---` delimiter row, an alignment colon in it, or a row not opening with a pipe | open every row with a pipe and give the delimiter row the header's cell count; to keep the lines literal text instead, escape the leading pipe of every one — escaping a single row leaves the next to open a fresh table and fail the same way |
|
||||||
| `unknown-directive-name` | a directive whose name is no node or mark this version spells | check the name in `spec/flavour.md`, or escape the prefix as `\!adf:`; the spelling itself is well formed, so a later minor may give the name meaning |
|
| `unknown-directive-name` | a directive whose name is no node or mark this version spells | check the name in `spec/flavour.md`, or escape the prefix as `\!adf:`; the spelling itself is well formed, so a later minor may give the name meaning |
|
||||||
| `unmappable-html` | the input holds an HTML construct the documented element set does not map, a comment and a processing instruction among them — at this version that is every raw HTML construct in markdown, the element set landing at `0.2.0` | remove the construct, or write what it holds in the lossless flavour |
|
| `unmappable-html` | the input holds an HTML construct the documented element set does not map, a comment and a processing instruction among them — at this version that is every raw HTML construct in markdown, the element set landing at `0.2.0` | remove the construct, or write what it holds in the lossless flavour |
|
||||||
@@ -198,7 +198,7 @@ emit refuses:
|
|||||||
| `unspellable-line-start` | a paragraph line begins with a code span whose backticks would read back as a code fence | put any text before the code span |
|
| `unspellable-line-start` | a paragraph line begins with a code span whose backticks would read back as a code fence | put any text before the code span |
|
||||||
| `unspellable-whitespace` | an `emoji`, `mention` or `status` holds a newline in the text its inline directive spells in the content slot | replace it with a space — an inline directive never spans lines |
|
| `unspellable-whitespace` | an `emoji`, `mention` or `status` holds a newline in the text its inline directive spells in the content slot | replace it with a space — an inline directive never spans lines |
|
||||||
| `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it |
|
| `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it |
|
||||||
| `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take, or lacks one it needs — or markdown writes as a directive a node or mark the lossless flavour spells as CommonMark | write the shape the message names, or remove the reserved directive it names; `spec/flavour.md` lists every type's attributes and body |
|
| `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take, or lacks one it needs — or markdown writes as a directive a node or mark the lossless flavour spells as CommonMark, or a reserved directive (`!adf:textBreak{}`, `!adf:listBreak`, `!adf:doc`) stands where it parts nothing | make the edit the message opens with; `spec/flavour.md` lists every type's attributes and body |
|
||||||
|
|
||||||
## The guarantees
|
## The guarantees
|
||||||
|
|
||||||
@@ -213,12 +213,13 @@ Serves Goals 1, 3 and 4.
|
|||||||
name every exception.
|
name every exception.
|
||||||
- Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML `unmappable-html`
|
- Plain CommonMark is valid input to `markdownToAdf` apart from the raw HTML `unmappable-html`
|
||||||
names, with four carve-outs — literal text matching directive, pipe-table or strikethrough syntax,
|
names, with four carve-outs — literal text matching directive, pipe-table or strikethrough syntax,
|
||||||
and a code fence whose info string opens `adf:`, are claimed (escapable — `spec/flavour.md`) — and
|
and a code fence whose info string opens `adf:`, are claimed (each can be kept literal —
|
||||||
one gap: a CommonMark image fits only as its own title-less paragraph; mid-text and titled images
|
`spec/flavour.md`) — and one gap: a CommonMark image fits only as its own title-less paragraph;
|
||||||
are error results, save an image inside another's description, which flattens into the alt text.
|
mid-text and titled images are error results, save an image inside another's description, which
|
||||||
Converting back yields the library's canonical spelling, which round-trips byte-identically —
|
flattens into the alt text. Converting back yields the library's canonical spelling, which
|
||||||
where it converts back at all: a parse succeeding is no promise of that, so keep the source until
|
round-trips byte-identically — where it converts back at all: a parse succeeding is no promise of
|
||||||
the way back succeeds. ``` ` `` ` ``` reads cleanly and then refuses.
|
that, so keep the source until the way back succeeds. ``` ` `` ` ``` reads cleanly and then
|
||||||
|
refuses.
|
||||||
- Four CommonMark spellings parse without an error and build a document the reference
|
- Four CommonMark spellings parse without an error and build a document the reference
|
||||||
implementation renders differently: `[](/url)` and `[]()` stay literal text against CommonMark's
|
implementation renders differently: `[](/url)` and `[]()` stay literal text against CommonMark's
|
||||||
empty link, a list continuing past a marker change stays one list against CommonMark's two, a
|
empty link, a list continuing past a marker change stays one list against CommonMark's two, a
|
||||||
|
|||||||
+31
-38
@@ -36,28 +36,23 @@ does not imply a spellable document; `corpus/commonmark-spec/exceptions.json` na
|
|||||||
2026-08-24, deep 2026-10-03, the maintainer. Goal 1. Valid while a pipeline or a bot can build a
|
2026-08-24, deep 2026-10-03, the maintainer. Goal 1. Valid while a pipeline or a bot can build a
|
||||||
shape the editor would not.
|
shape the editor would not.
|
||||||
|
|
||||||
"Equals" is deep equality over the document's JSON values: `assert.deepStrictEqual` on plain
|
"Equals" is deep equality over the document's JSON values: `assert.deepStrictEqual` on plain objects
|
||||||
objects as `JSON.parse` builds them, since ADF is JSON. Every key and value `doc` holds counts, two
|
as `JSON.parse` builds them, since ADF is JSON. Every key and value in `doc` counts, including two
|
||||||
adjacent text nodes, an empty `attrs`, `content` or `marks` and `-0` included, with no
|
adjacent text nodes, an empty `attrs`, `content` or `marks`, and `-0`. Neither side is normalized.
|
||||||
normalization on either side.
|
CommonMark's spelling stays wherever a document holds none of those shapes. The plain reader builds
|
||||||
CommonMark's spelling stays wherever a document holds none of those shapes. The plain reader
|
what is written, as `markdownToAdf` does. Only the plain writer is lossy: its reduction reads and
|
||||||
builds what is written, as `markdownToAdf` does. Only the plain writer is lossy: its reduction
|
writes editor-normal ADF — adjacent text nodes of identical marks and no attributes merged, `-0` as
|
||||||
reads and writes editor-normal ADF — adjacent text nodes of identical marks and no attributes
|
`0`, and an empty `attrs`, `content` or `marks` the absent key — so two documents the editor holds
|
||||||
merged, `-0` as `0`, and an empty `attrs`, `content` or `marks` the absent key — so two documents
|
equal write the same plain markdown.
|
||||||
the editor holds equal write the same plain markdown.
|
|
||||||
|
|
||||||
## `!adf:textBreak{}` parts text CommonMark would join
|
## `!adf:textBreak{}` parts text CommonMark would join
|
||||||
|
|
||||||
2026-10-03, the maintainer. Goal 1. Valid while CommonMark reads adjacent text as one run.
|
2026-10-03, the maintainer. Goal 1. Valid while CommonMark reads adjacent text as one run.
|
||||||
|
|
||||||
Two adjacent text nodes a reader would build back as one — neither carried, neither holding
|
Two adjacent text nodes CommonMark would read back as one are parted by the reserved inline leaf
|
||||||
`attrs` or an empty key, their marks identical, attributes included — are parted by the reserved
|
`!adf:textBreak{}`, mirroring `!adf:listBreak`: a leaf building no node keeps both nodes and asks
|
||||||
inline leaf `!adf:textBreak{}`, mirroring `!adf:listBreak`: it builds no node, and anywhere but
|
nothing of the text around it. A code span holds no directive, so the spans close and reopen
|
||||||
between two such nodes, or given `[content]` or `{attrs}`, it is `unsupported-node-shape`. It sits
|
around the leaf. The grammar: `spec/flavour.md` §Inline nodes, **Adjacent text nodes**.
|
||||||
inside every mark spelling the pair shares, `**Hello, !adf:textBreak{}world**`; a code span holds
|
|
||||||
no directive, so it closes and reopens, `` `a`!adf:textBreak{}`b` ``. A carried node never joins
|
|
||||||
its neighbour, so a carry needs no break. A mark run breaks on any difference in the mark, `attrs:
|
|
||||||
{}` against no `attrs` included.
|
|
||||||
|
|
||||||
## An empty key spells `empty`
|
## An empty key spells `empty`
|
||||||
|
|
||||||
@@ -84,14 +79,14 @@ the directive form, since no list marker spells the sign.
|
|||||||
|
|
||||||
## Empty markdown is a document of no blocks
|
## Empty markdown is a document of no blocks
|
||||||
|
|
||||||
2026-10-03, a panel and the maintainer. Goals 1 and 5. Valid while ADF's schema requires
|
2026-10-03, a writer panel and the maintainer. Goals 1 and 5. Valid while ADF's schema requires
|
||||||
`content` on `doc`.
|
`content` on `doc`.
|
||||||
|
|
||||||
Markdown holding no block reads as `{ content: [], type: 'doc', version: 1 }`, the document
|
Markdown holding no block reads as `{ content: [], type: 'doc', version: 1 }`, the document
|
||||||
`spec/adf-schema/full.json` requires. A document holding no `content` key is
|
`spec/adf-schema/full.json` requires. A document holding no `content` key is
|
||||||
`!adf:doc {content=none}` standing alone as its only block, and a named error anywhere else. The
|
`!adf:doc {content=none}` as its only block, and a named error anywhere else. The writer panel split
|
||||||
writer panel split 4 for `none` and 3 for `absent`, and the maintainer chose `none`; all seven rejected a
|
4 for `none` and 3 for `absent`, and the maintainer chose `none`; all seven rejected a bare
|
||||||
bare `!adf:doc`.
|
`!adf:doc`.
|
||||||
|
|
||||||
## Unknown nodes ride the carry
|
## Unknown nodes ride the carry
|
||||||
|
|
||||||
@@ -114,10 +109,10 @@ The block carry is a code fence whose info string `adf:<type>` names the node's
|
|||||||
node's JSON without `type`: ```` ```adf:blockCard ````. A type no info string carries back — by the
|
node's JSON without `type`: ```` ```adf:blockCard ````. A type no info string carries back — by the
|
||||||
rule a code language follows — leaves the info string `adf:` and keeps `type` in the body. Every
|
rule a code language follows — leaves the info string `adf:` and keeps `type` in the body. Every
|
||||||
info string opening `adf:` is reserved, so a `codeBlock` whose language opens so takes the
|
info string opening `adf:` is reserved, so a `codeBlock` whose language opens so takes the
|
||||||
`language` attribute, and `carry` is an ordinary language. A body holding `type` under a named
|
`language` attribute, and `carry` is an ordinary language. A body holding `type` under a named type,
|
||||||
type, or an `adf:` fence whose type an info string carries, is `unsupported-node-shape`. The
|
or a bare `adf:` fence whose body's `type` an info string could carry, is `unsupported-node-shape`.
|
||||||
reservation claims a fence CommonMark reads as code until `todo.md` item 43 gives CommonMark its own
|
The reservation claims a fence CommonMark reads as code until `todo.md` item 43 gives CommonMark its
|
||||||
reader.
|
own reader.
|
||||||
|
|
||||||
## A code block is a fence per text node
|
## A code block is a fence per text node
|
||||||
|
|
||||||
@@ -125,11 +120,9 @@ reader.
|
|||||||
node.
|
node.
|
||||||
|
|
||||||
A `codeBlock` holding several text nodes is the `!adf:codeBlock` container holding one fence per
|
A `codeBlock` holding several text nodes is the `!adf:codeBlock` container holding one fence per
|
||||||
node, each fence's info string the language; its other attributes sit on the opener, and a
|
node, so each node keeps its own text. The fences carry one info string, since ADF holds one
|
||||||
language no info string carries stays the opener's `language` with bare fences. A `codeBlock`
|
language, and none is empty beside another, since a text node holds text. The grammar:
|
||||||
spelling `content=empty` has no fence to carry the language, so the opener does. Fences with
|
`spec/flavour.md` §The CommonMark blocks, the `codeBlock` bullet.
|
||||||
differing info strings are `unsupported-node-shape` — ADF holds one language — and so is an empty
|
|
||||||
fence beside another, since a text node holds text.
|
|
||||||
|
|
||||||
## Foreign HTML sorts three ways
|
## Foreign HTML sorts three ways
|
||||||
|
|
||||||
@@ -372,14 +365,14 @@ handles one cause alike whichever node, attribute or direction raised it.
|
|||||||
colon rather than ADF's missing column model doing it. What the grammar itself refuses stays a
|
colon rather than ADF's missing column model doing it. What the grammar itself refuses stays a
|
||||||
claim code, key order among it, and a leaf given a body is refused at its opener, as a container
|
claim code, key order among it, and a leaf given a body is refused at its opener, as a container
|
||||||
missing its closer is (2026-09-16).
|
missing its closer is (2026-09-16).
|
||||||
- A directive whose name reads back to no node is `unknown-directive-name` rather than a claim
|
- A directive whose name reads back to no node is `unknown-directive-name` rather than a claim code
|
||||||
code — the spelling is well formed, and telling that apart from a typo is what a consumer
|
— the spelling is well formed, and telling that apart from a typo is what a consumer switches on
|
||||||
switches on when a later MINOR gives the name meaning. A reserved name is a known name, so never
|
when a later MINOR gives the name meaning. A reserved name is a known name, so never that code,
|
||||||
that code, and the names the flavour reserves part on form: a form the grammar does not have is
|
and the names the flavour reserves part on form: a form the grammar does not have is a claim code
|
||||||
a claim code — `!adf:carry`, whose carry is the fence — and a well-formed form in the wrong place
|
— `!adf:carry`, whose carry is the fence — and a well-formed form in the wrong place is
|
||||||
is `unsupported-node-shape`, `!adf:listBreak` parting anything but two adjacent lists of one
|
`unsupported-node-shape`, `!adf:listBreak` parting anything but two adjacent lists of one type,
|
||||||
type, `!adf:textBreak{}` anything but two text nodes a reader joins, `!adf:doc` standing beside
|
`!adf:textBreak{}` anything but two adjacent text nodes CommonMark would read back as one,
|
||||||
another block (2026-09-01, the text break and `doc` 2026-10-03).
|
`!adf:doc` standing beside another block (2026-09-01, the text break and `doc` 2026-10-03).
|
||||||
- A well-formed directive the node tables refuse — an attribute a node does not hold or spells
|
- A well-formed directive the node tables refuse — an attribute a node does not hold or spells
|
||||||
elsewhere, a value outside its kind or its canonical spelling, an argument, or a body of a shape
|
elsewhere, a value outside its kind or its canonical spelling, an argument, or a body of a shape
|
||||||
its content model does not take — is `unsupported-node-shape`, the emitter's code for the same
|
its content model does not take — is `unsupported-node-shape`, the emitter's code for the same
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@ The grammar of the extended markdown `adfToMarkdown` emits and `markdownToAdf` p
|
|||||||
CommonMark is a subset apart from raw HTML (below), with four carve-outs: literal text that matches
|
CommonMark is a subset apart from raw HTML (below), with four carve-outs: literal text that matches
|
||||||
directive syntax below or reads as a pipe table is claimed by the flavour, a matched `~~` pair
|
directive syntax below or reads as a pipe table is claimed by the flavour, a matched `~~` pair
|
||||||
spells `strike` (escape the `!adf:`, `|` or `~` to keep it literal), and a code fence whose info
|
spells `strike` (escape the `!adf:`, `|` or `~` to keep it literal), and a code fence whose info
|
||||||
string opens `adf:` is the opaque carry (wrap it as a bare fence in
|
string opens `adf:` is the opaque carry (drop the info string and wrap the fence in
|
||||||
`!adf:codeBlock {language="adf:…"}` to keep it code) — and one gap: a CommonMark image fits only as
|
`!adf:codeBlock {language="adf:…"}` to keep it code) — and one gap: a CommonMark image fits only as
|
||||||
its own title-less paragraph — mid-text and titled images are named errors. The emitted form is
|
its own title-less paragraph — mid-text and titled images are named errors. The emitted form is
|
||||||
contract (`docs/decisions.md` §The formats are API). Per-node syntaxes build on this grammar in the
|
contract (`docs/decisions.md` §The formats are API). Per-node syntaxes build on this grammar in the
|
||||||
@@ -181,8 +181,8 @@ product). Block and inline positions canonicalize differently, each fitting wher
|
|||||||
- **Block position**: a fenced code block with info string `adf:` and the node's type, body = the
|
- **Block position**: a fenced code block with info string `adf:` and the node's type, body = the
|
||||||
node's JSON without its `type` — two-space indent, object keys sorted: ```` ```adf:blockCard ````.
|
node's JSON without its `type` — two-space indent, object keys sorted: ```` ```adf:blockCard ````.
|
||||||
A type no info string carries back, by the rule a `codeBlock`'s language follows, leaves the info
|
A type no info string carries back, by the rule a `codeBlock`'s language follows, leaves the info
|
||||||
string `adf:` and keeps `type` in the body. A body holding `type` under a named type, or an
|
string `adf:` and keeps `type` in the body. A body holding `type` under a named type, or a bare
|
||||||
`adf:` fence whose type an info string carries, is a named error.
|
`adf:` fence whose body's `type` an info string could carry, is a named error.
|
||||||
- **Inline position**: `!adf:carry{json="…"}` — compact serialization (keys sorted, no whitespace),
|
- **Inline position**: `!adf:carry{json="…"}` — compact serialization (keys sorted, no whitespace),
|
||||||
JSON-string-escaped into the attribute.
|
JSON-string-escaped into the attribute.
|
||||||
|
|
||||||
|
|||||||
@@ -77,7 +77,6 @@ export function identicalMarks(left: readonly AdfMark[], right: readonly AdfMark
|
|||||||
return marksKey(left) === marksKey(right)
|
return marksKey(left) === marksKey(right)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Callers say when two nodes join.
|
|
||||||
export function mergeAdjacentText(nodes: readonly AdfNode[], joins: (previous: AdfNode, node: AdfNode) => boolean): AdfNode[] {
|
export function mergeAdjacentText(nodes: readonly AdfNode[], joins: (previous: AdfNode, node: AdfNode) => boolean): AdfNode[] {
|
||||||
const merged: AdfNode[] = []
|
const merged: AdfNode[] = []
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function reduceToPlain(document: AdfDocument): Result<AdfDocument> {
|
|||||||
// docs/decisions.md §Equality is deep: the reduction reads and writes editor-normal ADF.
|
// docs/decisions.md §Equality is deep: the reduction reads and writes editor-normal ADF.
|
||||||
const blocks = reduceBlocks(nodeContent(toEditorNormal(document)), { depth: 0, memo: new Map(), path: [] })
|
const blocks = reduceBlocks(nodeContent(toEditorNormal(document)), { depth: 0, memo: new Map(), path: [] })
|
||||||
if (!blocks.ok) {
|
if (!blocks.ok) {
|
||||||
// Merging text renumbers siblings, so the caller's document names the path of the refusal the editor-normal one meets.
|
// Merging text renumbers siblings, so a refusal's path comes from the caller's document.
|
||||||
const raw = reduceBlocks(nodeContent(document), { depth: 0, memo: new Map(), path: [] })
|
const raw = reduceBlocks(nodeContent(document), { depth: 0, memo: new Map(), path: [] })
|
||||||
return !raw.ok && raw.error.code === blocks.error.code ? raw : blocks
|
return !raw.ok && raw.error.code === blocks.error.code ? raw : blocks
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,10 +23,10 @@ export function readEmptyKeys(type: string, attributes: DirectiveAttributes, key
|
|||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
const spelled = rest.get(key)
|
const spelled = rest.get(key)
|
||||||
if (spelled === undefined) continue
|
if (spelled === undefined) continue
|
||||||
if (!spellsEmpty(spelled)) return { fault: unsupportedNodeShape(`the reserved key ${key} reads ${key}=${emptyValue} alone: this one spells ${key}=${spelled.spelling}`) }
|
if (!spellsEmpty(spelled)) return { fault: unsupportedNodeShape(`the reserved key ${key} takes only the value ${emptyValue}: this one spells ${key}=${spelled.spelling}`) }
|
||||||
empty.add(key)
|
empty.add(key)
|
||||||
rest.delete(key)
|
rest.delete(key)
|
||||||
}
|
}
|
||||||
if (empty.has('attrs') && (held || [...rest.keys()].some((key) => key !== 'marks'))) return { fault: unsupportedNodeShape(`${type} spells attrs=empty beside an attribute it holds`) }
|
if (empty.has('attrs') && (held || [...rest.keys()].some((key) => key !== 'marks'))) return { fault: unsupportedNodeShape(`${type} spells attrs=empty beside another attribute, an argument or content: drop attrs=empty or the rest`) }
|
||||||
return { value: { empty, rest } }
|
return { value: { empty, rest } }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function readCarriedBlock(type: string, body: string, depth: number): Rea
|
|||||||
}
|
}
|
||||||
const read = readCarriedJson(body, 'two-space', largestNesting - depth, type)
|
const read = readCarriedJson(body, 'two-space', largestNesting - depth, type)
|
||||||
if (read.fault !== undefined || type !== '' || !infoStringCarries(read.value.type)) return read
|
if (read.fault !== undefined || type !== '' || !infoStringCarries(read.value.type)) return read
|
||||||
return { fault: unsupportedNodeShape(`the carry fence names a type its info string carries: spell it ${carryFencePrefix}${read.value.type}`) }
|
return { fault: unsupportedNodeShape(`the ${carryFencePrefix} fence holds a type its info string can carry: spell the fence ${carryFencePrefix}${read.value.type} and drop type from the JSON`) }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readCarriedInline(span: DirectiveSpan): Read<AdfNode> | undefined {
|
export function readCarriedInline(span: DirectiveSpan): Read<AdfNode> | undefined {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ type SharedScan = Pick<Scan, 'definitions' | 'path'>
|
|||||||
|
|
||||||
type SlotContent = { carry: boolean; nodes: Inline[] }
|
type SlotContent = { carry: boolean; nodes: Inline[] }
|
||||||
|
|
||||||
const carriedInMark = 'no mark spelling wraps an opaque carry or an inline node spelling marks=empty: its marks are its own'
|
const carriedInMark = 'move the opaque carry, or the node spelling marks=empty, out of the mark spelling: it holds its own marks'
|
||||||
const editorHighlight: AdfMark = { attrs: { color: '#f8e6a0' }, type: 'backgroundColor' }
|
const editorHighlight: AdfMark = { attrs: { color: '#f8e6a0' }, type: 'backgroundColor' }
|
||||||
const hreflessLink = 'the link mark spells its href: this one spells none'
|
const hreflessLink = 'the link mark spells its href: this one spells none'
|
||||||
const imageAlone = 'an image fits only as a paragraph of its own: this one sits inside other content'
|
const imageAlone = 'an image fits only as a paragraph of its own: this one sits inside other content'
|
||||||
@@ -247,7 +247,7 @@ function directivePiece(scan: Scan, span: DirectiveSpan, index: number): Result<
|
|||||||
const mark = readDirectiveMark(span.name, span.attributes, scan.path)
|
const mark = readDirectiveMark(span.name, span.attributes, scan.path)
|
||||||
if (mark !== undefined) return mark.ok ? directiveMarkPiece(scan, span.name, mark.value, slot.value, index) : mark
|
if (mark !== undefined) return mark.ok ? directiveMarkPiece(scan, span.name, mark.value, slot.value, index) : mark
|
||||||
const content = slot.value?.nodes
|
const content = slot.value?.nodes
|
||||||
if (content?.some(isTextBreak) === true) return failure('unsupported-node-shape', `${textBreakName} parts two text nodes: a content slot holds plain text`, scan.path)
|
if (content?.some(isTextBreak) === true) return failure('unsupported-node-shape', `delete ${textBreakSpelling} from this content slot: a slot holds one text node`, scan.path)
|
||||||
const node = readInlineDirectiveNode(span.name, span.attributes, content === undefined ? undefined : adfNodes(content), scan.path)
|
const node = readInlineDirectiveNode(span.name, span.attributes, content === undefined ? undefined : adfNodes(content), scan.path)
|
||||||
if (!node.ok) return node
|
if (!node.ok) return node
|
||||||
return success(node.value.marks === undefined ? { kind: 'nodes', nodes: [node.value] } : { kind: 'carry', node: node.value })
|
return success(node.value.marks === undefined ? { kind: 'nodes', nodes: [node.value] } : { kind: 'carry', node: node.value })
|
||||||
@@ -488,7 +488,7 @@ function closeImage(scan: Scan, at: number, inner: readonly Piece[], definition:
|
|||||||
function imageAlt(inner: readonly Piece[], scan: Scan): Result<string> {
|
function imageAlt(inner: readonly Piece[], scan: Scan): Result<string> {
|
||||||
const nodes = resolveNodes(inner, scan, false)
|
const nodes = resolveNodes(inner, scan, false)
|
||||||
if (!nodes.ok) return nodes
|
if (!nodes.ok) return nodes
|
||||||
if (nodes.value.some(isTextBreak)) return failure('unsupported-node-shape', `${textBreakName} parts two text nodes: an image description holds plain text`, scan.path)
|
if (nodes.value.some(isTextBreak)) return failure('unsupported-node-shape', `delete ${textBreakSpelling} from this image description: the description reads as plain alt text`, scan.path)
|
||||||
return success(adfNodes(nodes.value).map(altText).join(''))
|
return success(adfNodes(nodes.value).map(altText).join(''))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ test('reads the codeBlock directive body as the node content, the info string it
|
|||||||
|
|
||||||
test('names the slot a codeBlock spells its language outside of', () => {
|
test('names the slot a codeBlock spells its language outside of', () => {
|
||||||
const slot = 'unsupported-node-shape: codeBlock spells its language in the fence info string, or in the attribute where no info string carries it back'
|
const slot = 'unsupported-node-shape: codeBlock spells its language in the fence info string, or in the attribute where no info string carries it back'
|
||||||
assert.equal(content(markdownToAdf('!adf:codeBlock {language=rust wrap=true}\n```\nx\n```\n!adf:/codeBlock\n')), slot)
|
assert.equal(content(markdownToAdf('!adf:codeBlock {language=rust wrap=true}\n```\nx\n```\n!adf:/codeBlock\n')), "unsupported-node-shape: move language=rust to the fence's info string: a fence carries the language wherever its info string can")
|
||||||
assert.equal(content(markdownToAdf('!adf:codeBlock {language=rust}\n```sql\nx\n```\n!adf:/codeBlock\n')), slot)
|
assert.equal(content(markdownToAdf('!adf:codeBlock {language=rust}\n```sql\nx\n```\n!adf:/codeBlock\n')), slot)
|
||||||
assert.equal(content(markdownToAdf('!adf:codeBlock {wrap=true}\n```adf:x\nx\n```\n!adf:/codeBlock\n')), slot)
|
assert.equal(content(markdownToAdf('!adf:codeBlock {wrap=true}\n```adf:x\nx\n```\n!adf:/codeBlock\n')), slot)
|
||||||
assert.equal(content(markdownToAdf('!adf:codeBlock {wrap=true}\n```a\\b\nx\n```\n!adf:/codeBlock\n')), slot)
|
assert.equal(content(markdownToAdf('!adf:codeBlock {wrap=true}\n```a\\b\nx\n```\n!adf:/codeBlock\n')), slot)
|
||||||
@@ -376,7 +376,7 @@ test('reads the carry fence back to the node its info string names and its JSON
|
|||||||
content(markdownToAdf('```adf:blockCard\n{\n "type": "blockCard"\n}\n```\n')),
|
content(markdownToAdf('```adf:blockCard\n{\n "type": "blockCard"\n}\n```\n')),
|
||||||
'unsupported-node-shape: the adf:blockCard fence names its node\'s type: this JSON holds a type as well; !adf:codeBlock {language="adf:blockCard"} around a bare fence keeps it a code block',
|
'unsupported-node-shape: the adf:blockCard fence names its node\'s type: this JSON holds a type as well; !adf:codeBlock {language="adf:blockCard"} around a bare fence keeps it a code block',
|
||||||
)
|
)
|
||||||
assert.equal(content(markdownToAdf('```adf:\n{\n "type": "blockCard"\n}\n```\n')), 'unsupported-node-shape: the carry fence names a type its info string carries: spell it adf:blockCard')
|
assert.equal(content(markdownToAdf('```adf:\n{\n "type": "blockCard"\n}\n```\n')), 'unsupported-node-shape: the adf: fence holds a type its info string can carry: spell the fence adf:blockCard and drop type from the JSON')
|
||||||
assert.equal(content(markdownToAdf('```adf:\n{}\n```\n')), "unsupported-node-shape: the opaque carry holds one ADF node's JSON: this JSON is no ADF node; !adf:codeBlock {language=\"adf:\"} around a bare fence keeps it a code block")
|
assert.equal(content(markdownToAdf('```adf:\n{}\n```\n')), "unsupported-node-shape: the opaque carry holds one ADF node's JSON: this JSON is no ADF node; !adf:codeBlock {language=\"adf:\"} around a bare fence keeps it a code block")
|
||||||
const unnamed = 'unsupported-node-shape: the carry fence names a type no info string carries back: spell it adf: with the type in the JSON; !adf:codeBlock {language="adf:\\\\"} around a bare fence keeps it a code block'
|
const unnamed = 'unsupported-node-shape: the carry fence names a type no info string carries back: spell it adf: with the type in the JSON; !adf:codeBlock {language="adf:\\\\"} around a bare fence keeps it a code block'
|
||||||
assert.equal(content(markdownToAdf('```adf:\\\\\n{}\n```\n')), unnamed)
|
assert.equal(content(markdownToAdf('```adf:\\\\\n{}\n```\n')), unnamed)
|
||||||
@@ -441,7 +441,7 @@ test('names the number no JSON spelling carries in an opaque carry', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('names the mark spelling no opaque carry sits inside', () => {
|
test('names the mark spelling no opaque carry sits inside', () => {
|
||||||
const named = 'unsupported-node-shape: no mark spelling wraps an opaque carry or an inline node spelling marks=empty: its marks are its own'
|
const named = 'unsupported-node-shape: move the opaque carry, or the node spelling marks=empty, out of the mark spelling: it holds its own marks'
|
||||||
assert.equal(content(markdownToAdf(`_a ${carried} b_\n`)), named)
|
assert.equal(content(markdownToAdf(`_a ${carried} b_\n`)), named)
|
||||||
assert.equal(content(markdownToAdf(`**${carried}**\n`)), named)
|
assert.equal(content(markdownToAdf(`**${carried}**\n`)), named)
|
||||||
assert.equal(content(markdownToAdf(`~~a ${carried}~~\n`)), named)
|
assert.equal(content(markdownToAdf(`~~a ${carried}~~\n`)), named)
|
||||||
@@ -1095,8 +1095,8 @@ test('reads the text break only between two text nodes CommonMark joins, buildin
|
|||||||
for (const markdown of ['!adf:textBreak{}a\n', 'a!adf:textBreak{}\n', 'a!adf:textBreak{}!adf:textBreak{}b\n', '**a**!adf:textBreak{}b\n', '[!adf:textBreak{}](/u)\n', `a!adf:textBreak{}${carried}\n`]) {
|
for (const markdown of ['!adf:textBreak{}a\n', 'a!adf:textBreak{}\n', 'a!adf:textBreak{}!adf:textBreak{}b\n', '**a**!adf:textBreak{}b\n', '[!adf:textBreak{}](/u)\n', `a!adf:textBreak{}${carried}\n`]) {
|
||||||
assert.equal(content(markdownToAdf(markdown)), parts, markdown)
|
assert.equal(content(markdownToAdf(markdown)), parts, markdown)
|
||||||
}
|
}
|
||||||
assert.equal(content(markdownToAdf('!adf:status[a!adf:textBreak{}b]\n')), 'unsupported-node-shape: textBreak parts two text nodes: a content slot holds plain text')
|
assert.equal(content(markdownToAdf('!adf:status[a!adf:textBreak{}b]\n')), 'unsupported-node-shape: delete !adf:textBreak{} from this content slot: a slot holds one text node')
|
||||||
assert.equal(content(markdownToAdf('\n')), 'unsupported-node-shape: textBreak parts two text nodes: an image description holds plain text')
|
assert.equal(content(markdownToAdf('\n')), 'unsupported-node-shape: delete !adf:textBreak{} from this image description: the description reads as plain alt text')
|
||||||
assert.equal(content(markdownToAdf('a!adf:textBreak{x=y}b\n')), 'unsupported-node-shape: textBreak spells the bare leaf form, !adf:textBreak{}: this one spells more')
|
assert.equal(content(markdownToAdf('a!adf:textBreak{x=y}b\n')), 'unsupported-node-shape: textBreak spells the bare leaf form, !adf:textBreak{}: this one spells more')
|
||||||
assert.deepEqual(content(markdownToAdf('!adf:carry{json="{\\"type\\":\\"textBreak\\"}"}\n')), [{ content: [{ type: 'textBreak' }], type: 'paragraph' }])
|
assert.deepEqual(content(markdownToAdf('!adf:carry{json="{\\"type\\":\\"textBreak\\"}"}\n')), [{ content: [{ type: 'textBreak' }], type: 'paragraph' }])
|
||||||
})
|
})
|
||||||
@@ -1110,4 +1110,12 @@ test('leads a refusal ordinary editing meets with the edit that fixes it', () =>
|
|||||||
content(markdownToAdf('!adf:codeBlock\n```\na\n```\n```\n```\n!adf:/codeBlock\n')),
|
content(markdownToAdf('!adf:codeBlock\n```\na\n```\n```\n```\n!adf:/codeBlock\n')),
|
||||||
'unsupported-node-shape: delete the empty fence: a fence beside another holds code, and this one holds none',
|
'unsupported-node-shape: delete the empty fence: a fence beside another holds code, and this one holds none',
|
||||||
)
|
)
|
||||||
|
assert.equal(
|
||||||
|
content(markdownToAdf('!adf:codeBlock {attrs=empty}\n```js\na\n```\n```js\nb\n```\n!adf:/codeBlock\n')),
|
||||||
|
"unsupported-node-shape: remove attrs=empty to give the codeBlock the fence's language: attrs=empty holds no language, and this fence names one",
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
content(markdownToAdf('!adf:panel info {attrs=empty}\nText.\n!adf:/panel\n')),
|
||||||
|
'unsupported-node-shape: panel spells attrs=empty beside another attribute, an argument or content: drop attrs=empty or the rest',
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -256,10 +256,15 @@ function codeDirectiveNode(node: AdfNode, blocks: readonly Block[], path: Conver
|
|||||||
const attribute = nodeAttrs(node)['language']
|
const attribute = nodeAttrs(node)['language']
|
||||||
const fromFence = first.language !== ''
|
const fromFence = first.language !== ''
|
||||||
const slot = languageSlot(fromFence ? first.language : attribute)
|
const slot = languageSlot(fromFence ? first.language : attribute)
|
||||||
|
if (!fromFence && slot.kind === 'fence') {
|
||||||
|
return failure('unsupported-node-shape', `move language=${slot.info} to the fence's info string: a fence carries the language wherever its info string can`, path)
|
||||||
|
}
|
||||||
if ((slot.kind === 'fence') !== fromFence || (fromFence && attribute !== undefined)) {
|
if ((slot.kind === 'fence') !== fromFence || (fromFence && attribute !== undefined)) {
|
||||||
return failure('unsupported-node-shape', `${node.type} spells its language in the fence info string, or in the attribute where no info string carries it back`, path)
|
return failure('unsupported-node-shape', `${node.type} spells its language in the fence info string, or in the attribute where no info string carries it back`, path)
|
||||||
}
|
}
|
||||||
if (fromFence && emptyKeys(node).includes('attrs')) return failure('unsupported-node-shape', `${node.type} spells attrs=empty beside an attribute it holds`, path)
|
if (fromFence && emptyKeys(node).includes('attrs')) {
|
||||||
|
return failure('unsupported-node-shape', `remove attrs=empty to give the ${node.type} the fence's language: attrs=empty holds no language, and this fence names one`, path)
|
||||||
|
}
|
||||||
const spelled = fromFence ? { ...node, attrs: { ...node.attrs, language: first.language } } : node
|
const spelled = fromFence ? { ...node, attrs: { ...node.attrs, language: first.language } } : node
|
||||||
return success(withContent(spelled, first.text === '' ? [] : fences.map((fence): AdfNode => ({ text: fence.text, type: 'text' }))))
|
return success(withContent(spelled, first.text === '' ? [] : fences.map((fence): AdfNode => ({ text: fence.text, type: 'text' }))))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
| 52 | 0.2.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 5 | 13.0 |
|
| 52 | 0.2.0 | | **Spell `colwidth` as a comma list, `colwidth="340,420"`.** | 3 | 3 | 5 | 6 | 5 | 13.0 |
|
||||||
| 51 | 0.2.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 2 | 2 | 7 | 3, 4 | 12.4 |
|
| 51 | 0.2.0 | | **Match a reference label to its definition under Unicode case folding.** | 2 | 2 | 2 | 7 | 3, 4 | 12.4 |
|
||||||
| 54 | 0.2.0 | | **Make `docs/decisions.md` §The source parts by ADF and format name every import `parse/` takes from `emit/`.** | 2 | 2 | 2 | 5 | 1 | 11.5 |
|
| 54 | 0.2.0 | | **Make `docs/decisions.md` §The source parts by ADF and format name every import `parse/` takes from `emit/`.** | 2 | 2 | 2 | 5 | 1 | 11.5 |
|
||||||
| 53 | 0.2.0 | | **Bench a block's `marks` spelling with the writer panel and adopt its pick.** | 4 | 5 | 5 | 6 | 5 | 11.5 |
|
| 53 | 0.2.0 | | **Put a block's `marks` spelling to the writer panel and adopt its pick.** | 4 | 5 | 5 | 6 | 5 | 11.5 |
|
||||||
| 50 | 0.2.0 | | **Read `[](/url)` and `[]()` as CommonMark's empty link.** | 4 | 4 | 3 | 6 | 3, 4, 6 | 10.4 |
|
| 50 | 0.2.0 | | **Read `[](/url)` and `[]()` as CommonMark's empty link.** | 4 | 4 | 3 | 6 | 3, 4, 6 | 10.4 |
|
||||||
| 38 | 0.3.0 | | **Spell a lone surrogate in a text node so it survives a UTF-8 encode.** | 2 | 2 | 4 | 7 | 1 | 19.5 |
|
| 38 | 0.3.0 | | **Spell a lone surrogate in a text node so it survives a UTF-8 encode.** | 2 | 2 | 4 | 7 | 1 | 19.5 |
|
||||||
| 47 | 0.3.0 | | **Open the README with what the package is, what it does and for whom.** | 1 | 4 | 7 | 9 | 9 | 14.0 |
|
| 47 | 0.3.0 | | **Open the README with what the package is, what it does and for whom.** | 1 | 4 | 7 | 9 | 9 | 14.0 |
|
||||||
@@ -117,7 +117,7 @@ The entry says `parse/` imports only `commonMarkSpelling` and `openingLinkTakesD
|
|||||||
`emit/`; `parse/markdown-to-adf.ts` also imports `inlineLeaves` from `emit/plain-inline.ts`. Either
|
`emit/`; `parse/markdown-to-adf.ts` also imports `inlineLeaves` from `emit/plain-inline.ts`. Either
|
||||||
the entry names it with its reason, or the reader stops importing it.
|
the entry names it with its reason, or the reader stops importing it.
|
||||||
|
|
||||||
### 53. Bench a block's `marks` spelling with the writer panel and adopt its pick.
|
### 53. Put a block's `marks` spelling to the writer panel and adopt its pick.
|
||||||
|
|
||||||
Today `marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"`, the marks array as
|
Today `marks="[{\"attrs\":{\"mode\":\"wide\"},\"type\":\"breakout\"}]"`, the marks array as
|
||||||
escaped JSON. Breaking where the panel picks another spelling: `MIGRATION.md`'s Spellings table
|
escaped JSON. Breaking where the panel picks another spelling: `MIGRATION.md`'s Spellings table
|
||||||
|
|||||||
Reference in New Issue
Block a user