mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Build a hardBreak carrying no marks where a mark spelling spans a line break, in every reader
This commit is contained in:
@@ -70,6 +70,10 @@
|
||||
Chrome) `markdownToAdf` could refuse markdown `adfToMarkdown` wrote, as `unsupported-node-shape`,
|
||||
once the process had read a key holding a backslash
|
||||
([V8 bug](https://issues.chromium.org/issues/521080746)).
|
||||
- **Breaking:** a hard break inside emphasis, strike, a link or a directive mark reads holding no
|
||||
marks, as Atlassian's schema requires, where every reader gave it the marks around it.
|
||||
`adfToLosslessMarkdown` writes a hard break holding marks as `!adf:carry{json="…"}`. See
|
||||
`MIGRATION.md`.
|
||||
- Fix a cyclic document, in which an object holds itself: `adfToLosslessMarkdown` and
|
||||
`adfToPortableMarkdown` refuse it as `not-an-adf-document` and `isAdfDocument` returns `false`,
|
||||
where all three hung.
|
||||
|
||||
@@ -66,6 +66,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 |
|
||||
| markdown holding no block (`""`) | `{ type: 'doc', version: 1 }` | `{ content: [], type: 'doc', version: 1 }`; `!adf:doc {content=none}` reads as the former |
|
||||
| a hard break inside a mark's spelling (`*a\` then `b*` on the next line) | the hard break holds the mark | the hard break holds no marks, as Atlassian's schema requires; converting per the recipe above keeps a stored one's marks in the inline carry |
|
||||
| a link whose text is empty (`[](/url)`, `[]()`) | literal text | `/url` linked to `/url`, and nothing where the destination is empty too |
|
||||
| a code fence whose info string opens `adf:` (```` ```adf:x ````) | a `codeBlock` with that language | in `losslessMarkdownToAdf`, 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 |
|
||||
|
||||
|
||||
@@ -463,16 +463,16 @@
|
||||
},
|
||||
{
|
||||
"check": "count",
|
||||
"divergence": "em 1/3",
|
||||
"divergence": "em 1/2",
|
||||
"example": 638,
|
||||
"kind": "pending",
|
||||
"reason": "The hard break carries the em mark, which ADF's schema forbids; todo.md item 87 strips it."
|
||||
"kind": "mark-model",
|
||||
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
|
||||
},
|
||||
{
|
||||
"check": "count",
|
||||
"divergence": "em 1/3",
|
||||
"divergence": "em 1/2",
|
||||
"example": 639,
|
||||
"kind": "pending",
|
||||
"reason": "The hard break carries the em mark, which ADF's schema forbids; todo.md item 87 strips it."
|
||||
"kind": "mark-model",
|
||||
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
|
||||
}
|
||||
]
|
||||
|
||||
+18
-17
@@ -533,24 +533,25 @@ destination is empty too, `[]()`, as nothing.
|
||||
- `textColor` — Attributes: `color` (string, `#rrggbb`).
|
||||
- `underline` — Attributes: none.
|
||||
|
||||
A spelling adds its mark to every inline node it wraps, and nesting is the marks array in order,
|
||||
outermost first: `_!adf:underline[x]_` gives marks `[em, underline]`, `!adf:underline[_x_]` the
|
||||
reverse. `adfToLosslessMarkdown` nests in the order the array holds rather than sorting it —
|
||||
`docs/decisions.md` §Equality is deep restores the array, not a set — and opens each spelling once
|
||||
over the longest run of adjacent inline nodes carrying an identical mark, attributes included, at
|
||||
that depth: `attrs: {}` differs from no `attrs`, and a directive spells it `{attrs=empty}`. A run
|
||||
breaks at every node the emitter carries, so no emitted carry sits inside a mark spelling.
|
||||
A spelling adds its mark to every inline node it wraps but a hard break, which Atlassian's schema
|
||||
gives no marks, and nesting is the marks array in order, outermost first: `_!adf:underline[x]_`
|
||||
gives marks `[em, underline]`, `!adf:underline[_x_]` the reverse. `adfToLosslessMarkdown` nests in
|
||||
the order the array holds rather than sorting it — `docs/decisions.md` §Equality is deep restores
|
||||
the array, not a set — and opens each spelling once over the longest run of adjacent inline nodes
|
||||
carrying an identical mark, attributes included, at that depth: `attrs: {}` differs from no `attrs`,
|
||||
and a directive spells it `{attrs=empty}`. A run 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
|
||||
spelling does not list, a value that is not the spelling's type, an attribute the spelling needs 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 text node holding text, or over text holding a line ending
|
||||
or a null character, or a spelling CommonMark's flanking rules cannot open or close where the run
|
||||
sits (`un**-real**istic`), or one CommonMark's matching pairs elsewhere — the intra-word `*` runs
|
||||
together with a neighbouring `**`, and the multiple-of-3 rule can leave the merged run's pairing to
|
||||
another delimiter — rides the inline carry whole. An opaque carry inside a mark spelling is a named
|
||||
error in input: the carry restores its node exactly, marks included (`docs/decisions.md` §Unknown
|
||||
nodes ride the carry).
|
||||
An inline node whose marks no nesting spells — a hard break holding any mark, 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 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 text node holding text, or over
|
||||
text holding a line ending or a null character, or a spelling CommonMark's flanking rules cannot
|
||||
open or close where the run sits (`un**-real**istic`), or one CommonMark's matching pairs elsewhere
|
||||
— the intra-word `*` runs together with a neighbouring `**`, and the multiple-of-3 rule can leave
|
||||
the merged run's pairing to another delimiter — rides the inline carry whole. An opaque carry inside
|
||||
a mark spelling is a named error in input: 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].
|
||||
|
||||
@@ -488,10 +488,6 @@ test('carries a text node holding no text, or holding content', () => {
|
||||
|
||||
test('carries a mark run whose edge holds whitespace CommonMark flanking counts', () => {
|
||||
const em = { type: 'em' }
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ marks: [em], text: 'a', type: 'text' }, { marks: [em], type: 'hardBreak' }, { text: 'b', type: 'text' })))),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\"a\\",\\"type\\":\\"text\\"}"}!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"type\\":\\"hardBreak\\"}"}b\n',
|
||||
)
|
||||
assert.equal(
|
||||
markdown(adfToLosslessMarkdown(document(paragraph({ marks: [em], text: '\u00a0a', type: 'text' })))),
|
||||
'!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"em\\"}],\\"text\\":\\"\u00a0a\\",\\"type\\":\\"text\\"}"}\n',
|
||||
@@ -714,15 +710,14 @@ test('spells the directive marks around the longest run they cover', () => {
|
||||
assert.equal(emitted(marked('x', { attrs: { color: '#091e42', size: 2 }, type: 'border' })), '!adf:border[x]{color="#091e42" size=2}\n')
|
||||
assert.equal(emitted(marked('x', { type: 'em' }, underline)), '_!adf:underline[x]_\n')
|
||||
assert.equal(emitted(marked('x', underline, { type: 'em' })), '!adf:underline[_x_]\n')
|
||||
assert.equal(emitted(marked('a', underline), { marks: [underline], type: 'hardBreak' }, marked('b', underline)), '!adf:underline[a!adf:hardBreak{}b]\n')
|
||||
assert.equal(emitted(marked('a', underline), { type: 'hardBreak' }, marked('b', underline)), '!adf:underline[a]\\\n!adf:underline[b]\n')
|
||||
})
|
||||
|
||||
test('carries a hard break holding marks, which Atlassian\'s schema gives it none of', () => {
|
||||
const link: AdfMark = { attrs: { href: 'https://example.com/' }, type: 'link' }
|
||||
assert.equal(
|
||||
emitted(marked('a', underline, link), { marks: [underline, link], type: 'hardBreak' }, marked('b', underline, link)),
|
||||
'!adf:underline[[a!adf:hardBreak{}b](https://example.com/)]\n',
|
||||
)
|
||||
assert.equal(
|
||||
emitted(marked('a', link), { marks: [link], type: 'hardBreak' }, marked('b', link)),
|
||||
'[a\\\nb](https://example.com/)\n',
|
||||
readsBack(document(paragraph({ marks: [link], text: 'a', type: 'text' }, { marks: [link], type: 'hardBreak' }, { marks: [link], text: 'b', type: 'text' }))),
|
||||
'[a](https://example.com/)!adf:carry{json="{\\"marks\\":[{\\"attrs\\":{\\"href\\":\\"https://example.com/\\"},\\"type\\":\\"link\\"}],\\"type\\":\\"hardBreak\\"}"}[b](https://example.com/)\n',
|
||||
)
|
||||
})
|
||||
|
||||
|
||||
@@ -221,6 +221,7 @@ function nodePath(context: InlineContext, index: number): ConvertErrorPath {
|
||||
function carries(node: AdfNode, carried: ReadonlySet<number>, index: number): boolean {
|
||||
if (carried.has(index)) return true
|
||||
if (node.type === 'text') return !isSpellableText(node) || typeof node.text !== 'string' || node.text === ''
|
||||
if (node.type === 'hardBreak') return nodeMarks(node).length > 0
|
||||
const model = inlineNodeModel(node.type)
|
||||
if (model === undefined) return true
|
||||
const slot = model.textAttribute === undefined ? undefined : nodeAttrs(node)[model.textAttribute]
|
||||
|
||||
@@ -669,10 +669,10 @@ function markType(character: string, used: number): string {
|
||||
return used === 2 ? 'strong' : 'em'
|
||||
}
|
||||
|
||||
// `*(*a*)*` builds one `em`: a node holds at most one mark of each type.
|
||||
// `*(*a*)*` builds one `em`: a node holds at most one mark of each type. Atlassian's schema gives a hard break no marks.
|
||||
function applyMark(nodes: readonly Inline[], mark: AdfMark): Inline[] {
|
||||
return nodes.map((node) => {
|
||||
if (!isNode(node)) return node
|
||||
if (!isNode(node) || node.type === 'hardBreak') return node
|
||||
const marks = nodeMarks(node)
|
||||
return marks.some((carried) => carried.type === mark.type) ? node : { ...node, marks: [mark, ...marks] }
|
||||
})
|
||||
|
||||
@@ -828,6 +828,18 @@ test('reads a hard break from a trailing backslash and from two trailing spaces
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('> One\\\n> two.\n')), [quote({ content: [text('One'), hardBreak(), text('two.')], type: 'paragraph' })])
|
||||
})
|
||||
|
||||
test('builds the hard break a mark spelling spans with no marks, in every reader', () => {
|
||||
const broken = (...marks: AdfMark[]): AdfNode[] => [{ content: [marked('a', ...marks), hardBreak(), marked('b', ...marks)], type: 'paragraph' }]
|
||||
for (const read of [commonMarkToAdf, losslessMarkdownToAdf, portableMarkdownToAdf]) {
|
||||
assert.deepEqual(content(read('*a \nb*\n')), broken(em))
|
||||
assert.deepEqual(content(read('**a\\\nb**\n')), broken(strong))
|
||||
assert.deepEqual(content(read('[a\\\nb](/u)\n')), broken(link('/u')))
|
||||
}
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('~~a\\\nb~~\n')), broken(strike))
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:underline[a!adf:hardBreak{}b]\n')), broken(underline))
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('_!adf:hardBreak{localId=x}_\n')), [{ content: [{ attrs: { localId: 'x' }, type: 'hardBreak' }], type: 'paragraph' }])
|
||||
})
|
||||
|
||||
test('decodes the fenced info string the block walk leaves raw', () => {
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('```java​script\nx\n```\n')), [
|
||||
{ attrs: { language: 'java\u200bscript' }, content: [text('x')], type: 'codeBlock' },
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
|
||||
| ID | Release | Exempt | Item | R | S | A | G | Goals | Score |
|
||||
|---|---|---|---|---|---|---|---|---|---|
|
||||
| 87 | 0.2.0 | defect | **Build a `hardBreak` carrying no marks where emphasis or a link spans a line break, in every reader.** | 4 | 3 | 6 | 7 | 4 | 16.6 |
|
||||
| 88 | 0.2.0 | defect | **Build a `blockTaskItem` holding only the paragraphs and extensions Atlassian's schema allows, in `portableMarkdownToAdf`.** | 4 | 4 | 6 | 7 | 4 | 16.4 |
|
||||
| 49 | 0.2.0 | | **Read a list whose bullet or ordered delimiter changes as two lists, in every reader, and retire `!adf:listBreak`.** | 4 | 5 | 5 | 8 | 4, 5 | 15.5 |
|
||||
| 61 | 0.2.0 | decision | **Have the emitter ask the inline reader how a line reads back, in place of `line-escaping.ts` predicting it.** | 6 | 8 | 3 | 8 | 1 | 14.0 |
|
||||
@@ -67,13 +66,6 @@
|
||||
|
||||
## Details
|
||||
|
||||
### 87. Build a `hardBreak` carrying no marks where emphasis or a link spans a line break, in every reader.
|
||||
|
||||
`*foo \nbar*` builds a `hardBreak` marked `em` (CommonMark examples 638 and 639), which Atlassian's
|
||||
schema refuses: its `hardBreak_node` holds no `marks` (`spec/adf-schema/full.json`). So
|
||||
`commonMarkToAdf` builds a document a site may reject from plain CommonMark. The suite's `em 1/3`
|
||||
`pending` exceptions for 638 and 639 then return to `mark-model` and `em 1/2`.
|
||||
|
||||
### 88. Build a `blockTaskItem` holding only the paragraphs and extensions Atlassian's schema allows, in `portableMarkdownToAdf`.
|
||||
|
||||
`- [x] done\n\n ` builds a `blockTaskItem` holding a `mediaSingle`, and `- [x] done\n\n > q`
|
||||
|
||||
Reference in New Issue
Block a user