mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Keep the link around a note in adfToPortableMarkdown, chosen in shown-text beside the image's
This commit is contained in:
@@ -2,6 +2,9 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- `adfToPortableMarkdown` keeps the link marking a stored image, an extension or a card with no
|
||||
text around the note standing in its place, so its target is no longer dropped:
|
||||
`[_(image not included)_](https://example.com/a)`.
|
||||
- **Breaking:** `readAdfDocument` replaces `isAdfDocument`: it returns a `Result<AdfDocument>` whose
|
||||
error names the branch that refused, and `MIGRATION.md` shows the change.
|
||||
- `readAdfDocument`, `adfToHtml`, `adfToLosslessMarkdown` and `adfToPortableMarkdown` read the
|
||||
|
||||
@@ -486,7 +486,7 @@ saving what this pair read replaces mentions, attachments and macros with text.
|
||||
naming it where it stood: `_(image not included)_` for stored media with no `alt`,
|
||||
`_(link card not included)_` for a card with neither URL nor name, an extension with no `text`
|
||||
its key, `_(jira-issues-table not included)_`, or `_(extension not included)_` without one, and
|
||||
`_(synced block not included)_` for a `syncBlock`.
|
||||
`_(synced block not included)_` for a `syncBlock`. A link marking the node stays around its note.
|
||||
- `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. A line still
|
||||
read back wrong after 32 tries drops every `em`, `strike` and `strong` at once. Code text keeps
|
||||
|
||||
@@ -936,3 +936,17 @@ Read most (opens and searches):
|
||||
- `src/markdown/portable/adf-to-portable-markdown.ts` 2
|
||||
- `src/result.ts` 2
|
||||
- `AGENTS.md` 1
|
||||
|
||||
## Reading cost, chunk 93989f5f1745, finished 2026-11-09T08:12:20Z
|
||||
|
||||
89422 new input tokens before the first commit.
|
||||
|
||||
Read most (opens and searches):
|
||||
|
||||
- `ci.sh` 4
|
||||
- `todo.md` 4
|
||||
- `docker-runner.sh` 2
|
||||
- `docs/decisions.md` 2
|
||||
- `AGENTS.md` 1
|
||||
- `README.md` 1
|
||||
- `publish.sh` 1
|
||||
|
||||
@@ -17,8 +17,15 @@ export function oneLine(text: string): string {
|
||||
return text.replace(/[\r\u0000]/g, '').replace(/\n/g, ' ')
|
||||
}
|
||||
|
||||
// The name of what a node references where it shows no text of its own, for a note standing in its place.
|
||||
export function referenceName(node: AdfNode): string | undefined {
|
||||
export type ShownNote = { link: LinkAttrs | undefined; name: string }
|
||||
|
||||
// The note standing in place of a node that shows no text of its own: `name` what it references, and `link` the node's link, which both writers keep around the note. `undefined` where the node references nothing.
|
||||
export function shownNote(node: AdfNode): ShownNote | undefined {
|
||||
const name = referenceName(node)
|
||||
return name === undefined ? undefined : { link: shownLink(node), name }
|
||||
}
|
||||
|
||||
function referenceName(node: AdfNode): string | undefined {
|
||||
switch (node.type) {
|
||||
case 'blockCard':
|
||||
case 'embedCard':
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { LinkAttrs } from '../adf/mark-attributes.ts'
|
||||
import { anchorOpener } from './anchor-element.ts'
|
||||
import { languageClass } from './language-class.ts'
|
||||
import { nodeAttrs, nodeContent } from '../adf/document.ts'
|
||||
import { partsShownText, referenceName, shownImage, shownLink, shownText, shownUrl } from '../adf/shown-text.ts'
|
||||
import { partsShownText, shownImage, shownLink, shownNote, shownText, shownUrl } from '../adf/shown-text.ts'
|
||||
import { shownValue } from './escaping.ts'
|
||||
|
||||
// Compared by reference: each block makes one, so its content after a nested block starts a new shown block. `code`: this block, where it is a code block.
|
||||
@@ -40,7 +40,7 @@ function shownPiece(node: AdfNode, lineFeed: string): string {
|
||||
return linkedText(node, shownText(node), shownUrl(node), lineFeed)
|
||||
}
|
||||
|
||||
// As portable markdown writes it inline, except that a link around a note stays (docs/decisions.md §The HTML dialect).
|
||||
// As portable markdown writes it inline (docs/decisions.md §The HTML dialect).
|
||||
function linkedImage(media: AdfNode, lineFeed: string): string {
|
||||
const image = shownImage(media, shownLink(media)?.href)
|
||||
const shown = linkedText(media, image?.text, image?.link, lineFeed)
|
||||
@@ -48,11 +48,10 @@ function linkedImage(media: AdfNode, lineFeed: string): string {
|
||||
}
|
||||
|
||||
function linkedText(node: AdfNode, standIn: string | undefined, url: string | undefined, lineFeed: string): string {
|
||||
const link = shownLink(node) ?? (url === undefined ? undefined : { href: url })
|
||||
const text = node.text ?? standIn
|
||||
if (text !== undefined) return linked(shownValue(text, lineFeed), link)
|
||||
const name = referenceName(node)
|
||||
return name === undefined ? '' : linked(`<em>(${shownValue(name, lineFeed)})</em>`, link)
|
||||
if (text !== undefined) return linked(shownValue(text, lineFeed), shownLink(node) ?? (url === undefined ? undefined : { href: url }))
|
||||
const note = shownNote(node)
|
||||
return note === undefined ? '' : linked(`<em>(${shownValue(note.name, lineFeed)})</em>`, note.link)
|
||||
}
|
||||
|
||||
// The shown HTML inside an `<a>` where it links and HTML can hold the link.
|
||||
|
||||
@@ -296,6 +296,15 @@ test('keeps an external image wherever it stands and spells a stored file as its
|
||||
assert.equal(portable(caption), 'The moon.\n')
|
||||
})
|
||||
|
||||
test('keeps the link marking a node around the note standing in its place', () => {
|
||||
const linked = (stored: AdfNode, href: string, title?: string): AdfNode => ({ ...stored, marks: [{ attrs: title === undefined ? { href } : { href, title }, type: 'link' }, { type: 'strong' }] })
|
||||
assert.equal(portable(paragraph(text('a '), linked(node('mediaInline', { type: 'file' }), 'https://example.com/a'))), 'a [_(image not included)_](https://example.com/a)\n')
|
||||
assert.equal(portable(paragraph(linked(node('mediaInline', { type: 'file' }), 'https://example.com/a', 'T'))), '[_(image not included)_](https://example.com/a "T")\n')
|
||||
assert.equal(portable(paragraph(linked(node('inlineExtension', { extensionKey: 'macro' }), 'https://example.com/b'))), '[_(macro not included)_](https://example.com/b)\n')
|
||||
assert.equal(portable(paragraph(linked(node('mediaInline', { type: 'file' }), 'a|b'))), '[_(image not included)_](a|b)\n')
|
||||
assert.equal(portable(node('table', {}, node('tableRow', {}, node('tableCell', {}, paragraph(linked(node('mediaInline', { type: 'file' }), 'a|b')))))), '| [_(image not included)_](a%7Cb) |\n| --- |\n')
|
||||
})
|
||||
|
||||
test('spells an extension as its text attribute, else a note naming it, and a placeholder as nothing', () => {
|
||||
assert.equal(portable(node('extension', { extensionKey: 'toc', text: 'Contents' }), node('extension', { extensionKey: 'jira-issues-table' })), 'Contents\n\n_(jira-issues-table not included)_\n')
|
||||
assert.equal(portable(node('syncBlock', { resourceId: 'r' })), '_(synced block not included)_\n')
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import type { LineContainer } from '../line-container.ts'
|
||||
import type { MarkRun } from '../emit/line-escaping.ts'
|
||||
import type { RunGrouping } from '../emit/inline-runs.ts'
|
||||
import type { ShownNote } from '../../adf/shown-text.ts'
|
||||
import { codeTextMarkTypes } from '../../adf/inline-nodes.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { identicalMark, mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||
@@ -10,7 +11,7 @@ import { insideRun, runsSharedWithNext } from '../emit/inline-runs.ts'
|
||||
import { isEmphasis } from '../mark-spellings.ts'
|
||||
import { joinsWhenEditorNormal, sourceIndex, type SourceIndexes } from './editor-normal.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { partsShownText, referenceName, shownImage, shownLink, shownText, shownUrl } from '../../adf/shown-text.ts'
|
||||
import { partsShownText, shownImage, shownLink, shownNote, shownText, shownUrl } from '../../adf/shown-text.ts'
|
||||
import { portableLineFallback, settledLine, type LineJudgement, type PortableLineFallback } from '../emit/inline-line.ts'
|
||||
import { spellDestination } from '../commonmark/link-syntax.ts'
|
||||
|
||||
@@ -72,7 +73,7 @@ function nodeLeaves(node: AdfNode, container: LineContainer, place: ReductionPla
|
||||
if (node.type === 'hardBreak') return success([lineBreak(container)])
|
||||
if (['date', 'emoji', 'mention', 'status'].includes(node.type)) return success(textLeaves(shownText(node), marks, container))
|
||||
if (node.type === 'placeholder') return success([])
|
||||
if (['extension', 'inlineExtension', 'syncBlock'].includes(node.type)) return success(textLeaves(shownText(node), marks, container, referenceName(node)))
|
||||
if (['extension', 'inlineExtension', 'syncBlock'].includes(node.type)) return success(textLeaves(shownText(node), marks, container, shownNote(node)))
|
||||
if (['media', 'mediaInline'].includes(node.type)) return success(mediaLeaves(node, marks, container))
|
||||
if (['blockCard', 'embedCard', 'inlineCard'].includes(node.type)) return success(cardLeaves(node, marks, container))
|
||||
const own = textLeaves(node.text ?? shownText(node), marks, container)
|
||||
@@ -83,7 +84,7 @@ function nodeLeaves(node: AdfNode, container: LineContainer, place: ReductionPla
|
||||
|
||||
function cardLeaves(card: AdfNode, marks: readonly AdfMark[], container: LineContainer): AdfNode[] {
|
||||
const href = shownUrl(card)
|
||||
if (href === undefined) return textLeaves(shownText(card), marks, container, referenceName(card))
|
||||
if (href === undefined) return textLeaves(shownText(card), marks, container, shownNote(card))
|
||||
return linkedLeaves(shownText(card) ?? href, href, marks, container)
|
||||
}
|
||||
|
||||
@@ -94,22 +95,23 @@ function linkedLeaves(text: string, href: string, marks: readonly AdfMark[], con
|
||||
// An image standing inline is a link to it: the reader builds no node for an inline image, reading it as its alt text linked, and inside a link as the link's text, then (image).
|
||||
function mediaLeaves(media: AdfNode, marks: readonly AdfMark[], container: LineContainer): AdfNode[] {
|
||||
const image = shownImage(media, shownLink(media)?.href)
|
||||
if (image === undefined) return textLeaves(undefined, marks, container, referenceName(media))
|
||||
if (image === undefined) return textLeaves(undefined, marks, container, shownNote(media))
|
||||
const { after, link, text } = image
|
||||
const shown = link === undefined ? textLeaves(text, marks, container) : linkedLeaves(text, link, marks, container)
|
||||
return after === undefined ? shown : [...shown, ...imageAside((word) => linkedLeaves(word, after, marks, container))]
|
||||
}
|
||||
|
||||
function noteLeaves(name: string): AdfNode[] {
|
||||
return [textLeaf(`(${name} not included)`, [{ type: 'em' }])]
|
||||
function noteLeaves({ link, name }: ShownNote, container: LineContainer): AdfNode[] {
|
||||
const kept = link === undefined ? undefined : portableLink({ attrs: link, type: 'link' }, container)
|
||||
return [textLeaf(`(${name} not included)`, kept === undefined ? [{ type: 'em' }] : [kept, { type: 'em' }])]
|
||||
}
|
||||
|
||||
function lineBreak(container: LineContainer): AdfNode {
|
||||
return container === 'paragraph' ? { type: 'hardBreak' } : textLeaf(' ', [])
|
||||
}
|
||||
|
||||
function textLeaves(value: unknown, marks: readonly AdfMark[], container: LineContainer, note?: string): AdfNode[] {
|
||||
if (typeof value !== 'string') return note === undefined ? [] : noteLeaves(note)
|
||||
function textLeaves(value: unknown, marks: readonly AdfMark[], container: LineContainer, note?: ShownNote): AdfNode[] {
|
||||
if (typeof value !== 'string') return note === undefined ? [] : noteLeaves(note, container)
|
||||
const text = value.replace(/[\r\u0000]/g, '')
|
||||
const kept = portableMarks(marks, container, text)
|
||||
const leaves: AdfNode[] = []
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 153 | 0.4.0 | | question | **Decide whether `src/markdown/` root may import `emit/`, `parse/` or `portable/`, and hold the answer in `.oxlintrc.json` and §The source parts by ADF and format.** | 1 | 2 | 2 | 3 | 2 | code health by 3 | 26.2 |
|
||||
| 143 | 0.4.0 | | defect | **Keep, in `adfToPortableMarkdown`, the link around a note, so a linked stored image writes every target.** | 2 | 1 | 3 | 4 | 3 | 7 by 1 | 6.5 |
|
||||
| 169 | 0.4.0 | | owner | **Investigate `kovetskiy/mark` in depth and record in a report how its formats differ from ours, where the two projects overlap and how they compare.** | 1 | 1 | 1 | 1 | 3 | 3 by 4 | 6.3 |
|
||||
| 112 | 0.4.0 | | | **Pad a GFM pipe table's short rows with empty cells, read its body rows holding no pipe, and read a one-column table without leading pipes in `portableMarkdownToAdf`.** | 2 | 1 | 2 | 3 | 3 | 4 by 4 | 6.2 |
|
||||
| 80 | 0.4.0 | | | **Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document.** | 2 | 1 | 2 | 3 | 3 | 6 by 4 | 6.0 |
|
||||
@@ -64,19 +63,6 @@ a paragraph opening with a spellable `!adf:link` (§Nothing recurses unbounded).
|
||||
`src/markdown/**` bans only `html/`; banning `emit/`, `parse/` and `portable/` for the root's own
|
||||
files, with its `ci.sh` probe, extends the decision.
|
||||
|
||||
### 143. Keep, in `adfToPortableMarkdown`, the link around a note, so a linked stored image writes every target.
|
||||
|
||||
`noteLeaves` (`src/markdown/portable/inline-reduction.ts`) drops the node's marks, so a stored
|
||||
image a link marks writes `_(image not included)_` without the link's target, which Goal 7 counts as
|
||||
content. `adfToHtml`'s carry element keeps it, `<a href="…"><em>(image)</em></a>`.
|
||||
|
||||
Move the choice of where a note's link goes into `src/adf/shown-text.ts`, beside `shownImage`'s for
|
||||
an image, so both writers render one answer.
|
||||
|
||||
Leave what an HTML carry element shows unchanged, or ship the change as a breaking HTML change with
|
||||
a `MIGRATION.md` entry: `htmlToAdf` refuses any other content, so HTML stored by an earlier release
|
||||
would no longer read (`docs/decisions.md` §The formats are API).
|
||||
|
||||
### 169. Investigate `kovetskiy/mark` in depth and record in a report how its formats differ from ours, where the two projects overlap and how they compare.
|
||||
|
||||
`https://github.com/kovetskiy/mark` publishes markdown files as Confluence pages. Read its
|
||||
|
||||
Reference in New Issue
Block a user