From c9f9276df9c0467c4089bc9f446a6011de4da84b Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Wed, 14 Oct 2026 20:42:33 +0200 Subject: [PATCH] =?UTF-8?q?Answer=20architecture=20round=201:=201=20?= =?UTF-8?q?=F0=9F=94=B4,=202=20=F0=9F=9F=A1;=20read=20a=20node's=20taken?= =?UTF-8?q?=20mark=20types=20once,=20let=20the=20highlight=20ask=20takesMa?= =?UTF-8?q?rk,=20correct=20the=20README's=20code-span=20bullet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MIGRATION.md | 2 +- README.md | 4 ++-- corpus/commonmark-spec/exceptions.json | 4 ++-- docs/decisions.md | 3 ++- src/adf/inline-nodes.ts | 12 ++++++++---- src/markdown/emit/inline-line.ts | 5 +++-- src/markdown/parse/inline-content.ts | 4 ++-- 7 files changed, 20 insertions(+), 14 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 48c20dc..214577e 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -7,7 +7,7 @@ In `losslessMarkdownToAdf`, a mark spelling skips a node Atlassian's schema with | Input | `0.2.0` | `0.3.0` | | --- | --- | --- | | a `date`, `emoji`, `inlineCard`, `mention`, `status` or `mediaInline` inside a mark's spelling (`**!adf:mention[@A]{id=x}**`) | the node holds the mark | the node holds only marks its type takes; text beside it keeps the mark | -| a link or directive mark around only nodes that cannot take it, hard breaks under a link aside (`[!adf:status[Done]{color=green}](/u)`) | the nodes hold the mark | `unsupported-node-shape`; write the mark around text beside them | +| a link or directive mark around only nodes that cannot take it, a code span included, hard breaks under a link aside (`[!adf:status[Done]{color=green}](/u)`) | the nodes hold the mark | `unsupported-node-shape`; write the mark around text beside them | | a code span inside any mark's spelling but a link's (``**`npm ci`**``), in every reader | the code text holds the mark | the code text holds only `code` and the link | `0.2.0` wrote these spellings for a node holding such a mark; `0.3.0` writes the node as diff --git a/README.md b/README.md index 9ef3b5f..3cdb33b 100644 --- a/README.md +++ b/README.md @@ -536,8 +536,8 @@ Serves [Goals](#goals) 1, 4 and 5. alt text linked to its URL: `See ![the moon](moon.png).` reads as `See [the moon](moon.png).`. To keep an image an image, give it a paragraph of its own: a blank line between it and any other line. - - A code span inside emphasis, strikethrough or a directive mark reads as code alone, since ADF - gives code text no mark but a link: ``**`npm ci`**`` reads as `` `npm ci` ``. + - A code span inside emphasis reads as code alone, since ADF gives code text no mark but a link + or an annotation: ``**`npm ci`**`` reads as `` `npm ci` ``. - Inside a link, an image reads as plain text the link marks. A link alone in its paragraph whose whole text is one image reads as that image, with the link marking it. - An image's title becomes its caption when the image stays an image, and its link's title when diff --git a/corpus/commonmark-spec/exceptions.json b/corpus/commonmark-spec/exceptions.json index 9c40e9d..e364c38 100644 --- a/corpus/commonmark-spec/exceptions.json +++ b/corpus/commonmark-spec/exceptions.json @@ -277,7 +277,7 @@ "divergence": "a 1/5 em 1/3", "example": 516, "kind": "mark-model", - "reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element." + "reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element, and skips the code text, which takes no em." }, { "check": "count", @@ -291,7 +291,7 @@ "divergence": "a 1/5 em 1/3", "example": 530, "kind": "mark-model", - "reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element." + "reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element, and skips the code text, which takes no em." }, { "check": "count", diff --git a/docs/decisions.md b/docs/decisions.md index 4deb0f4..ce77576 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -259,7 +259,8 @@ Their CommonMark suite exceptions are `node-model` (§The CommonMark suite check ## A mark skips a node that cannot hold it, and a mark wrapping only such nodes is refused, but for a link around hard breaks alone 2026-10-14, two reader panels of the viewer/editor app, the LLM pipeline and a support agent, 3 of -3 each. Goals 1, 4 and 6. Valid while Atlassian's schema withholds marks from inline nodes. +3 each. Goals 1, 4 and 6. Valid while Atlassian's schema withholds marks from inline nodes. Code +text follows by this entry's rule, unpanelled, 2026-10-14. A mark spelling skips a node that takes none of its type, code text included, which takes only `link` and `annotation`. So `**Ping !adf:mention[@A]{id=x} now**` reads as bold text around a plain diff --git a/src/adf/inline-nodes.ts b/src/adf/inline-nodes.ts index 8320174..71768d3 100644 --- a/src/adf/inline-nodes.ts +++ b/src/adf/inline-nodes.ts @@ -37,8 +37,12 @@ export function inlineNodeModel(type: string): InlineNodeModel | undefined { return Object.hasOwn(inlineNodes, type) ? inlineNodes[type] : undefined } -// The table withholds nothing from a type it does not list, such as text; Atlassian's schema withholds from code text all but `codeTextMarkTypes`. -export function takesMark(node: AdfNode, markType: string): boolean { - if (node.type === 'text' && nodeMarks(node).some((mark) => mark.type === 'code')) return codeTextMarkTypes.includes(markType) - return inlineNodeModel(node.type)?.markTypes.includes(markType) ?? true +// Undefined where the schema withholds no mark: the table lists no such type, text among them, but code text takes only `codeTextMarkTypes`. +export function takenMarkTypes(node: AdfNode): readonly string[] | undefined { + if (node.type === 'text') return nodeMarks(node).some((mark) => mark.type === 'code') ? codeTextMarkTypes : undefined + return inlineNodeModel(node.type)?.markTypes +} + +export function takesMark(node: AdfNode, markType: string): boolean { + return takenMarkTypes(node)?.includes(markType) ?? true } diff --git a/src/markdown/emit/inline-line.ts b/src/markdown/emit/inline-line.ts index 705e957..a061840 100644 --- a/src/markdown/emit/inline-line.ts +++ b/src/markdown/emit/inline-line.ts @@ -11,7 +11,7 @@ import { failure, success, type ConvertErrorPath, type Result } from '../../resu import { flavourClaims, highlightDelimiter } from '../portable/conventions.ts' import { holdsNullCharacter, trimTrailingSpace } from '../commonmark/grammar.ts' import { identicalMark, isSpellableText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts' -import { inlineNodeModel, takesMark } from '../../adf/inline-nodes.ts' +import { inlineNodeModel, takenMarkTypes } from '../../adf/inline-nodes.ts' import { joinsWhenRead, textBreakSpelling } from '../adjacent-text.ts' import { largestNesting } from '../../nesting.ts' import { longestBacktickRun } from '../commonmark/backtick-runs.ts' @@ -223,7 +223,8 @@ function nodePath(context: InlineContext, index: number): ConvertErrorPath { // Decides the text-node, withheld-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) || nodeMarks(node).some((mark) => !takesMark(node, mark.type))) return true + const taken = takenMarkTypes(node) + if (carried.has(index) || (taken !== undefined && nodeMarks(node).some((mark) => !taken.includes(mark.type)))) return true if (node.type === 'text') return !isSpellableText(node) || typeof node.text !== 'string' || node.text === '' const model = inlineNodeModel(node.type) if (model === undefined) return true diff --git a/src/markdown/parse/inline-content.ts b/src/markdown/parse/inline-content.ts index ab563ce..9a998d6 100644 --- a/src/markdown/parse/inline-content.ts +++ b/src/markdown/parse/inline-content.ts @@ -569,7 +569,7 @@ function pairedHighlights(pieces: readonly Piece[], nodes: readonly Inline[][]): return paired } -// Atlassian's schema refuses a highlight on code, a node holds one highlight, and a rebuilt node would lose its attributes. +// A node holds one highlight, and a rebuilt node would lose its attributes. function markHighlights(pieces: readonly Piece[], nodes: Inline[][], paired: readonly { closer: number; opener: number }[]): void { for (const [index, piece] of pieces.entries()) { if (piece.kind === 'highlight') nodes[index] = adfNodes(nodes[index] ?? []).map((holder) => ({ ...holder, text: highlightDelimiter })) @@ -584,7 +584,7 @@ function markHighlights(pieces: readonly Piece[], nodes: Inline[][], paired: rea function highlighted(node: Inline): Inline { if (!isNode(node)) return node const marks = nodeMarks(node) - if (node.type !== 'text' || Object.keys(nodeAttrs(node)).length > 0 || marks.some((mark) => mark.type === 'code' || mark.type === 'backgroundColor')) return node + if (node.type !== 'text' || Object.keys(nodeAttrs(node)).length > 0 || !marksItem(node, editorHighlight.type) || marks.some((mark) => mark.type === editorHighlight.type)) return node return { ...node, marks: [editorHighlight, ...marks] } }