3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-13 04:08:29 +01:00

Answer architecture round 4: 0 🔴, 1 🟡 (0 misread); one image rule in shown-text for both writers, a link mark's href kept past a bad title, the card rule recorded

This commit is contained in:
2026-10-24 11:58:23 +02:00
parent 76d524298f
commit 0373b7bcf8
10 changed files with 115 additions and 50 deletions
+35 -1
View File
@@ -1,7 +1,11 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { isBlockNodeType, shownText, shownUrl } from './shown-text.ts'
import type { AdfAttributes, AdfNode } from './document.ts'
import { isBlockNodeType, shownImage, shownLink, shownText, shownUrl } from './shown-text.ts'
const moon = 'https://example.com/moon.png'
const target = 'https://example.com/'
test('shows a date as its UTC day, one before 1970 included', () => {
assert.equal(shownText({ attrs: { timestamp: '1700000000000' }, type: 'date' }), '2023-11-14')
@@ -28,6 +32,36 @@ test('links an external image to its URL, and a stored one to nothing', () => {
assert.equal(shownUrl({ attrs: { type: 'file', url: 'https://example.com/a.png' }, type: 'mediaInline' }), undefined)
})
test('shows an image no link marks by its alt, else its URL, linked to its URL', () => {
assert.deepEqual(shownImage({ attrs: { alt: 'The moon', type: 'external', url: moon }, type: 'media' }, undefined), { after: undefined, link: moon, text: 'The moon' })
assert.deepEqual(shownImage({ attrs: { alt: ' ', type: 'external', url: moon }, type: 'mediaInline' }, undefined), { after: undefined, link: moon, text: moon })
})
test('shows an image a link to its own URL marks by the same text, under that link', () => {
assert.deepEqual(shownImage({ attrs: { alt: 'The moon', type: 'external', url: moon }, type: 'media' }, moon), { after: undefined, link: undefined, text: 'The moon' })
assert.deepEqual(shownImage({ attrs: { type: 'external', url: moon }, type: 'mediaInline' }, moon), { after: undefined, link: undefined, text: moon })
})
test('shows an image a link to another target marks by its alt, else the target, else its URL, under the link, its URL after it', () => {
assert.deepEqual(shownImage({ attrs: { alt: 'The moon', type: 'external', url: moon }, type: 'media' }, target), { after: moon, link: undefined, text: 'The moon' })
assert.deepEqual(shownImage({ attrs: { type: 'external', url: moon }, type: 'mediaInline' }, target), { after: moon, link: undefined, text: target })
assert.deepEqual(shownImage({ attrs: { type: 'external', url: moon }, type: 'media' }, ' '), { after: moon, link: undefined, text: moon })
})
test('shows an image with no URL by its alt, else by no text, a note standing in its place', () => {
assert.deepEqual(shownImage({ attrs: { alt: 'The moon', type: 'file' }, type: 'media' }, target), { after: undefined, link: undefined, text: 'The moon' })
assert.deepEqual(shownImage({ attrs: { type: 'file' }, type: 'mediaInline' }, undefined), { after: undefined, link: undefined, text: undefined })
assert.deepEqual(shownImage({ attrs: { alt: 'The moon', type: 'external', url: ' ' }, type: 'media' }, undefined), { after: undefined, link: undefined, text: 'The moon' })
})
test('follows a link mark holding a string href, keeping its title only where that is a string', () => {
const linked = (attrs: AdfAttributes): AdfNode => ({ marks: [{ type: 'strong' }, { attrs, type: 'link' }], text: 'x', type: 'text' })
assert.deepEqual(shownLink(linked({ href: target, title: 'Home' })), { href: target, title: 'Home' })
assert.deepEqual(shownLink(linked({ href: target, title: 5 })), { href: target })
assert.equal(shownLink(linked({ title: 'Home' })), undefined)
assert.equal(shownLink({ text: 'x', type: 'text' }), undefined)
})
test('counts a card standing apart as a block, as a block node is', () => {
assert.equal(isBlockNodeType('blockCard'), true)
assert.equal(isBlockNodeType('embedCard'), true)
+20 -5
View File
@@ -1,7 +1,8 @@
import type { AdfNode } from './document.ts'
import type { JsonValue } from '../json-value.ts'
import type { LinkAttrs } from './mark-attributes.ts'
import { blockNodeModel } from './block-nodes.ts'
import { nodeAttrs } from './document.ts'
import { nodeAttrs, nodeMarks } from './document.ts'
export function isBlockNodeType(type: string): boolean {
return blockNodeModel(type) !== undefined || type === 'blockCard' || type === 'embedCard'
@@ -38,7 +39,24 @@ export function referenceName(node: AdfNode): string | undefined {
}
}
// The text a node shows in place of itself, read from the attribute holding its content; `undefined` where it holds none.
// What an image shows in place of itself, `href` the target of the link marking it, if any: `text`, else a note; `link`, its URL, which `text` links to where no link marks it; `after`, its URL, shown after a link going elsewhere.
export function shownImage(media: AdfNode, href: string | undefined): { after: string | undefined; link: string | undefined; text: string | undefined } {
const alt = visibleText(nodeAttrs(media)['alt'])
const url = shownUrl(media)
if (url === undefined) return { after: undefined, link: undefined, text: alt }
if (href === undefined) return { after: undefined, link: url, text: alt ?? url }
if (href === url) return { after: undefined, link: undefined, text: alt ?? url }
return { after: url, link: undefined, text: alt ?? visibleText(href) ?? url }
}
// The link a node's first link mark makes: its `href` where that is a string, and its `title` only where that is one.
export function shownLink(node: AdfNode): LinkAttrs | undefined {
const { href, title } = nodeAttrs(nodeMarks(node).find((mark) => mark.type === 'link') ?? {})
if (typeof href !== 'string') return undefined
return typeof title === 'string' ? { href, title } : { href }
}
// The text a node shows in place of itself, read from the attribute holding its content; `undefined` where it holds none, and for an image, which `shownImage` answers.
export function shownText(node: AdfNode): string | undefined {
const attrs = nodeAttrs(node)
switch (node.type) {
@@ -56,9 +74,6 @@ export function shownText(node: AdfNode): string | undefined {
case 'extension':
case 'inlineExtension':
return visibleText(attrs['text'])
case 'media':
case 'mediaInline':
return visibleText(attrs['alt']) ?? shownUrl(node)
case 'mention': {
const id = attrs['id']
return visibleText(attrs['text']) ?? (typeof id === 'string' && id !== '' ? `@${id}` : undefined)
+7 -3
View File
@@ -149,11 +149,12 @@ test('shows a carried node\'s content after a block it holds as a paragraph of i
assert.equal(shown(html(adfToHtml(document({ content: [paragraph(text('a')), text('b')], type: 'panel' })))), '<p>a</p><p>b</p>')
})
test('shows text a link marks in a carried node as a link', () => {
test('shows text a link marks in a carried node as a link, leaving out a title that is no string', () => {
const link: AdfMark = { attrs: { href: 'https://spec.commonmark.org/', title: 'The spec' }, type: 'link' }
const panel: AdfNode = { content: [paragraph(text('Read the '), text('CommonMark spec', link), text(' first.'))], type: 'panel' }
assert.equal(shown(html(adfToHtml(document(panel)))), '<p>Read the <a href="https://spec.commonmark.org/" title="The spec">CommonMark spec</a> first.</p>')
assert.equal(shown(html(adfToHtml(document(paragraph(text('x', { type: 'underline' }, link)))))), '<a href="https://spec.commonmark.org/" title="The spec">x</a>')
assert.equal(shown(html(adfToHtml(document(paragraph(text('x', { attrs: { href: 'https://spec.commonmark.org/', title: 5 }, type: 'link' })))))), '<a href="https://spec.commonmark.org/">x</a>')
})
test('shows a hard break, and a line feed in carried text outside a code block, as a line break', () => {
@@ -219,8 +220,11 @@ test('shows a carried image marked by a link to another target as that link, the
assert.equal(image({ alt: 'The moon', type: 'external', url: 'https://example.com/' }), '<p><a href="https://example.com/">The moon</a></p>')
const blank = shown(html(adfToHtml(document({ attrs: { layout: 'wide' }, content: [{ attrs: { type: 'external', url: 'https://example.com/moon.jpg' }, marks: [{ attrs: { href: ' ' }, type: 'link' }], type: 'media' }], type: 'mediaSingle' }))))
assert.equal(blank, '<p><a href=" ">https://example.com/moon.jpg</a> (<a href="https://example.com/moon.jpg">image</a>)</p>')
const inline: AdfNode = { attrs: { alt: 'The moon', type: 'external', url: 'https://example.com/moon.jpg' }, marks: [link], type: 'mediaInline' }
assert.equal(shown(html(adfToHtml(document(paragraph(inline))))), '<a href="https://example.com/">The moon</a> (<a href="https://example.com/moon.jpg">image</a>)')
const texted = shown(html(adfToHtml(document({ attrs: { layout: 'wide' }, content: [{ attrs: { alt: 'The moon', type: 'external', url: 'https://example.com/moon.jpg' }, marks: [link], text: 'Moon', type: 'media' }], type: 'mediaSingle' }))))
assert.equal(texted, '<p><a href="https://example.com/">Moon</a> (<a href="https://example.com/moon.jpg">image</a>)</p>')
const inline = (mark: AdfMark): string => shown(html(adfToHtml(document(paragraph({ attrs: { alt: 'The moon', type: 'external', url: 'https://example.com/moon.jpg' }, marks: [mark], type: 'mediaInline' })))))
assert.equal(inline(link), '<a href="https://example.com/">The moon</a> (<a href="https://example.com/moon.jpg">image</a>)')
assert.equal(inline({ attrs: { href: 'https://example.com/', title: 5 }, type: 'link' }), '<a href="https://example.com/">The moon</a> (<a href="https://example.com/moon.jpg">image</a>)')
})
test('links a carried card a link marks to that link\'s target alone', () => {
+17 -15
View File
@@ -1,10 +1,10 @@
import type { AdfNode } from '../../adf/document.ts'
import type { LinkAttrs } from '../../adf/mark-attributes.ts'
import { anchorOpener, jsonValue, languageClass, shownValue } from './escaping.ts'
import { carriedJson } from '../../adf/carried-json.ts'
import { largestNesting } from '../../nesting.ts'
import { linkAttrs, type LinkAttrs } from '../../adf/mark-attributes.ts'
import { nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
import { partsShownText, referenceName, shownText, shownUrl, visibleText } from '../../adf/shown-text.ts'
import { nodeAttrs, nodeContent } from '../../adf/document.ts'
import { partsShownText, referenceName, shownImage, shownLink, shownText, shownUrl } from '../../adf/shown-text.ts'
import { success, type ConvertErrorPath, type Result } from '../../result.ts'
// The content a carry element shows of one block; `code` the code block holding it, if any.
@@ -51,23 +51,25 @@ function shownBlocks(root: AdfNode, codeLineFeed: string): ShownBlock[] {
// The node's own part of what its carry element shows.
function shownPiece(node: AdfNode, lineFeed: string): string {
if (node.type === 'hardBreak') return '<br>'
const mark = nodeMarks(node).find((held) => held.type === 'link')
const target = mark === undefined ? undefined : linkAttrs(nodeAttrs(mark))
const url = shownUrl(node)
if (['media', 'mediaInline'].includes(node.type) && target !== undefined && url !== undefined && target.href !== url) return linkedImage(node, target, url, lineFeed)
const link = target ?? (url === undefined ? undefined : { href: url })
const text = node.text ?? shownText(node)
if (['media', 'mediaInline'].includes(node.type)) return linkedImage(node, lineFeed)
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).
function linkedImage(media: AdfNode, lineFeed: string): string {
const { after, link, text } = shownImage(media, shownLink(media)?.href)
const shown = linkedText(media, text, link, lineFeed)
return after === undefined ? shown : `${shown} (${linked('image', { href: after })})`
}
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)
}
// As portable markdown writes it inline (docs/decisions.md §The HTML dialect).
function linkedImage(media: AdfNode, target: LinkAttrs, url: string, lineFeed: string): string {
const text = visibleText(nodeAttrs(media)['alt']) ?? visibleText(target.href) ?? url
return `${linked(shownValue(text, lineFeed), target)} (${linked('image', { href: url })})`
}
// The shown HTML inside an `<a>` where it links and HTML can hold the link.
function linked(shown: string, link: LinkAttrs | undefined): string {
const anchor = link === undefined ? undefined : anchorOpener(link)
+5 -10
View File
@@ -7,7 +7,7 @@ import { imageAside } from '../image-aside.ts'
import { joinsWhenEditorNormal, sameMarkWhenEditorNormal } from './editor-normal.ts'
import { largestNesting } from '../../nesting.ts'
import { mergeAdjacentText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
import { partsShownText, referenceName, shownText, shownUrl, visibleText } from '../../adf/shown-text.ts'
import { partsShownText, referenceName, shownImage, shownLink, shownText, shownUrl } from '../../adf/shown-text.ts'
import { portableLineFallback, type PortableLineFallback } from '../emit/inline-line.ts'
import { spellDestination } from '../commonmark/link-syntax.ts'
@@ -59,20 +59,15 @@ function cardLeaves(card: AdfNode, marks: readonly AdfMark[], container: LineCon
return linkedLeaves(shownText(card) ?? href, href, marks, container)
}
function linkedLeaves(text: string, href: string, marks: readonly AdfMark[], container: LineContainer): AdfNode[] {
function linkedLeaves(text: string | undefined, href: string, marks: readonly AdfMark[], container: LineContainer): AdfNode[] {
return textLeaves(text, [...marks.filter((mark) => mark.type !== 'link'), { attrs: { href }, type: 'link' }], container)
}
// 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 alt = visibleText(nodeAttrs(media)['alt'])
const url = shownUrl(media)
if (url === undefined) return textLeaves(alt, marks, container, referenceName(media))
const link = marks.find((mark) => mark.type === 'link')
const href = link === undefined ? undefined : nodeAttrs(link)['href']
if (typeof href !== 'string') return linkedLeaves(alt ?? url, url, marks, container)
if (href === url) return textLeaves(alt ?? url, marks, container)
return [...textLeaves(alt ?? visibleText(href) ?? url, marks, container), ...imageAside((text) => linkedLeaves(text, url, marks, container))]
const { after, link, text } = shownImage(media, shownLink(media)?.href)
const shown = link === undefined ? textLeaves(text, marks, container, referenceName(media)) : linkedLeaves(text, link, marks, container)
return after === undefined ? shown : [...shown, ...imageAside((word) => linkedLeaves(word, after, marks, container))]
}
function noteLeaves(name: string): AdfNode[] {