From 018c8b1ea7a9871e416052b1faeb2cb8700d4e62 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 9 Oct 2026 11:04:30 +0200 Subject: [PATCH] Answer architecture round 1: the node table says a hard break takes no marks, and a link text of hard breaks alone reads as its destination --- CHANGELOG.md | 6 +- MIGRATION.md | 2 +- README.md | 6 +- corpus/normalization/hard-break-in-mark.json | 64 +++++++++++++++++++ corpus/normalization/hard-break-in-mark.md | 5 ++ .../inline-nodes/hard-break-marks.json | 37 +++++++++++ .../inline-nodes/hard-break-marks.md | 1 + docs/comprehension-history.md | 9 +++ docs/decisions.md | 7 +- spec/flavour.md | 2 +- src/adf/inline-nodes.ts | 14 ++-- src/conformance/adf-schema.test.ts | 15 +++-- src/markdown/emit/inline-line.ts | 7 +- src/markdown/parse/inline-content.ts | 18 ++++-- src/markdown/parse/markdown-to-adf.test.ts | 10 ++- todo.md | 16 ++++- 16 files changed, 185 insertions(+), 34 deletions(-) create mode 100644 corpus/normalization/hard-break-in-mark.json create mode 100644 corpus/normalization/hard-break-in-mark.md create mode 100644 corpus/round-trip/inline-nodes/hard-break-marks.json create mode 100644 corpus/round-trip/inline-nodes/hard-break-marks.md diff --git a/CHANGELOG.md b/CHANGELOG.md index ba9a2ac..c7aeecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -72,8 +72,10 @@ ([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`. + A link whose text is only hard breaks reads as an empty link text does, as its destination + linked, and `losslessMarkdownToAdf` refuses a directive mark wrapping only hard breaks as + `unsupported-node-shape`. `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. diff --git a/MIGRATION.md b/MIGRATION.md index c8f5197..9753706 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -66,7 +66,7 @@ Markdown the spelling table leaves alone, which `0.2.0` reads as a different doc | --- | --- | --- | | a link whose text already holds one (`[ab](/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 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 only a hard break reads as its destination linked | | 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 | diff --git a/README.md b/README.md index b539568..dc55948 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ emit refuses: | Code | Fires when | What you can do | | --- | --- | --- | | `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` | parsing, in `losslessMarkdownToAdf`: markdown spells a node with an attribute, value, argument or body its type does not take, or without one it needs, writes as a directive a node or mark the lossless flavour spells as CommonMark, or puts a reserved directive out of place: `!adf:textBreak{}` or `!adf:listBreak` parting nothing, `!adf:doc` anywhere but as the whole document. Emitting: a node carrying one mark type twice, a block or a non-text inline node holding `text`, or a leaf block or a non-text inline node holding `content` | fix what the message names; `spec/flavour.md` lists every type's attributes and body | +| `unsupported-node-shape` | parsing, in `losslessMarkdownToAdf`: markdown spells a node with an attribute, value, argument or body its type does not take, or without one it needs, writes as a directive a node or mark the lossless flavour spells as CommonMark, or puts a reserved directive out of place: `!adf:textBreak{}` or `!adf:listBreak` parting nothing, `!adf:doc` anywhere but as the whole document. Emitting: a node carrying one mark type twice, unless it rides the carry as a hard break holding marks or a type `spec/flavour.md` does not spell, a block or a non-text inline node holding `text`, or a leaf block or a non-text inline node holding `content` | fix what the message names; `spec/flavour.md` lists every type's attributes and body | ## The guarantees @@ -240,8 +240,8 @@ Serves Goals 1, 4 and 5. canonical spelling, which round-trips byte-identically. - Seven CommonMark spellings parse without an error and build a document the reference implementation renders differently. Four because ADF holds no node for what it renders: - - An empty link text reads as its destination, `[](/url)` as `/url` linked to `/url`, and - `[]()` as nothing. + - An empty link text, or one holding only hard breaks, reads as its destination, `[](/url)` as + `/url` linked to `/url`, and `[]()` as nothing. - An image reads as an image only alone in its paragraph. Anywhere else — beside other text, in a heading or a table cell, or in the paragraph a task marker opens — it reads as its alt text linked to its URL: `See ![the moon](moon.png).` reads as diff --git a/corpus/normalization/hard-break-in-mark.json b/corpus/normalization/hard-break-in-mark.json new file mode 100644 index 0000000..2fadef9 --- /dev/null +++ b/corpus/normalization/hard-break-in-mark.json @@ -0,0 +1,64 @@ +{ + "content": [ + { + "content": [ + { + "marks": [ + { + "type": "em" + } + ], + "text": "One", + "type": "text" + }, + { + "type": "hardBreak" + }, + { + "marks": [ + { + "type": "em" + } + ], + "text": "Two", + "type": "text" + } + ], + "type": "paragraph" + }, + { + "content": [ + { + "marks": [ + { + "attrs": { + "href": "/u" + }, + "type": "link" + } + ], + "text": "Three", + "type": "text" + }, + { + "type": "hardBreak" + }, + { + "marks": [ + { + "attrs": { + "href": "/u" + }, + "type": "link" + } + ], + "text": "Four", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/normalization/hard-break-in-mark.md b/corpus/normalization/hard-break-in-mark.md new file mode 100644 index 0000000..9325aa7 --- /dev/null +++ b/corpus/normalization/hard-break-in-mark.md @@ -0,0 +1,5 @@ +*One\ +Two* + +[Three +Four](/u) diff --git a/corpus/round-trip/inline-nodes/hard-break-marks.json b/corpus/round-trip/inline-nodes/hard-break-marks.json new file mode 100644 index 0000000..c3d0737 --- /dev/null +++ b/corpus/round-trip/inline-nodes/hard-break-marks.json @@ -0,0 +1,37 @@ +{ + "content": [ + { + "content": [ + { + "marks": [ + { + "type": "em" + } + ], + "text": "One", + "type": "text" + }, + { + "marks": [ + { + "type": "em" + } + ], + "type": "hardBreak" + }, + { + "marks": [ + { + "type": "em" + } + ], + "text": "Two", + "type": "text" + } + ], + "type": "paragraph" + } + ], + "type": "doc", + "version": 1 +} diff --git a/corpus/round-trip/inline-nodes/hard-break-marks.md b/corpus/round-trip/inline-nodes/hard-break-marks.md new file mode 100644 index 0000000..15d8208 --- /dev/null +++ b/corpus/round-trip/inline-nodes/hard-break-marks.md @@ -0,0 +1 @@ +_One_!adf:carry{json="{\"marks\":[{\"type\":\"em\"}],\"type\":\"hardBreak\"}"}_Two_ diff --git a/docs/comprehension-history.md b/docs/comprehension-history.md index 74874e5..3455ed0 100644 --- a/docs/comprehension-history.md +++ b/docs/comprehension-history.md @@ -124,3 +124,12 @@ Inherited architect, decided by: - `src/result.ts:1` `ConvertErrorCode`: one rule no longer enforced at five sites - `src/markdown/parse/inline-content.ts:26` `InlineContent`: costs more — `contentNode` drops `image` silently - `src/markdown/parse/inline-content.ts:302` `assemble`: costs more — three routes from an image piece to text + +## 2026-10-09T09:00:13Z, PR #22 at fe5c731, against 78655bc + +Ruling: same + +| Seat | Vote | +|---|---| +| Mid A | same | +| Inherited architect | same | diff --git a/docs/decisions.md b/docs/decisions.md index c210ac9..87a1460 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -205,14 +205,15 @@ where its reference implementation nests one `` in another. ## An image reads as an image only alone in its paragraph, else as its alt text linked; an empty link text reads as its destination 2026-10-04, the maintainer and a writer panel, 3 of 3; `[]()` 2026-10-09, by Goals 4 and 7, as the -maintainer's plan item asked. Goals 4, 5, 6 and 7. Valid while `mediaInline` carries no URL and ADF +maintainer's plan item asked; a link text of hard breaks alone 2026-10-09, by Goal 7. Goals 4, 5, 6 and 7. Valid while `mediaInline` carries no URL and ADF holds no empty text node. Every reader reads every CommonMark image and empty link, as `spec/flavour.md` §The CommonMark image and §Marks spell out. An image alone in its paragraph reads as the image, its title as the caption. Anywhere else it reads as its alt text linked to its URL, titled by its title. An empty alt text -reads as the URL, and an empty link text as its destination. `[]()` reads as nothing: it renders -nothing, so Goal 7 loses no content. A link alone in its paragraph whose whole text is one image +reads as the URL, and an empty link text as its destination; so does a link text holding only hard +breaks, which take no marks. `[]()` reads as nothing: it renders nothing, so Goal 7 loses no +content. A link alone in its paragraph whose whole text is one image reads as that image, with the link marking its `media`. Inside any other link, the alt text is plain text the link marks, and the image's URL and title drop: the panel chose this by Goal 6, which outranks Goal 7. diff --git a/spec/flavour.md b/spec/flavour.md index 933ceae..73bd968 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -522,7 +522,7 @@ reference definition. Every such spelling carries an `href`: a directive link Co spell is a named error, and so is one spelling none. No link wraps a link at any nesting, which is CommonMark's own rule: a `[text]` already holding one leaves the outer brackets literal text, and the directive form, open to no literal reading, is a named error. ADF holds no empty text node, so -a link whose text is empty reads as its destination linked, `[](/url)` as `/url`, and one whose +a link whose text is empty or holds only hard breaks reads as its destination linked, `[](/url)` as `/url`, and one whose destination is empty too, `[]()`, as nothing. - `border` — Attributes: `color` (string, `#rrggbb` or `#rrggbbaa`), `size` (number, 1–3). diff --git a/src/adf/inline-nodes.ts b/src/adf/inline-nodes.ts index 998ae87..7c6b57a 100644 --- a/src/adf/inline-nodes.ts +++ b/src/adf/inline-nodes.ts @@ -2,14 +2,15 @@ import type { AttributeVocabulary } from './attribute-vocabulary.ts' export type InlineNodeModel = { attributes: AttributeVocabulary + marks: boolean textAttribute?: string } export const inlineNodes: Readonly> = { - date: { attributes: { localId: 'string', timestamp: 'string' } }, - emoji: { attributes: { id: 'string', localId: 'string', shortName: 'string', text: 'string' }, textAttribute: 'text' }, - hardBreak: { attributes: { localId: 'string', text: 'string' } }, - inlineCard: { attributes: { data: 'json', localId: 'string', url: 'string' } }, + date: { attributes: { localId: 'string', timestamp: 'string' }, marks: true }, + emoji: { attributes: { id: 'string', localId: 'string', shortName: 'string', text: 'string' }, marks: true, textAttribute: 'text' }, + hardBreak: { attributes: { localId: 'string', text: 'string' }, marks: false }, + inlineCard: { attributes: { data: 'json', localId: 'string', url: 'string' }, marks: true }, mediaInline: { attributes: { alt: 'string', @@ -22,9 +23,10 @@ export const inlineNodes: Readonly> = { type: 'string', width: 'number', }, + marks: true, }, - mention: { attributes: { accessLevel: 'string', id: 'string', localId: 'string', text: 'string', userType: 'string' }, textAttribute: 'text' }, - status: { attributes: { color: 'string', localId: 'string', style: 'string', text: 'string' }, textAttribute: 'text' }, + mention: { attributes: { accessLevel: 'string', id: 'string', localId: 'string', text: 'string', userType: 'string' }, marks: true, textAttribute: 'text' }, + status: { attributes: { color: 'string', localId: 'string', style: 'string', text: 'string' }, marks: true, textAttribute: 'text' }, } export function inlineNodeModel(type: string): InlineNodeModel | undefined { diff --git a/src/conformance/adf-schema.test.ts b/src/conformance/adf-schema.test.ts index 60b345e..33789b9 100644 --- a/src/conformance/adf-schema.test.ts +++ b/src/conformance/adf-schema.test.ts @@ -31,7 +31,7 @@ test('the ADF JSON Schemas are @atlaskit/adf-schema 57.4.9, vendored byte-exact' }) test('the tables spell the attribute names and kinds the ADF JSON Schemas give each type they spell, the pinned gaps apart', () => { - const held = schemaTypes() + const { held } = schemaTypes() const spelledTypes = spelled() const found: string[] = [] const gapsHeld = new Set() @@ -63,7 +63,7 @@ test('the tables spell the attribute names and kinds the ADF JSON Schemas give e }) test("the ADF JSON Schemas hold no type the tables leave unspelled, the pinned carried ones and the grammar's own apart", () => { - const held = schemaTypes() + const { held } = schemaTypes() const spelledNames = new Set(spelled().map(([type]) => type)) const found: string[] = [] for (const type of held.keys()) { @@ -76,6 +76,11 @@ test("the ADF JSON Schemas hold no type the tables leave unspelled, the pinned c assert.deepEqual(found, []) }) +test('the inline node table gives a type marks exactly where the ADF JSON Schemas give it a marks key', () => { + const { marked } = schemaTypes() + for (const [type, model] of Object.entries(inlineNodes)) assert.equal(model.marks, marked.has(type), type) +}) + function spelled(): Spelled[] { return [ ...Object.entries(blockNodes).map(([type, model]) => spelledType(type, model.attributes, blockArgument(type))), @@ -90,8 +95,9 @@ function spelledType(type: string, attributes: AttributeVocabulary, argument?: s return [type, kinds] } -function schemaTypes(): Map { +function schemaTypes(): { held: Map; marked: Set } { const held = new Map() + const marked = new Set() for (const file of schemaFiles) { const definitions = schemaObject(schemaObject(JSON.parse(readFileSync(join(root, file), 'utf8')), file)['definitions'], `${file} definitions`) for (const [name, definition] of Object.entries(definitions)) { @@ -101,6 +107,7 @@ function schemaTypes(): Map { for (const type of (properties.get('type') ?? []).flatMap((schema) => enumStrings(schema, `${where} type`))) { const kinds = held.get(type) ?? new Map>() held.set(type, kinds) + if (properties.has('marks')) marked.add(type) for (const [attribute, schemas] of attributes) { const attributeKinds = kinds.get(attribute) ?? new Set() kinds.set(attribute, attributeKinds) @@ -110,7 +117,7 @@ function schemaTypes(): Map { } } } - return held + return { held, marked } } function alternatives(schema: SchemaObject, definitions: SchemaObject, where: string): Properties[] { diff --git a/src/markdown/emit/inline-line.ts b/src/markdown/emit/inline-line.ts index 32fd8f5..a6adb76 100644 --- a/src/markdown/emit/inline-line.ts +++ b/src/markdown/emit/inline-line.ts @@ -217,13 +217,12 @@ function nodePath(context: InlineContext, index: number): ConvertErrorPath { return [...context.path, 'content', index] } -// Decides the text-node and slot-fault carries up front, so a line holding many of them is emitted once. +// Decides the text-node, mark and slot-fault carries up front, so a line holding many of them is emitted once. function carries(node: AdfNode, carried: ReadonlySet, 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 + if (model === undefined || (!model.marks && nodeMarks(node).length > 0)) return true const slot = model.textAttribute === undefined ? undefined : nodeAttrs(node)[model.textAttribute] return typeof slot === 'string' && slotFault(node.type, slot) !== undefined && nodeContent(node).length === 0 && node.text === undefined } @@ -283,7 +282,7 @@ function emitMarkedRun(nodes: readonly AdfNode[], mark: AdfMark, depth: number, if (spelling.kind === 'emphasis') return emitEmphasis(nodes, spelling.spelling, depth, range, context) const link = spelling.kind === 'link' ? tryLink(nodes, mark, depth, range, context) : undefined if (link !== undefined) return link - const inner = emitRun(nodes, depth + 1, index, { ...context, bracketed: true, spansLines: false }) + const inner = emitRun(nodes, depth + 1, index, { ...context, bracketed: true }) if (!inner.ok) return inner if (inner.value.carry !== undefined) return inner return success({ segments: [syntax(spellInlineDirectiveOpener(mark.type)), ...inner.value.segments, syntax(`]${attributes}`)] }) diff --git a/src/markdown/parse/inline-content.ts b/src/markdown/parse/inline-content.ts index 776948c..c81c3f7 100644 --- a/src/markdown/parse/inline-content.ts +++ b/src/markdown/parse/inline-content.ts @@ -260,10 +260,10 @@ function directivePiece(scan: Scan, span: DirectiveSpan, index: number): Result< } function directiveMarkPiece(scan: Scan, name: string, mark: AdfMark, slot: SlotContent | undefined, index: number): Result { - if (slot === undefined || slot.nodes.length === 0) { - return failure('unsupported-node-shape', `the ${name} mark wraps the [content] it marks: this one wraps none`, scan.path) + if (slot?.nodes.some(holdsOwnMarks) === true) return failure('unsupported-node-shape', ownMarksInMark, scan.path) + if (slot === undefined || slot.nodes.every(takesNoMarks)) { + return failure('unsupported-node-shape', `the ${name} mark wraps the [content] it marks: this one wraps nothing a mark rides`, scan.path) } - if (slot.nodes.some(holdsOwnMarks)) return failure('unsupported-node-shape', ownMarksInMark, scan.path) const refused = mark.type === 'link' ? refuseLinkDirective(scan, mark, slot.nodes, index) : undefined if (refused !== undefined) return refused return success({ kind: 'nodes', nodes: applyMark(slot.nodes, mark) }) @@ -322,9 +322,9 @@ function linkMark({ destination, title }: LinkDefinition): AdfMark { return { attrs: title === undefined ? { href: destination } : { href: destination, title }, type: 'link' } } -// ADF holds no empty text node for a link mark to ride, so an empty link text reads as its destination, and an empty destination as nothing. +// ADF holds no empty text node for a link mark to ride, so a link text holding nothing a mark rides reads as its destination, and an empty destination as nothing. function linkTo(nodes: readonly Inline[], definition: LinkDefinition): Inline[] { - if (nodes.length > 0) return applyMark(nodes, linkMark(definition)) + if (!nodes.every(takesNoMarks)) return applyMark(nodes, linkMark(definition)) return definition.destination === '' ? [] : [{ marks: [linkMark(definition)], text: definition.destination, type: 'text' }] } @@ -345,6 +345,10 @@ function partText(items: readonly Inline[], scan: Scan): Result { return success(parted) } +function takesNoMarks(item: Inline): boolean { + return isNode(item) && inlineNodeModel(item.type)?.marks === false +} + function isNode(item: Inline): item is AdfNode { return !('kind' in item) } @@ -669,10 +673,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. Atlassian's schema gives a hard break no marks. +// `*(*a*)*` builds one `em`: a node holds at most one mark of each type. function applyMark(nodes: readonly Inline[], mark: AdfMark): Inline[] { return nodes.map((node) => { - if (!isNode(node) || node.type === 'hardBreak') return node + if (!isNode(node) || takesNoMarks(node)) return node const marks = nodeMarks(node) return marks.some((carried) => carried.type === mark.type) ? node : { ...node, marks: [mark, ...marks] } }) diff --git a/src/markdown/parse/markdown-to-adf.test.ts b/src/markdown/parse/markdown-to-adf.test.ts index 4aefe35..c3853f7 100644 --- a/src/markdown/parse/markdown-to-adf.test.ts +++ b/src/markdown/parse/markdown-to-adf.test.ts @@ -837,7 +837,11 @@ test('builds the hard break a mark spelling spans with no marks, in every reader } 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' }]) + const localId = '01a0431d-201e-7400-a175-9c04d3628f95' + assert.deepEqual(content(losslessMarkdownToAdf(`_!adf:hardBreak{localId=${localId}}_\n`)), [{ content: [{ attrs: { localId }, type: 'hardBreak' }], type: 'paragraph' }]) + for (const read of [commonMarkToAdf, losslessMarkdownToAdf, portableMarkdownToAdf]) { + assert.deepEqual(content(read('[\\\n](/u)\n')), [{ content: [marked('/u', link('/u'))], type: 'paragraph' }]) + } }) test('decodes the fenced info string the block walk leaves raw', () => { @@ -1177,9 +1181,11 @@ test('names the link a directive link wraps, no link holding another', () => { }) test('names the directive mark left without the content it wraps', () => { - const named = 'unsupported-node-shape: the underline mark wraps the [content] it marks: this one wraps none' + const named = 'unsupported-node-shape: the underline mark wraps the [content] it marks: this one wraps nothing a mark rides' assert.equal(content(losslessMarkdownToAdf('!adf:underline[]\n')), named) assert.equal(content(losslessMarkdownToAdf('!adf:underline{}\n')), named) + assert.equal(content(losslessMarkdownToAdf('!adf:underline[!adf:hardBreak{}]\n')), named) + assert.equal(content(losslessMarkdownToAdf('!adf:link[!adf:hardBreak{}]{href="/u" title=t}\n')), named.replace('underline', 'link')) }) test('reads the text break only between two text nodes CommonMark joins, building no node', () => { diff --git a/todo.md b/todo.md index f9a8da8..ff49da4 100644 --- a/todo.md +++ b/todo.md @@ -6,7 +6,7 @@ `Bar = 9` -`Next ID = 89` +`Next ID = 91` | Goal | W | |---|---| @@ -25,10 +25,12 @@ | ID | Release | Exempt | Item | R | S | A | G | Goals | Score | |---|---|---|---|---|---|---|---|---|---| +| 90 | 0.2.0 | | **Describe only the formats the release converts in `package.json`'s description and the README's tagline.** | 1 | 1 | 6 | 7 | 3 | 21.7 | | 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 | | 86 | 0.2.0 | | **Write an image whose `media` only a link marks, and one captioned with plain text, in the CommonMark spelling a writer panel picks, in `adfToLosslessMarkdown`.** | 4 | 4 | 5 | 6 | 4, 6 | 13.0 | +| 89 | 0.2.0 | | **Build a `date`, `emoji`, `inlineCard`, `mention` or `status` holding no mark Atlassian's schema withholds from it, in every reader.** | 4 | 4 | 5 | 6 | 4 | 13.0 | | 85 | 0.2.0 | defect | **Write an external image a link marks as `[![alt](url)](href)` in `adfToPortableMarkdown`, keeping the link target.** | 3 | 3 | 6 | 7 | 7 | 12.9 | | 60 | 0.2.0 | decision | **Collect the questions `parse/` asks `emit/` into one named module.** | 3 | 4 | 2 | 6 | 2 | 10.7 | | 59 | 0.2.0 | decision | **Group the directive grammar into `src/markdown/directive/`, move `Read` to `result.ts`, and move `Flavour` and `flavourClaims` to `markdown/flavour.ts`.** | 3 | 5 | 2 | 6 | 2 | 10.4 | @@ -66,6 +68,11 @@ ## Details +### 90. Describe only the formats the release converts in `package.json`'s description and the README's tagline. + +Both say "to and from markdown and HTML", and `0.2.0` ships no HTML (item 7 brings it in `0.3.0`): +`docs/decisions.md` §Docs describe the release being built. + ### 88. Build a `blockTaskItem` holding only the paragraphs and extensions Atlassian's schema allows, in `portableMarkdownToAdf`. `- [x] done\n\n ![a](u)` builds a `blockTaskItem` holding a `mediaSingle`, and `- [x] done\n\n > q` @@ -98,6 +105,13 @@ Every reader reads `[![alt](url)](href)` and `![alt](url "caption")` as these sh (`docs/decisions.md` §The formats are API): changing it inside `0.2.0` spares consumers a second breaking change to stored markdown. +### 89. Build a `date`, `emoji`, `inlineCard`, `mention` or `status` holding no mark Atlassian's schema withholds from it, in every reader. + +`*!adf:mention[@a]{id=…}*` and `[!adf:status[Done]{…}](/u)` build a node holding `em` or `link`. +`full.json` gives these five no `marks`, and `stage-0.json` gives them `annotation` alone, so a site +may reject the document. `InlineNodeModel.marks` (`src/adf/inline-nodes.ts`) then names the mark +types a node takes, and the emitter carries a node holding another. + ### 85. Write an external image a link marks as `[![alt](url)](href)` in `adfToPortableMarkdown`, keeping the link target. Today `[![a](/u)](/v)` reads as the image whose `media` the link marks, and `adfToPortableMarkdown`