mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
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
This commit is contained in:
+4
-2
@@ -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.
|
||||
|
||||
+1
-1
@@ -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 (`[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 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 |
|
||||
|
||||
|
||||
@@ -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 .` reads as
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
*One\
|
||||
Two*
|
||||
|
||||
[Three
|
||||
Four](/u)
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
_One_!adf:carry{json="{\"marks\":[{\"type\":\"em\"}],\"type\":\"hardBreak\"}"}_Two_
|
||||
@@ -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 |
|
||||
|
||||
+4
-3
@@ -205,14 +205,15 @@ where its reference implementation nests one `<a>` 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.
|
||||
|
||||
+1
-1
@@ -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).
|
||||
|
||||
@@ -2,14 +2,15 @@ import type { AttributeVocabulary } from './attribute-vocabulary.ts'
|
||||
|
||||
export type InlineNodeModel = {
|
||||
attributes: AttributeVocabulary
|
||||
marks: boolean
|
||||
textAttribute?: string
|
||||
}
|
||||
|
||||
export const inlineNodes: Readonly<Record<string, InlineNodeModel>> = {
|
||||
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<Record<string, InlineNodeModel>> = {
|
||||
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 {
|
||||
|
||||
@@ -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<string>()
|
||||
@@ -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<string, Held> {
|
||||
function schemaTypes(): { held: Map<string, Held>; marked: Set<string> } {
|
||||
const held = new Map<string, Held>()
|
||||
const marked = new Set<string>()
|
||||
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<string, Held> {
|
||||
for (const type of (properties.get('type') ?? []).flatMap((schema) => enumStrings(schema, `${where} type`))) {
|
||||
const kinds = held.get(type) ?? new Map<string, Set<AttributeKind>>()
|
||||
held.set(type, kinds)
|
||||
if (properties.has('marks')) marked.add(type)
|
||||
for (const [attribute, schemas] of attributes) {
|
||||
const attributeKinds = kinds.get(attribute) ?? new Set<AttributeKind>()
|
||||
kinds.set(attribute, attributeKinds)
|
||||
@@ -110,7 +117,7 @@ function schemaTypes(): Map<string, Held> {
|
||||
}
|
||||
}
|
||||
}
|
||||
return held
|
||||
return { held, marked }
|
||||
}
|
||||
|
||||
function alternatives(schema: SchemaObject, definitions: SchemaObject, where: string): Properties[] {
|
||||
|
||||
@@ -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<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
|
||||
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}`)] })
|
||||
|
||||
@@ -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<Piece> {
|
||||
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<AdfNode[]> {
|
||||
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] }
|
||||
})
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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 `[](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<T>` 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 ` builds a `blockTaskItem` holding a `mediaSingle`, and `- [x] done\n\n > q`
|
||||
@@ -98,6 +105,13 @@ Every reader reads `[](href)` and `` 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 `[](href)` in `adfToPortableMarkdown`, keeping the link target.
|
||||
|
||||
Today `[](/v)` reads as the image whose `media` the link marks, and `adfToPortableMarkdown`
|
||||
|
||||
Reference in New Issue
Block a user