mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Give code text no mark but code, link and annotation, in every reader
This commit is contained in:
@@ -8,6 +8,10 @@
|
||||
`unsupported-node-shape`, even in markdown `adfToLosslessMarkdown` wrote. A link around hard
|
||||
breaks alone still reads as before. `adfToLosslessMarkdown` writes a node holding a mark its type
|
||||
cannot take as `!adf:carry{json="…"}`. `MIGRATION.md` says how to convert stored markdown.
|
||||
- **Breaking:** every reader gives code text no mark but `link` and `annotation`, as Atlassian's
|
||||
schema requires: ``**`npm ci`**`` now reads as plain code, and a directive mark around only code
|
||||
spans is `unsupported-node-shape`. `adfToLosslessMarkdown` writes code text holding another mark
|
||||
as `!adf:carry{json="…"}`, and `adfToPortableMarkdown` drops that mark.
|
||||
- `losslessMarkdownToAdf` names the CommonMark spelling to write when it refuses a directive whose
|
||||
node CommonMark spells, such as `[](href)` for a linked `!adf:mediaSingle`.
|
||||
- The README compares the package with seven other ADF converters, each checked against its
|
||||
|
||||
@@ -8,6 +8,7 @@ In `losslessMarkdownToAdf`, a mark spelling skips a node Atlassian's schema with
|
||||
| --- | --- | --- |
|
||||
| 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 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
|
||||
`!adf:carry{json="…"}`. `0.3.0` reads markdown stored from such a document without the mark, or
|
||||
|
||||
@@ -390,7 +390,8 @@ saving what this pair read replaces mentions, attachments and macros with text.
|
||||
its key, `_(jira-issues-table not included)_`, or `_(extension not included)_` without one, and
|
||||
`_(synced block not included)_` for a `syncBlock`.
|
||||
- `backgroundColor`, `code`, `em`, `link`, `strike` and `strong` stay; every other mark drops,
|
||||
keeping its text, and so does a mark the flavour cannot spell where it stands.
|
||||
keeping its text, and so does a mark the flavour cannot spell where it stands. Code text keeps
|
||||
only `link`.
|
||||
- A newline in text is a hard break and in an expand's title a space, edge whitespace outside a
|
||||
link or code span is trimmed, carriage returns and null characters are removed, and an empty
|
||||
paragraph drops.
|
||||
@@ -484,7 +485,7 @@ Every call can raise these:
|
||||
- puts a line ending or a null character in an `emoji`, `mention` or `status` content slot:
|
||||
replace a line ending with a space, or, for either, write the node as `!adf:carry`;
|
||||
- wraps a directive mark around nothing, or only around nodes that take no such mark, such as a
|
||||
hard break or a mention;
|
||||
hard break, a mention or a code span;
|
||||
- writes a link whose text holds something besides hard breaks, but no node that takes a link,
|
||||
such as `[!adf:status[Done]{color=green}](/u)`: write the link around text beside them;
|
||||
- writes as a directive a node or mark the lossless flavour spells as CommonMark;
|
||||
@@ -535,6 +536,8 @@ Serves [Goals](#goals) 1, 4 and 5.
|
||||
alt text linked to its URL: `See .` 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` ``.
|
||||
- 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
|
||||
|
||||
@@ -258,20 +258,6 @@
|
||||
"kind": "mark-model",
|
||||
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
|
||||
},
|
||||
{
|
||||
"check": "count",
|
||||
"divergence": "em 1/2",
|
||||
"example": 478,
|
||||
"kind": "mark-model",
|
||||
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
|
||||
},
|
||||
{
|
||||
"check": "count",
|
||||
"divergence": "em 1/2",
|
||||
"example": 479,
|
||||
"kind": "mark-model",
|
||||
"reason": "The mark spans adjacent text nodes, counted once per node where CommonMark nests one element."
|
||||
},
|
||||
{
|
||||
"check": "text",
|
||||
"divergence": "\"\" against \"./target.md\"",
|
||||
@@ -288,7 +274,7 @@
|
||||
},
|
||||
{
|
||||
"check": "count",
|
||||
"divergence": "a 1/5 em 1/4",
|
||||
"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."
|
||||
@@ -302,7 +288,7 @@
|
||||
},
|
||||
{
|
||||
"check": "count",
|
||||
"divergence": "a 1/5 em 1/4",
|
||||
"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."
|
||||
|
||||
+3
-2
@@ -261,8 +261,9 @@ Their CommonMark suite exceptions are `node-model` (§The CommonMark suite check
|
||||
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.
|
||||
|
||||
A mark spelling skips a node that takes none of its type, so `**Ping !adf:mention[@A]{id=x} now**`
|
||||
reads as bold text around a plain mention. A link whose text holds something besides hard breaks,
|
||||
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
|
||||
mention, and ``**`npm ci`**`` as plain code. A link whose text holds something besides hard breaks,
|
||||
but no node that takes a link, is `unsupported-node-shape` in `losslessMarkdownToAdf`: the panel
|
||||
refused to have `[!adf:status[Done]{color=green}](/u)` read as `/u` linked beside the status. A link
|
||||
text of hard breaks alone still reads as its destination, then the breaks (§An image reads as an
|
||||
|
||||
+4
-3
@@ -549,9 +549,10 @@ A block's `marks` key also holds these, which no inline spelling wraps:
|
||||
- `fragment` — Attributes: `localId` (string), `name` (string).
|
||||
- `indentation` — Attributes: `level` (number, 1–6).
|
||||
|
||||
A spelling adds its mark to every text node it wraps, and to every other inline node Atlassian's
|
||||
schema gives that mark. Of the marks a spelling makes, `date`, `emoji`, `hardBreak`, `inlineCard`,
|
||||
`mention` and `status` take none, and `mediaInline` takes only `border` and `link`. Nesting is the
|
||||
A spelling adds its mark to every text node it wraps but one marked `code`, which takes only
|
||||
`link`, and to every other inline node Atlassian's schema gives that mark: ``**`npm ci`**`` reads
|
||||
as plain code. Of the marks a spelling makes, `date`, `emoji`, `hardBreak`, `inlineCard`, `mention`
|
||||
and `status` take none, and `mediaInline` takes only `border` and `link`. 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
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
import type { AdfNode } from './document.ts'
|
||||
import type { AttributeVocabulary } from './attribute-vocabulary.ts'
|
||||
import { nodeMarks } from './document.ts'
|
||||
|
||||
export const codeTextMarkTypes: readonly string[] = ['annotation', 'code', 'link']
|
||||
|
||||
export type InlineNodeModel = {
|
||||
attributes: AttributeVocabulary
|
||||
@@ -33,7 +37,8 @@ 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.
|
||||
export function takesMark(type: string, markType: string): boolean {
|
||||
return inlineNodeModel(type)?.markTypes.includes(markType) ?? true
|
||||
// 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
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import test from 'node:test'
|
||||
import type { AttributeKind, AttributeVocabulary } from '../adf/attribute-vocabulary.ts'
|
||||
import { blockArgument } from '../markdown/block-directive.ts'
|
||||
import { blockNodes } from '../adf/block-nodes.ts'
|
||||
import { inlineNodes } from '../adf/inline-nodes.ts'
|
||||
import { codeTextMarkTypes, inlineNodes } from '../adf/inline-nodes.ts'
|
||||
import { markVocabularies } from '../adf/mark-attributes.ts'
|
||||
|
||||
type Held = Map<string, Set<AttributeKind>>
|
||||
@@ -81,6 +81,17 @@ test('the inline node table gives a type exactly the mark types the ADF JSON Sch
|
||||
for (const [type, model] of Object.entries(inlineNodes)) assert.deepEqual(model.markTypes, [...(marked.get(type) ?? [])].sort(), type)
|
||||
})
|
||||
|
||||
test('code text takes exactly the mark types the ADF JSON Schemas give a code_inline_node', () => {
|
||||
for (const file of schemaFiles) {
|
||||
const definitions = schemaObject(schemaObject(JSON.parse(readFileSync(join(root, file), 'utf8')), file)['definitions'], `${file} definitions`)
|
||||
const where = `${file} code_inline_node`
|
||||
const marks = alternatives(schemaObject(definitions['code_inline_node'], where), definitions, where).flatMap((properties) => properties.get('marks') ?? [])
|
||||
// allOf narrows text_node's marks, which hold every mark type, to the ones code text takes.
|
||||
const narrowed = marks.map((schema) => heldMarkTypes(schema, definitions, `${where}.marks`)).filter((types) => !types.includes('every mark type'))
|
||||
assert.deepEqual(narrowed.flat().sort(), codeTextMarkTypes, file)
|
||||
}
|
||||
})
|
||||
|
||||
function spelled(): Spelled[] {
|
||||
return [
|
||||
...Object.entries(blockNodes).map(([type, model]) => spelledType(type, model.attributes, blockArgument(type))),
|
||||
|
||||
@@ -289,6 +289,8 @@ test('carries an inline node holding a mark Atlassian\'s schema withholds from i
|
||||
assert.equal(carried({ attrs: { id: 'b1c2', text: '@A' }, marks: [{ type: 'strong' }], type: 'mention' }), '!adf:carry{json="{\\"attrs\\":{\\"id\\":\\"b1c2\\",\\"text\\":\\"@A\\"},\\"marks\\":[{\\"type\\":\\"strong\\"}],\\"type\\":\\"mention\\"}"}\n')
|
||||
assert.equal(carried({ attrs: { id: 'm1' }, marks: [{ type: 'em' }], type: 'mediaInline' }), '!adf:carry{json="{\\"attrs\\":{\\"id\\":\\"m1\\"},\\"marks\\":[{\\"type\\":\\"em\\"}],\\"type\\":\\"mediaInline\\"}"}\n')
|
||||
assert.equal(carried({ attrs: { id: 'm1' }, marks: [{ attrs: { href: '/u' }, type: 'link' }], type: 'mediaInline' }), '[!adf:mediaInline{id=m1}](/u)\n')
|
||||
assert.equal(carried({ marks: [{ type: 'strong' }, { type: 'code' }], text: 'x', type: 'text' }), '!adf:carry{json="{\\"marks\\":[{\\"type\\":\\"strong\\"},{\\"type\\":\\"code\\"}],\\"text\\":\\"x\\",\\"type\\":\\"text\\"}"}\n')
|
||||
assert.equal(carried({ marks: [{ attrs: { href: '/u' }, type: 'link' }, { type: 'code' }], text: 'x', type: 'text' }), '[`x`](/u)\n')
|
||||
})
|
||||
|
||||
test('spells a heading level no ATX heading fits as a directive', () => {
|
||||
|
||||
@@ -223,10 +223,10 @@ 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<number>, index: number): boolean {
|
||||
if (carried.has(index)) return true
|
||||
if (carried.has(index) || nodeMarks(node).some((mark) => !takesMark(node, 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 || nodeMarks(node).some((mark) => !takesMark(node.type, mark.type))) return true
|
||||
if (model === undefined) 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
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ function partText(items: readonly Inline[], scan: Scan): Result<AdfNode[]> {
|
||||
|
||||
// spec/flavour.md, Marks: a spelling skips a node Atlassian's schema withholds its mark from.
|
||||
function marksItem(item: Inline, type: string): boolean {
|
||||
return !isNode(item) || takesMark(item.type, type)
|
||||
return !isNode(item) || takesMark(item, type)
|
||||
}
|
||||
|
||||
function isNode(item: Inline): item is AdfNode {
|
||||
|
||||
@@ -931,6 +931,18 @@ test('gives an inline node no mark Atlassian\'s schema withholds from it', () =>
|
||||
assert.equal(read('!adf:link[!adf:status[Done]{color=green}]{collection=c href="/u"}\n'), 'unsupported-node-shape: the link mark wraps the [content] it marks: this one wraps only nodes that take no link mark: write the link mark around text beside them')
|
||||
})
|
||||
|
||||
test('gives code text no mark but code, link and annotation', () => {
|
||||
const code: AdfMark = { type: 'code' }
|
||||
for (const read of [commonMarkToAdf, losslessMarkdownToAdf, portableMarkdownToAdf]) {
|
||||
assert.deepEqual(content(read('**`npm ci`**\n')), [{ content: [marked('npm ci', code)], type: 'paragraph' }])
|
||||
assert.deepEqual(content(read('*run `npm ci`*\n')), [{ content: [marked('run ', em), marked('npm ci', code)], type: 'paragraph' }])
|
||||
assert.deepEqual(content(read('**[`npm ci`](/u)**\n')), [{ content: [marked('npm ci', link('/u'), code)], type: 'paragraph' }])
|
||||
}
|
||||
assert.deepEqual(content(portableMarkdownToAdf('~~`npm ci`~~\n')), [{ content: [marked('npm ci', code)], type: 'paragraph' }])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('!adf:underline[run `npm ci`]\n')), [{ content: [marked('run ', underline), marked('npm ci', code)], type: 'paragraph' }])
|
||||
assert.equal(content(losslessMarkdownToAdf('!adf:underline[`npm ci`]\n')), 'unsupported-node-shape: the underline mark wraps the [content] it marks: this one wraps only nodes that take no underline mark: write the underline mark around text beside them')
|
||||
})
|
||||
|
||||
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' },
|
||||
|
||||
@@ -307,10 +307,11 @@ test('keeps a table as a pipe table headed by its first row, one line per cell',
|
||||
assert.equal(portable(node('table', {}, row())), '')
|
||||
})
|
||||
|
||||
test('keeps code, em, link, strike and strong and drops every other mark, keeping its text', () => {
|
||||
test('keeps code, em, link, strike and strong and drops every other mark, and code text every mark but link, keeping its text', () => {
|
||||
const marks: AdfMark[] = [{ type: 'strike' }, { attrs: { type: 'sub' }, type: 'subsup' }, { type: 'underline' }, { attrs: { color: '#f00' }, type: 'textColor' }]
|
||||
assert.equal(portable(paragraph(text('H'), text('2', ...marks), text('O', em, strong), text('!', { attrs: { size: 1 }, type: 'border' }))), 'H~~2~~_**O**_!\n')
|
||||
assert.equal(portable(paragraph(text('x', code, strong), text(' '), text('y', { attrs: { x: 1 }, type: 'strong' }), text('z', em, em))), '**`x`** **y**_z_\n')
|
||||
assert.equal(portable(paragraph(text('x', code, strong), text(' '), text('y', { attrs: { x: 1 }, type: 'strong' }), text('z', em, em))), '`x` **y**_z_\n')
|
||||
assert.equal(portable(paragraph(text('x', strong, link('/u'), code))), '[`x`](/u)\n')
|
||||
assert.equal(portable(paragraph(text('site', { attrs: { collection: 'c', href: 'https://e.com', id: 'i' }, type: 'link' }))), '[site](https://e.com)\n')
|
||||
})
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { AdfAttributes, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import type { LineContainer } from '../line-container.ts'
|
||||
import type { MarkRun } from '../emit/line-escaping.ts'
|
||||
import { blockNodeModel } from '../../adf/block-nodes.ts'
|
||||
import { codeTextMarkTypes } from '../../adf/inline-nodes.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { joinsWhenEditorNormal, sameMarkWhenEditorNormal } from './editor-normal.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
@@ -141,9 +142,8 @@ function portableMarks(marks: readonly AdfMark[], container: LineContainer, text
|
||||
if (portable !== undefined) kept.push(portable)
|
||||
}
|
||||
const rank = (mark: AdfMark): number => (mark.type === highlight ? 0 : mark.type === 'code' ? 2 : 1)
|
||||
// The reader highlights no code, as Atlassian's schema allows none.
|
||||
const code = kept.some((mark) => mark.type === 'code')
|
||||
return kept.filter((mark) => !code || mark.type !== highlight).sort((first, second) => rank(first) - rank(second))
|
||||
return kept.filter((mark) => !code || codeTextMarkTypes.includes(mark.type)).sort((first, second) => rank(first) - rank(second))
|
||||
}
|
||||
|
||||
export function portableLink(mark: AdfMark, container: LineContainer): AdfMark | undefined {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
`Bar = 9`
|
||||
|
||||
`Next ID = 115`
|
||||
`Next ID = 116`
|
||||
|
||||
| Goal | W |
|
||||
|---|---|
|
||||
@@ -26,8 +26,8 @@
|
||||
| ID | Release | Exempt | Item | R | S | C | A | G | Goals | Score |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 73 | 0.3.0 | | **Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.** | 1 | 2 | 1 | 6 | 8 | 3 | 23.0 |
|
||||
| 114 | 0.3.0 | defect | **Build code-marked text holding no mark besides `link` and `annotation`, in every reader, and have `adfToLosslessMarkdown` carry code text holding any other mark.** | 3 | 4 | 5 | 5 | 7 | 4 | 15.4 |
|
||||
| 99 | 0.3.0 | defect | **Write a linked external image standing inline or in a table cell in the spelling a reader panel picks, in `adfToPortableMarkdown`.** | 3 | 3 | 5 | 6 | 7 | 7 | 12.9 |
|
||||
| 115 | 0.3.0 | defect | **Build text holding no `border` mark in every reader, and have `adfToLosslessMarkdown` carry text holding one.** | 3 | 3 | 3 | 4 | 6 | 4 | 12.3 |
|
||||
| 100 | 0.3.0 | decision | **Put the reading of an image's title as its caption back to a reader panel, now that a writer panel reads a title as hover text.** | 3 | 3 | 3 | 4 | 5 | 4, 6 | 10.9 |
|
||||
| 60 | 0.3.0 | decision | **Collect the questions `parse/` asks `emit/` into one named module.** | 3 | 4 | 4 | 2 | 6 | 2 | 10.7 |
|
||||
| 59 | 0.3.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 | 3 | 2 | 6 | 2 | 10.4 |
|
||||
@@ -77,13 +77,6 @@ README table, its versions and its date in step with the output. We draft each p
|
||||
goes; the maintainer posts them once `0.3.0` is published. Found while planning discoverability,
|
||||
2026-10-04.
|
||||
|
||||
### 114. Build code-marked text holding no mark besides `link` and `annotation`, in every reader, and have `adfToLosslessMarkdown` carry code text holding any other mark.
|
||||
|
||||
``**`npm ci`**`` builds text holding `[strong, code]`; `code_inline_node` (`spec/adf-schema/full.json`)
|
||||
gives code text `code`, `link` and `annotation` alone, so a site may reject the document. The `==`
|
||||
highlight already skips code (`highlighted`, `src/markdown/parse/inline-content.ts`). It changes
|
||||
how stored markdown reads, so it ships in a minor.
|
||||
|
||||
### 99. Write a linked external image standing inline or in a table cell in the spelling a reader panel picks, in `adfToPortableMarkdown`.
|
||||
|
||||
`mediaLeaves` (`src/markdown/portable/inline-reduction.ts`) writes such an image `[alt](url)`, dropping
|
||||
@@ -92,6 +85,14 @@ there reads back as `[alt](href)`, breaking the byte-for-byte rewrite, so only o
|
||||
`href` can survive; a reader panel picks which. If it picks `url`, the README names the dropped
|
||||
`href` where it says link targets stay.
|
||||
|
||||
### 115. Build text holding no `border` mark in every reader, and have `adfToLosslessMarkdown` carry text holding one.
|
||||
|
||||
`!adf:border[x]{color="#091e42" size=2}` builds text holding `border`, which
|
||||
`formatted_text_inline_node` (`spec/adf-schema/full.json`) withholds from text: only `media` and
|
||||
`mediaInline` take it, so a site may reject the document. `takesMark` (`src/adf/inline-nodes.ts`)
|
||||
gives text every mark but code text's. It changes how stored markdown reads, so it ships in a minor.
|
||||
Found while building item 114, 2026-10-14.
|
||||
|
||||
### 100. Put the reading of an image's title as its caption back to a reader panel, now that a writer panel reads a title as hover text.
|
||||
|
||||
Every reader reads `` as an image captioned `t` (`docs/decisions.md` §An image reads as an
|
||||
|
||||
Reference in New Issue
Block a user