Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 79d9b4db53 | |||
| 912c5039aa | |||
| fb78063f12 | |||
| 5cffe82dd5 | |||
| d9f2d325ca | |||
| 419bbcb4f2 | |||
| 4d7e8c944c | |||
| ff9bb7fab2 | |||
| 5a7279059d | |||
| 0ac5dccf11 | |||
| 6fc0d6bba9 |
+2
-2
@@ -9,8 +9,8 @@
|
|||||||
`type`: text holding an unescaped `!adf:` and a code fence whose info string opens `adf:` are
|
`type`: text holding an unescaped `!adf:` and a code fence whose info string opens `adf:` are
|
||||||
claimed, and `adf` is an ordinary code block language. Convert stored markdown per `MIGRATION.md`.
|
claimed, and `adf` is an ordinary code block language. 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}` spells a document holding no
|
document whose `content` is empty, as Atlassian's schema requires; `!adf:doc {content=none}`
|
||||||
`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`: two adjacent text nodes a reader would
|
||||||
join are parted by `!adf:textBreak{}`, an empty `attrs`, `content` or `marks` is spelled
|
join are parted by `!adf:textBreak{}`, an empty `attrs`, `content` or `marks` is spelled
|
||||||
`{attrs=empty}`, `{content=empty}` or `{marks=empty}`, `-0` is spelled `-0`, and a `codeBlock`
|
`{attrs=empty}`, `{content=empty}` or `{marks=empty}`, `-0` is spelled `-0`, and a `codeBlock`
|
||||||
|
|||||||
+6
-4
@@ -6,7 +6,8 @@ Directives moved under the `!adf:` prefix. `0.2.0` reads `0.1.0`'s spelling with
|
|||||||
turning each directive into text and each carried node into an `adf` code block. Before `0.2.0`
|
turning each directive into text and each carried node into an `adf` code block. Before `0.2.0`
|
||||||
reads any `0.1.0` markdown, convert what is stored or in flight (an open editor, a queue) with the
|
reads any `0.1.0` markdown, convert what is stored or in flight (an open editor, a queue) with the
|
||||||
recipe below, and rewrite markdown your code writes or matches (templates, prompts, patterns) by
|
recipe below, and rewrite markdown your code writes or matches (templates, prompts, patterns) by
|
||||||
the tables below. Stored ADF needs no change.
|
the tables below. Stored ADF needs one change: a document `0.1.0` read from empty markdown holds
|
||||||
|
no `content` key, and gets `content: []`, the empty document it meant.
|
||||||
|
|
||||||
### Convert markdown
|
### Convert markdown
|
||||||
|
|
||||||
@@ -30,8 +31,9 @@ function migrateMarkdown(stored: string) {
|
|||||||
- Convert each document once: a second pass can return ok while turning the directives into text.
|
- Convert each document once: a second pass can return ok while turning the directives into text.
|
||||||
Stop `0.1.0` writing first, and record which documents are converted.
|
Stop `0.1.0` writing first, and record which documents are converted.
|
||||||
- A refusal carrying `position` is `0.1.0`'s parse, which refused that markdown before too. One
|
- A refusal carrying `position` is `0.1.0`'s parse, which refused that markdown before too. One
|
||||||
without is `0.2.0`'s emit: store the document `markdownToAdf010` read as ADF rather than keeping
|
without is `0.2.0`'s emit: store the document `markdownToAdf010` read as ADF, with
|
||||||
the unconverted markdown.
|
`content: parsed.value.content ?? []` as the recipe gives it, rather than keeping the unconverted
|
||||||
|
markdown.
|
||||||
|
|
||||||
### Spellings
|
### Spellings
|
||||||
|
|
||||||
@@ -56,7 +58,7 @@ Markdown the spelling table leaves alone, which `0.2.0` reads as a different doc
|
|||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| a link whose text already holds one (`[a<https://example.com/>b](/v)`) | marks every node the inner link does not, splitting the outer link around it | leaves the outer brackets literal text; write the pieces as separate links to keep them |
|
| a link whose text already holds one (`[a<https://example.com/>b](/v)`) | marks every node the inner link does not, splitting the outer link around it | leaves the outer brackets literal text; write the pieces as separate links to keep them |
|
||||||
| markdown holding no block (`markdownToAdf("")`) | `{ type: 'doc', version: 1 }` | `{ content: [], type: 'doc', version: 1 }`; `!adf:doc {content=none}` reads as the former |
|
| markdown holding no block (`markdownToAdf("")`) | `{ type: 'doc', version: 1 }` | `{ content: [], type: 'doc', version: 1 }`; `!adf:doc {content=none}` reads as the former |
|
||||||
| a code fence whose info string opens `adf:` (```` ```adf:x ````) | a `codeBlock` with that language | the block carry; write `!adf:codeBlock {language="adf:x"}` around a bare fence to keep the code block |
|
| a code fence whose info string opens `adf:` (```` ```adf:x ````) | a `codeBlock` with that language | the block carry, refusing a body that is not one node's canonical JSON; write `!adf:codeBlock {language="adf:x"}` around a bare fence to keep the code block |
|
||||||
|
|
||||||
### Error codes
|
### Error codes
|
||||||
|
|
||||||
|
|||||||
@@ -198,28 +198,27 @@ 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; `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 | write the shape the message names, or remove the reserved directive it names; `spec/flavour.md` lists every type's attributes and body |
|
||||||
|
|
||||||
## The guarantees
|
## The guarantees
|
||||||
|
|
||||||
Serves Goals 1, 3 and 4.
|
Serves Goals 1, 3 and 4.
|
||||||
|
|
||||||
- `markdownToAdf(adfToMarkdown(doc))` deep-equals `doc` — every key and value as `doc` holds it,
|
- `markdownToAdf(adfToMarkdown(doc))` deep-equals `doc` as JSON, for a document of plain objects
|
||||||
adjacent text nodes, an empty `attrs`, `content` or `marks` and `-0` included, and unknown node
|
as `JSON.parse` builds them — every key and value as `doc` holds it, adjacent text nodes, an
|
||||||
types carried opaquely
|
empty `attrs`, `content` or `marks` and `-0` included, and unknown node types carried opaquely
|
||||||
([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)).
|
([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)).
|
||||||
- Markdown this library reads, and markdown it writes, means what the CommonMark spec says; from
|
- Markdown this library reads, and markdown it writes, means what the CommonMark spec says; from
|
||||||
`0.2.0`, well-formed HTML means what the HTML standard says, read or written. The bullets below
|
`0.2.0`, well-formed HTML means what the HTML standard says, read or written. The bullets below
|
||||||
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
|
names, with four carve-outs — literal text matching directive, pipe-table or strikethrough syntax,
|
||||||
syntax, and a code fence whose info string opens `adf:`, is claimed (escapable — `spec/flavour.md`)
|
and a code fence whose info string opens `adf:`, are claimed (escapable — `spec/flavour.md`) — and
|
||||||
— and one gap: a CommonMark image fits only as
|
one gap: a CommonMark image fits only as its own title-less paragraph; mid-text and titled images
|
||||||
its own title-less paragraph; mid-text and titled images are error results, save an image inside
|
are error results, save an image inside another's description, which flattens into the alt text.
|
||||||
another's description, which flattens into the alt text. Converting back yields the library's
|
Converting back yields the library's canonical spelling, which round-trips byte-identically —
|
||||||
canonical spelling, which round-trips byte-identically — where it converts back at all: a parse
|
where it converts back at all: a parse succeeding is no promise of that, so keep the source until
|
||||||
succeeding is no promise of that, so keep the source until the way back succeeds.
|
the way back succeeds. ``` ` `` ` ``` reads cleanly and then refuses.
|
||||||
``` ` `` ` ``` 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
|
||||||
|
|||||||
+5
-4
@@ -19,7 +19,8 @@ One directory per contract kind:
|
|||||||
model), `unspellable` (parses but the flavour has no spelling) or `pending` (a parser gap).
|
model), `unspellable` (parses but the flavour has no spelling) or `pending` (a parser gap).
|
||||||
|
|
||||||
JSON is two-space indent, keys sorted, and a document read back must deep-equal the fixture's
|
JSON is two-space indent, keys sorted, and a document read back must deep-equal the fixture's
|
||||||
(`docs/decisions.md` §Equality is deep). `spec.json` is the vendored, upstream machine-readable suite, byte-exact from
|
(`docs/decisions.md` §Equality is deep). `spec.json` is the vendored, upstream machine-readable
|
||||||
[spec.commonmark.org](https://spec.commonmark.org/0.31.2/spec.json) (CommonMark 0.31.2, © John
|
suite, byte-exact from [spec.commonmark.org](https://spec.commonmark.org/0.31.2/spec.json)
|
||||||
MacFarlane, [CC-BY-SA-4.0](https://creativecommons.org/licenses/by-sa/4.0/)), and is not
|
(CommonMark 0.31.2, © John MacFarlane,
|
||||||
re-serialized by the corpus gate.
|
[CC-BY-SA-4.0](https://creativecommons.org/licenses/by-sa/4.0/)), and is not re-serialized by the
|
||||||
|
corpus gate.
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
unsupported-node-shape
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
!adf:codeBlock {attrs=empty}
|
||||||
|
```js
|
||||||
|
a
|
||||||
|
```
|
||||||
|
```js
|
||||||
|
b
|
||||||
|
```
|
||||||
|
!adf:/codeBlock
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
unsupported-node-shape
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
!adf:codeBlock
|
||||||
|
```
|
||||||
|
a
|
||||||
|
```
|
||||||
|
```
|
||||||
|
```
|
||||||
|
!adf:/codeBlock
|
||||||
@@ -82,6 +82,13 @@
|
|||||||
},
|
},
|
||||||
"marks": [],
|
"marks": [],
|
||||||
"type": "codeBlock"
|
"type": "codeBlock"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"language": "js"
|
||||||
|
},
|
||||||
|
"content": [],
|
||||||
|
"type": "codeBlock"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "doc",
|
"type": "doc",
|
||||||
|
|||||||
@@ -30,3 +30,6 @@ Next
|
|||||||
```js
|
```js
|
||||||
```
|
```
|
||||||
!adf:/codeBlock
|
!adf:/codeBlock
|
||||||
|
|
||||||
|
!adf:codeBlock {content=empty language=js}
|
||||||
|
!adf:/codeBlock
|
||||||
|
|||||||
+4
-2
@@ -36,8 +36,10 @@ 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 `assert.deepStrictEqual`: every key and value `doc` holds, two adjacent text nodes, an
|
"Equals" is deep equality over the document's JSON values: `assert.deepStrictEqual` on plain
|
||||||
empty `attrs`, `content` or `marks` and `-0` included, with no normalization on either side.
|
objects as `JSON.parse` builds them, since ADF is JSON. Every key and value `doc` holds counts, two
|
||||||
|
adjacent text nodes, an empty `attrs`, `content` or `marks` and `-0` included, with no
|
||||||
|
normalization on either side.
|
||||||
CommonMark's spelling stays wherever a document holds none of those shapes. The plain reader
|
CommonMark's spelling stays wherever a document holds none of those shapes. The plain reader
|
||||||
builds what is written, as `markdownToAdf` does. Only the plain writer is lossy: its reduction
|
builds what is written, as `markdownToAdf` does. Only the plain writer is lossy: its reduction
|
||||||
reads and writes editor-normal ADF — adjacent text nodes of identical marks and no attributes
|
reads and writes editor-normal ADF — adjacent text nodes of identical marks and no attributes
|
||||||
|
|||||||
+15
-14
@@ -4,10 +4,11 @@ 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 (The opaque carry says how to keep it code) — and one gap: a CommonMark
|
string opens `adf:` is the opaque carry (wrap it as a bare fence in
|
||||||
image fits only as its own title-less paragraph — mid-text and titled images are named errors. The
|
`!adf:codeBlock {language="adf:…"}` to keep it code) — and one gap: a CommonMark image fits only as
|
||||||
emitted form is contract (`docs/decisions.md` §The formats are API). Per-node syntaxes build on this
|
its own title-less paragraph — mid-text and titled images are named errors. The emitted form is
|
||||||
grammar in the sections below.
|
contract (`docs/decisions.md` §The formats are API). Per-node syntaxes build on this grammar in the
|
||||||
|
sections below.
|
||||||
|
|
||||||
## Canonical form
|
## Canonical form
|
||||||
|
|
||||||
@@ -446,8 +447,8 @@ Attributes and the carry fallback read as in the block sections, the carry in it
|
|||||||
the nodes below, `emoji`, `mention` and `status` spell their `text` attribute in the content slot as
|
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
|
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 is a named
|
parsing to anything but one text node carrying neither marks, attributes nor content 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,
|
error, and so is a `text` key in `{attrs}`. An enclosing mark spelling does not reach into the slot.
|
||||||
`!adf:text` included; content on a node that takes none is a named error.
|
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).
|
- `date` — Attributes: `localId` (string), `timestamp` (string, epoch milliseconds).
|
||||||
- `emoji` — Attributes: `id` (string), `localId` (string), `shortName` (string, `:name:`), `text`
|
- `emoji` — Attributes: `id` (string), `localId` (string), `shortName` (string, `:name:`), `text`
|
||||||
@@ -527,14 +528,14 @@ that depth: `attrs: {}` differs from no `attrs`, and a directive spells it `{att
|
|||||||
breaks at every node the emitter carries, so no emitted carry sits inside a mark spelling.
|
breaks at every node the emitter carries, so no emitted carry sits inside a mark spelling.
|
||||||
|
|
||||||
An inline node whose marks no nesting spells — a mark type not listed here, an attrs key its
|
An inline node whose marks no nesting spells — a mark type not listed here, an attrs key its
|
||||||
spelling does not list, a value that is not the spelling's type, an attribute the spelling needs
|
spelling does not list, a value that is not the spelling's type, an attribute the spelling needs and
|
||||||
and the mark lacks, an empty `attrs` on a mark CommonMark spells, an order putting a code span outside another mark, `code` over anything but a
|
the mark lacks, an empty `attrs` on a mark CommonMark spells, an order putting a code span outside
|
||||||
text node or over text holding a newline, or a spelling CommonMark's flanking rules cannot open or
|
another mark, `code` over anything but a text node or over text holding a newline, or a spelling
|
||||||
close where the run sits (`un**-real**istic`), or one CommonMark's matching pairs elsewhere — the
|
CommonMark's flanking rules cannot open or close where the run sits (`un**-real**istic`), or one
|
||||||
intra-word `*` runs together with a neighbouring `**`, and the multiple-of-3 rule can leave the
|
CommonMark's matching pairs elsewhere — the intra-word `*` runs together with a neighbouring `**`,
|
||||||
merged run's pairing to another delimiter — rides the inline carry whole. An opaque carry inside a
|
and the multiple-of-3 rule can leave the merged run's pairing to another delimiter — rides the
|
||||||
mark spelling is a named error in input: the carry restores its node exactly, marks included
|
inline carry whole. An opaque carry inside a mark spelling is a named error in input: the carry
|
||||||
(`docs/decisions.md` §Unknown nodes ride the carry).
|
restores its node exactly, marks included (`docs/decisions.md` §Unknown nodes ride the carry).
|
||||||
|
|
||||||
```
|
```
|
||||||
!adf:textColor[**Overdue**]{color="#ae2e24"}, H!adf:subsup[2]{type=sub}O, !adf:underline[signed].
|
!adf:textColor[**Overdue**]{color="#ae2e24"}, H!adf:subsup[2]{type=sub}O, !adf:underline[signed].
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const argumentByType = new Map(
|
|||||||
export const documentName = 'doc'
|
export const documentName = 'doc'
|
||||||
|
|
||||||
// spec/flavour.md, Directives: a document holding no content key, which the empty string cannot spell.
|
// spec/flavour.md, Directives: a document holding no content key, which the empty string cannot spell.
|
||||||
export const documentAttribute = { key: 'content', value: 'none' } as const
|
export const documentAttribute = { key: 'content', value: 'none' }
|
||||||
|
|
||||||
export const documentSpelling = spellDirectiveOpener(documentName, undefined, spellAttributes([[documentAttribute.key, documentAttribute.value]]))
|
export const documentSpelling = spellDirectiveOpener(documentName, undefined, spellAttributes([[documentAttribute.key, documentAttribute.value]]))
|
||||||
|
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export function carryFenceType(info: string): string | undefined {
|
|||||||
|
|
||||||
export function readCarriedBlock(type: string, body: string, depth: number): Read<AdfNode> {
|
export function readCarriedBlock(type: string, body: string, depth: number): Read<AdfNode> {
|
||||||
if (type !== '' && !infoStringCarries(type)) {
|
if (type !== '' && !infoStringCarries(type)) {
|
||||||
return { fault: unsupportedNodeShape(`the carry fence names a type no info string carries back: spell it ${carryFencePrefix} with the type in the JSON`) }
|
return { fault: unsupportedNodeShape(`the carry fence names a type no info string carries back: spell it ${carryFencePrefix} with the type in the JSON; ${fenceEscape(type)}`) }
|
||||||
}
|
}
|
||||||
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
|
||||||
@@ -89,7 +89,7 @@ function fenceEscape(type: string): string {
|
|||||||
|
|
||||||
function typedValue(value: JsonValue, type: string): Read<JsonValue> {
|
function typedValue(value: JsonValue, type: string): Read<JsonValue> {
|
||||||
if (value === null || typeof value !== 'object' || Array.isArray(value)) return { value }
|
if (value === null || typeof value !== 'object' || Array.isArray(value)) return { value }
|
||||||
if ('type' in value) return { fault: unsupportedNodeShape(`the ${carryFencePrefix}${type} fence names its node's type: this JSON holds a type as well`) }
|
if ('type' in value) return { fault: unsupportedNodeShape(`the ${carryFencePrefix}${type} fence names its node's type: this JSON holds a type as well; ${fenceEscape(type)}`) }
|
||||||
return { value: { ...value, type } }
|
return { value: { ...value, type } }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ function partText(items: readonly Inline[], scan: Scan): Result<AdfNode[]> {
|
|||||||
const previous = items[index - 1]
|
const previous = items[index - 1]
|
||||||
const next = items[index + 1]
|
const next = items[index + 1]
|
||||||
if (previous === undefined || next === undefined || !isNode(previous) || !isNode(next) || !readsAsOne(previous, next)) {
|
if (previous === undefined || next === undefined || !isNode(previous) || !isNode(next) || !readsAsOne(previous, next)) {
|
||||||
return failure('unsupported-node-shape', `${textBreakName} parts two text nodes CommonMark reads back as one: this one parts something else`, scan.path)
|
return failure('unsupported-node-shape', `delete ${textBreakSpelling} here: it stands only between two runs of text with the same formatting, which would otherwise read as one`, scan.path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return success(parted)
|
return success(parted)
|
||||||
|
|||||||
@@ -92,9 +92,12 @@ test('builds an empty document from input holding no block, and one holding no c
|
|||||||
const form = 'unsupported-node-shape: doc spells the one form !adf:doc {content=none}: this one spells another'
|
const form = 'unsupported-node-shape: doc spells the one form !adf:doc {content=none}: this one spells another'
|
||||||
assert.equal(content(markdownToAdf('!adf:doc\n')), form)
|
assert.equal(content(markdownToAdf('!adf:doc\n')), form)
|
||||||
assert.equal(content(markdownToAdf('!adf:doc x {content=none}\n')), form)
|
assert.equal(content(markdownToAdf('!adf:doc x {content=none}\n')), form)
|
||||||
assert.equal(content(markdownToAdf('!adf:doc {content=empty}\n')), form)
|
assert.equal(
|
||||||
|
content(markdownToAdf('!adf:doc {content=empty}\n')),
|
||||||
|
'unsupported-node-shape: an empty document is empty markdown, and !adf:doc {content=none} spells a document holding no content key: this one spells content=empty',
|
||||||
|
)
|
||||||
assert.equal(content(markdownToAdf('!adf:doc {content="none"}\n')), form)
|
assert.equal(content(markdownToAdf('!adf:doc {content="none"}\n')), form)
|
||||||
const alone = 'unsupported-node-shape: !adf:doc {content=none} spells a whole document holding no content key, alone: this one stands among other blocks'
|
const alone = 'unsupported-node-shape: delete the !adf:doc {content=none} line to give the document content: it stands only as the whole document'
|
||||||
assert.equal(content(markdownToAdf('x\n\n!adf:doc {content=none}\n')), alone)
|
assert.equal(content(markdownToAdf('x\n\n!adf:doc {content=none}\n')), alone)
|
||||||
assert.equal(content(markdownToAdf('!adf:panel info\n!adf:doc {content=none}\n!adf:/panel\n')), alone)
|
assert.equal(content(markdownToAdf('!adf:panel info\n!adf:doc {content=none}\n!adf:/panel\n')), alone)
|
||||||
assert.equal(content(markdownToAdf('!adf:doc\n!adf:/doc\n')), 'malformed-directive: doc takes no body, so no !adf:/doc closes it; \\!adf: keeps the prefix literal')
|
assert.equal(content(markdownToAdf('!adf:doc\n!adf:/doc\n')), 'malformed-directive: doc takes no body, so no !adf:/doc closes it; \\!adf: keeps the prefix literal')
|
||||||
@@ -371,11 +374,11 @@ test('reads the carry fence back to the node its info string names and its JSON
|
|||||||
assert.deepEqual(content(markdownToAdf('```adf:\n{\n "type": "a`b"\n}\n```\n')), [{ type: 'a`b' }])
|
assert.deepEqual(content(markdownToAdf('```adf:\n{\n "type": "a`b"\n}\n```\n')), [{ type: 'a`b' }])
|
||||||
assert.equal(
|
assert.equal(
|
||||||
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",
|
'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 carry fence names a type its info string carries: spell it adf:blockCard')
|
||||||
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'
|
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)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1087,7 +1090,7 @@ test('names the directive mark left without the content it wraps', () => {
|
|||||||
test('reads the text break only between two text nodes CommonMark joins, building no node', () => {
|
test('reads the text break only between two text nodes CommonMark joins, building no node', () => {
|
||||||
assert.deepEqual(content(markdownToAdf('a!adf:textBreak{}b\n')), [{ content: [text('a'), text('b')], type: 'paragraph' }])
|
assert.deepEqual(content(markdownToAdf('a!adf:textBreak{}b\n')), [{ content: [text('a'), text('b')], type: 'paragraph' }])
|
||||||
assert.deepEqual(content(markdownToAdf('==a!adf:textBreak{}b==\n')), [{ content: [text('==a'), text('b==')], type: 'paragraph' }])
|
assert.deepEqual(content(markdownToAdf('==a!adf:textBreak{}b==\n')), [{ content: [text('==a'), text('b==')], type: 'paragraph' }])
|
||||||
const parts = 'unsupported-node-shape: textBreak parts two text nodes CommonMark reads back as one: this one parts something else'
|
const parts = 'unsupported-node-shape: delete !adf:textBreak{} here: it stands only between two runs of text with the same formatting, which would otherwise read as one'
|
||||||
const carried = '!adf:carry{json="{\\"text\\":\\"b\\",\\"type\\":\\"text\\"}"}'
|
const carried = '!adf:carry{json="{\\"text\\":\\"b\\",\\"type\\":\\"text\\"}"}'
|
||||||
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)
|
||||||
@@ -1097,3 +1100,14 @@ test('reads the text break only between two text nodes CommonMark joins, buildin
|
|||||||
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' }])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('leads a refusal ordinary editing meets with the edit that fixes it', () => {
|
||||||
|
assert.equal(
|
||||||
|
content(markdownToAdf('!adf:paragraph {content=empty}\nText.\n!adf:/paragraph\n')),
|
||||||
|
'unsupported-node-shape: remove content=empty to give the paragraph a body: content=empty holds none, and this one holds one',
|
||||||
|
)
|
||||||
|
assert.equal(
|
||||||
|
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',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|||||||
@@ -8,16 +8,17 @@ import type { LinkDefinitions } from './inline-content.ts'
|
|||||||
import { carryFenceType, readCarriedBlock } from '../opaque-carry.ts'
|
import { carryFenceType, readCarriedBlock } from '../opaque-carry.ts'
|
||||||
import { commonMarkSpelling, type SpellingMemo } from '../emit/adf-to-markdown.ts'
|
import { commonMarkSpelling, type SpellingMemo } from '../emit/adf-to-markdown.ts'
|
||||||
import { documentAttribute, documentName, documentSpelling, listBreakName, listBreakSpelling } from '../block-directive.ts'
|
import { documentAttribute, documentName, documentSpelling, listBreakName, listBreakSpelling } from '../block-directive.ts'
|
||||||
|
import { emptyKeys, nodeAttrs, nodeContent } from '../../adf/document.ts'
|
||||||
import { failure, faulted, positioned, success, type ConvertErrorPath, type ParseError, type Result, type SourcePosition } from '../../result.ts'
|
import { failure, faulted, positioned, success, type ConvertErrorPath, type ParseError, type Result, type SourcePosition } from '../../result.ts'
|
||||||
import { inlineLeaves } from '../emit/plain-inline.ts'
|
import { inlineLeaves } from '../emit/plain-inline.ts'
|
||||||
import { languageSlot } from '../code-language.ts'
|
import { languageSlot } from '../code-language.ts'
|
||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
import { leadingMarker, readAlertMarker, readTaskMarker } from '../plain-conventions.ts'
|
import { leadingMarker, readAlertMarker, readTaskMarker } from '../plain-conventions.ts'
|
||||||
import { mintTaskIds } from './task-ids.ts'
|
import { mintTaskIds } from './task-ids.ts'
|
||||||
import { nodeAttrs, nodeContent } from '../../adf/document.ts'
|
|
||||||
import { parseBlocks } from './blocks.ts'
|
import { parseBlocks } from './blocks.ts'
|
||||||
import { parseInlineContent } from './inline-content.ts'
|
import { parseInlineContent } from './inline-content.ts'
|
||||||
import { readBlockDirectiveNode } from './directive-nodes.ts'
|
import { readBlockDirectiveNode } from './directive-nodes.ts'
|
||||||
|
import { spellsEmpty } from '../empty-keys.ts'
|
||||||
import { unsupportedNodeShape } from '../directive-syntax.ts'
|
import { unsupportedNodeShape } from '../directive-syntax.ts'
|
||||||
|
|
||||||
type Paragraph = Extract<Block, { kind: 'paragraph' }>
|
type Paragraph = Extract<Block, { kind: 'paragraph' }>
|
||||||
@@ -58,7 +59,7 @@ function readBlocks(blocks: readonly Block[], reading: Reading, path: ConvertErr
|
|||||||
for (const [index, block] of blocks.entries()) {
|
for (const [index, block] of blocks.entries()) {
|
||||||
const nodePath = [...path, 'content', content.length]
|
const nodePath = [...path, 'content', content.length]
|
||||||
if (block.kind === 'directive' && block.name === documentName) {
|
if (block.kind === 'directive' && block.name === documentName) {
|
||||||
const fault = documentFault(block) ?? unsupportedNodeShape(`${documentSpelling} spells a whole document holding no content key, alone: this one stands among other blocks`)
|
const fault = documentFault(block) ?? unsupportedNodeShape(`delete the ${documentSpelling} line to give the document content: it stands only as the whole document`)
|
||||||
return positioned(faulted(fault, nodePath), block.position)
|
return positioned(faulted(fault, nodePath), block.position)
|
||||||
}
|
}
|
||||||
if (block.kind === 'directive' && block.name === listBreakName) {
|
if (block.kind === 'directive' && block.name === listBreakName) {
|
||||||
@@ -85,6 +86,9 @@ function listBreakFault(block: DirectiveBlock, previous: Block | undefined, next
|
|||||||
function documentFault(block: DirectiveBlock): ConvertFault | undefined {
|
function documentFault(block: DirectiveBlock): ConvertFault | undefined {
|
||||||
const spelled = block.attributes.get(documentAttribute.key)
|
const spelled = block.attributes.get(documentAttribute.key)
|
||||||
if (block.argument === undefined && block.attributes.size === 1 && spelled?.spelling === documentAttribute.value) return undefined
|
if (block.argument === undefined && block.attributes.size === 1 && spelled?.spelling === documentAttribute.value) return undefined
|
||||||
|
if (block.argument === undefined && block.attributes.size === 1 && spellsEmpty(spelled)) {
|
||||||
|
return unsupportedNodeShape(`an empty document is empty markdown, and ${documentSpelling} spells a document holding no content key: this one spells ${documentAttribute.key}=empty`)
|
||||||
|
}
|
||||||
return unsupportedNodeShape(`${documentName} spells the one form ${documentSpelling}: this one spells another`)
|
return unsupportedNodeShape(`${documentName} spells the one form ${documentSpelling}: this one spells another`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,7 +236,7 @@ function directiveBody(read: BlockDirectiveNode, blocks: Block[] | undefined, re
|
|||||||
const { contentModel, node } = read
|
const { contentModel, node } = read
|
||||||
if (blocks === undefined) return success(node)
|
if (blocks === undefined) return success(node)
|
||||||
// A directive builds a content key only from content=empty, which holds no body.
|
// A directive builds a content key only from content=empty, which holds no body.
|
||||||
if (node.content !== undefined) return blocks.length === 0 ? success(node) : failure('unsupported-node-shape', `${node.type} spells content=empty, which holds no body: this one holds one`, path)
|
if (node.content !== undefined) return blocks.length === 0 ? success(node) : failure('unsupported-node-shape', `remove content=empty to give the ${node.type} a body: content=empty holds none, and this one holds one`, path)
|
||||||
if (contentModel === 'code') return codeDirectiveNode(node, blocks, path)
|
if (contentModel === 'code') return codeDirectiveNode(node, blocks, path)
|
||||||
if (contentModel === 'inline') return inlineBodyNode(node, blocks, reading, path)
|
if (contentModel === 'inline') return inlineBodyNode(node, blocks, reading, path)
|
||||||
return containerNode(node, blocks, reading, path, depth)
|
return containerNode(node, blocks, reading, path, depth)
|
||||||
@@ -248,13 +252,14 @@ function codeDirectiveNode(node: AdfNode, blocks: readonly Block[], path: Conver
|
|||||||
const [first] = fences
|
const [first] = fences
|
||||||
if (first === undefined) return failure('unsupported-node-shape', `${node.type} takes code blocks as its body: this body holds none`, path)
|
if (first === undefined) return failure('unsupported-node-shape', `${node.type} takes code blocks as its body: this body holds none`, path)
|
||||||
if (fences.some((fence) => fence.language !== first.language)) return failure('unsupported-node-shape', `${node.type} holds one language, so its fences carry one info string: these differ`, path)
|
if (fences.some((fence) => fence.language !== first.language)) return failure('unsupported-node-shape', `${node.type} holds one language, so its fences carry one info string: these differ`, path)
|
||||||
if (fences.length > 1 && fences.some((fence) => fence.text === '')) return failure('unsupported-node-shape', `a fence beside another spells a text node, which holds text: this one is empty`, path)
|
if (fences.length > 1 && fences.some((fence) => fence.text === '')) return failure('unsupported-node-shape', `delete the empty fence: a fence beside another holds code, and this one holds none`, path)
|
||||||
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 ((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)
|
||||||
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' }))))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,10 +30,6 @@ function read(markdown: string): readonly AdfNode[] | string {
|
|||||||
return blocks
|
return blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
function blocks(...content: AdfNode[]): readonly AdfNode[] {
|
|
||||||
return content
|
|
||||||
}
|
|
||||||
|
|
||||||
function document(...content: AdfNode[]): AdfDocument {
|
function document(...content: AdfNode[]): AdfDocument {
|
||||||
return { content, type: 'doc', version: 1 }
|
return { content, type: 'doc', version: 1 }
|
||||||
}
|
}
|
||||||
@@ -100,7 +96,7 @@ test('reads the rest of an alert marker line as the panel first body paragraph,
|
|||||||
assert.deepEqual(read('> [!tip] Title\n> body\n'), [panel('tip', said('Title'), said('body'))])
|
assert.deepEqual(read('> [!tip] Title\n> body\n'), [panel('tip', said('Title'), said('body'))])
|
||||||
assert.deepEqual(read('> [!tip] Title\\\n> body\n'), [panel('tip', said('Title'), said('body'))])
|
assert.deepEqual(read('> [!tip] Title\\\n> body\n'), [panel('tip', said('Title'), said('body'))])
|
||||||
assert.deepEqual(read('> [!NOTE]\\\n> Broken.\n'), [panel('info', said('Broken.'))])
|
assert.deepEqual(read('> [!NOTE]\\\n> Broken.\n'), [panel('info', said('Broken.'))])
|
||||||
assert.deepEqual(read('> [!NOTE]\n'), blocks(panel('info', paragraph())))
|
assert.deepEqual(read('> [!NOTE]\n'), [panel('info', paragraph())])
|
||||||
assert.deepEqual(read('> > [!WARNING]\n> > Inner.\n'), [bare('blockquote', panel('warning', said('Inner.')))])
|
assert.deepEqual(read('> > [!WARNING]\n> > Inner.\n'), [bare('blockquote', panel('warning', said('Inner.')))])
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -119,27 +115,27 @@ test('reads a folded callout to an expand titled by the rest of its marker line,
|
|||||||
node('expand', { title: 'Why?' }, paragraph(text('See '), text('the docs', link), text(', '), text('now', strong), text('.'))),
|
node('expand', { title: 'Why?' }, paragraph(text('See '), text('the docs', link), text(', '), text('now', strong), text('.'))),
|
||||||
])
|
])
|
||||||
assert.deepEqual(read('> [!NOTE]- Two\\\n> lines\n'), [node('expand', { title: 'Two' }, said('lines'))])
|
assert.deepEqual(read('> [!NOTE]- Two\\\n> lines\n'), [node('expand', { title: 'Two' }, said('lines'))])
|
||||||
assert.deepEqual(read('> [!faq]- See [x](http://y)\n'), blocks(node('expand', { title: 'See x (http://y)' }, paragraph())))
|
assert.deepEqual(read('> [!faq]- See [x](http://y)\n'), [node('expand', { title: 'See x (http://y)' }, paragraph())])
|
||||||
assert.deepEqual(read('> [!faq]- [a **b**](u "t")[c](u) and [d](v)\n'), blocks(node('expand', { title: 'a bc (u) and d (v)' }, paragraph())))
|
assert.deepEqual(read('> [!faq]- [a **b**](u "t")[c](u) and [d](v)\n'), [node('expand', { title: 'a bc (u) and d (v)' }, paragraph())])
|
||||||
assert.deepEqual(read('> [!faq]- <http://y> or <a@b.c> or <http://a\\b>\n'), blocks(node('expand', { title: 'http://y or a@b.c or http://a\\b' }, paragraph())))
|
assert.deepEqual(read('> [!faq]- <http://y> or <a@b.c> or <http://a\\b>\n'), [node('expand', { title: 'http://y or a@b.c or http://a\\b' }, paragraph())])
|
||||||
assert.deepEqual(read('> [!faq]- [a b](u)\n'), blocks(node('expand', { title: 'a\nb (u)' }, paragraph())))
|
assert.deepEqual(read('> [!faq]- [a b](u)\n'), [node('expand', { title: 'a\nb (u)' }, paragraph())])
|
||||||
assert.deepEqual(read('> [!faq]- [!adf:mention[@M]{id=5}](u) !adf:inlineCard{url="http://y"}\n'), blocks(node('expand', { title: '@M (u) http://y' }, paragraph())))
|
assert.deepEqual(read('> [!faq]- [!adf:mention[@M]{id=5}](u) !adf:inlineCard{url="http://y"}\n'), [node('expand', { title: '@M (u) http://y' }, paragraph())])
|
||||||
assert.deepEqual(read('> [!NOTE]- Set ==x== here\n'), blocks(node('expand', { title: 'Set ==x== here' }, paragraph())))
|
assert.deepEqual(read('> [!NOTE]- Set ==x== here\n'), [node('expand', { title: 'Set ==x== here' }, paragraph())])
|
||||||
assert.deepEqual(read('> [!NOTE]-\n>\n> Line.\n'), [bare('expand', said('Line.'))])
|
assert.deepEqual(read('> [!NOTE]-\n>\n> Line.\n'), [bare('expand', said('Line.'))])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('reads a folded callout inside an expand to a nested expand', () => {
|
test('reads a folded callout inside an expand to a nested expand', () => {
|
||||||
const markdown = '> [!NOTE]- Outer\n>\n> > [!NOTE]- Inner\n> >\n> > Deep.\n>\n> > [!TIP]\n> >\n> > > [!NOTE]-\n'
|
const markdown = '> [!NOTE]- Outer\n>\n> > [!NOTE]- Inner\n> >\n> > Deep.\n>\n> > [!TIP]\n> >\n> > > [!NOTE]-\n'
|
||||||
assert.deepEqual(read(markdown), blocks(node('expand', { title: 'Outer' }, node('nestedExpand', { title: 'Inner' }, said('Deep.')), panel('tip', bare('nestedExpand', paragraph())))))
|
assert.deepEqual(read(markdown), [node('expand', { title: 'Outer' }, node('nestedExpand', { title: 'Inner' }, said('Deep.')), panel('tip', bare('nestedExpand', paragraph())))])
|
||||||
assert.deepEqual(read('- > [!NOTE]-\n'), blocks(bare('bulletList', bare('listItem', bare('expand', paragraph())))))
|
assert.deepEqual(read('- > [!NOTE]-\n'), [bare('bulletList', bare('listItem', bare('expand', paragraph())))])
|
||||||
assert.deepEqual(read('!adf:expand\n> [!NOTE]- Inner\n!adf:/expand\n'), blocks(bare('expand', node('nestedExpand', { title: 'Inner' }, paragraph()))))
|
assert.deepEqual(read('!adf:expand\n> [!NOTE]- Inner\n!adf:/expand\n'), [bare('expand', node('nestedExpand', { title: 'Inner' }, paragraph()))])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('reads a bullet list whose every item leads with a task marker to a task list', () => {
|
test('reads a bullet list whose every item leads with a task marker to a task list', () => {
|
||||||
assert.deepEqual(read('- [x] Write the spec\n- [ ] Ship **it**\n- [X] Tell\n'), [
|
assert.deepEqual(read('- [x] Write the spec\n- [ ] Ship **it**\n- [X] Tell\n'), [
|
||||||
bare('taskList', task('DONE', text('Write the spec')), task('TODO', text('Ship '), text('it', strong)), task('DONE', text('Tell'))),
|
bare('taskList', task('DONE', text('Write the spec')), task('TODO', text('Ship '), text('it', strong)), task('DONE', text('Tell'))),
|
||||||
])
|
])
|
||||||
assert.deepEqual(read('- [x]\n- [ ]\\\n after\n'), blocks(bare('taskList', task('DONE'), task('TODO', text('after')))))
|
assert.deepEqual(read('- [x]\n- [ ]\\\n after\n'), [bare('taskList', task('DONE'), task('TODO', text('after')))])
|
||||||
const minted = plainMarkdownToAdf('- [x] Parent\n - [ ] Child\n')
|
const minted = plainMarkdownToAdf('- [x] Parent\n - [ ] Child\n')
|
||||||
assert.deepEqual(minted.ok ? minted.value.content : minted.error.code, [
|
assert.deepEqual(minted.ok ? minted.value.content : minted.error.code, [
|
||||||
node(
|
node(
|
||||||
@@ -201,7 +197,7 @@ test('reads what the reduction wrote back to the node it reduced, less the attri
|
|||||||
assert.deepEqual(roundTripped(node('panel', { localId, panelType }, said('Check.'))), [panel(panelType, said('Check.'))], panelType)
|
assert.deepEqual(roundTripped(node('panel', { localId, panelType }, said('Check.'))), [panel(panelType, said('Check.'))], panelType)
|
||||||
}
|
}
|
||||||
const expand = node('expand', { localId, title: 'Log' }, said('Line.'), node('nestedExpand', { title: 'Inner' }, said('Deep.')))
|
const expand = node('expand', { localId, title: 'Log' }, said('Line.'), node('nestedExpand', { title: 'Inner' }, said('Deep.')))
|
||||||
assert.deepEqual(roundTripped(node('panel', { panelType: 'tip' }, paragraph()), node('expand', { title: 'Empty' }, paragraph())), blocks(panel('tip', paragraph()), node('expand', { title: 'Empty' }, paragraph())))
|
assert.deepEqual(roundTripped(node('panel', { panelType: 'tip' }, paragraph()), node('expand', { title: 'Empty' }, paragraph())), [panel('tip', paragraph()), node('expand', { title: 'Empty' }, paragraph())])
|
||||||
assert.deepEqual(roundTripped(expand), [node('expand', { title: 'Log' }, said('Line.'), node('nestedExpand', { title: 'Inner' }, said('Deep.')))])
|
assert.deepEqual(roundTripped(expand), [node('expand', { title: 'Log' }, said('Line.'), node('nestedExpand', { title: 'Inner' }, said('Deep.')))])
|
||||||
const tasks = bare(
|
const tasks = bare(
|
||||||
'taskList',
|
'taskList',
|
||||||
@@ -268,5 +264,5 @@ test('keeps what markdownToAdf reads that no row reads, and refuses only what it
|
|||||||
|
|
||||||
test('joins a carried text node to no neighbour, inside a highlight too', () => {
|
test('joins a carried text node to no neighbour, inside a highlight too', () => {
|
||||||
const carried = '!adf:carry{json="{\\"marks\\":[],\\"text\\":\\"b\\",\\"type\\":\\"text\\"}"}'
|
const carried = '!adf:carry{json="{\\"marks\\":[],\\"text\\":\\"b\\",\\"type\\":\\"text\\"}"}'
|
||||||
assert.deepEqual(read(`==a${carried}==\n`), blocks(paragraph(text('a', highlight), { marks: [], text: 'b', type: 'text' })))
|
assert.deepEqual(read(`==a${carried}==\n`), [paragraph(text('a', highlight), { marks: [], text: 'b', type: 'text' })])
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user