mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 19:58:28 +01:00
Answer architecture round 1: one owner for the image's ADF shape, the readings' decision, marker paragraphs, the exceptions the README names; items 87 filed and 56 widened
This commit is contained in:
@@ -11,9 +11,10 @@ import { commonMarkToAdf, losslessMarkdownToAdf } from '../markdown/parse/markdo
|
||||
|
||||
const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'corpus', 'commonmark-spec')
|
||||
const checks = ['count', 'fixpoint', 'text'] as const
|
||||
const kinds = ['mark-model', 'node-model', 'pending'] as const
|
||||
|
||||
type Check = (typeof checks)[number]
|
||||
type ExceptionKind = 'mark-model' | 'node-model' | 'pending'
|
||||
type ExceptionKind = (typeof kinds)[number]
|
||||
|
||||
type SpecExample = { example: number; html: string; markdown: string; section: string }
|
||||
|
||||
@@ -30,7 +31,7 @@ function isCheck(value: unknown): value is Check {
|
||||
}
|
||||
|
||||
function isKind(value: unknown): value is ExceptionKind {
|
||||
return value === 'mark-model' || value === 'node-model' || value === 'pending'
|
||||
return kinds.some((kind) => kind === value)
|
||||
}
|
||||
|
||||
function isSpecExample(value: unknown): value is SpecExample {
|
||||
|
||||
@@ -1,26 +1,14 @@
|
||||
import type { AdfNode } from '../../adf/document.ts'
|
||||
import { holdsOnlyAttributes, nodeAttrs, nodeContent } from '../../adf/document.ts'
|
||||
import { centeredImage, externalMedia } from '../external-image.ts'
|
||||
import { nodeAttrs, nodeContent } from '../../adf/document.ts'
|
||||
import { serializeCanonicalJson } from '../../canonical-json.ts'
|
||||
import { tryImageLine } from './inline-line.ts'
|
||||
|
||||
const centeredMediaSingle = '{"layout":"center"}'
|
||||
const imageAttributes = ['alt', 'type', 'url']
|
||||
|
||||
// An empty `alt` takes the directive form: `` reads back with no `alt`.
|
||||
export function tryImage(node: AdfNode): string | undefined {
|
||||
const image = imageShape(node)
|
||||
if (image === undefined) return undefined
|
||||
return tryImageLine(image.alt, image.url)
|
||||
}
|
||||
|
||||
function imageShape(node: AdfNode): { alt: string | undefined; url: string } | undefined {
|
||||
const content = nodeContent(node)
|
||||
const media = content[0]
|
||||
if (!holdsOnlyAttributes(node, ['layout']) || serializeCanonicalJson(nodeAttrs(node), 'compact') !== centeredMediaSingle) return undefined
|
||||
if (media === undefined || content.length !== 1 || media.type !== 'media' || !holdsOnlyAttributes(media, imageAttributes) || nodeContent(media).length > 0) return undefined
|
||||
const attrs = nodeAttrs(media)
|
||||
const alt = attrs['alt']
|
||||
const url = attrs['url']
|
||||
if (attrs['type'] !== 'external' || typeof url !== 'string') return undefined
|
||||
if (alt !== undefined && (typeof alt !== 'string' || alt === '')) return undefined
|
||||
return { alt, url }
|
||||
const [media] = nodeContent(node)
|
||||
const { alt, url } = media === undefined ? {} : nodeAttrs(media)
|
||||
if (typeof url !== 'string' || (alt !== undefined && typeof alt !== 'string') || alt === '') return undefined
|
||||
const read = centeredImage([externalMedia(alt ?? '', url)])
|
||||
return serializeCanonicalJson(node, 'compact') === serializeCanonicalJson(read, 'compact') ? tryImageLine(alt, url) : undefined
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import type { AdfNode } from '../adf/document.ts'
|
||||
|
||||
// spec/flavour.md, The CommonMark image: the shape `` reads to, and the only one written back as it.
|
||||
export function centeredImage(content: AdfNode[]): AdfNode {
|
||||
return { attrs: { layout: 'center' }, content, type: 'mediaSingle' }
|
||||
}
|
||||
|
||||
export function externalMedia(alt: string, url: string): AdfNode {
|
||||
return { attrs: alt === '' ? { type: 'external', url } : { alt, type: 'external', url }, type: 'media' }
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import type { LineContainer } from '../line-container.ts'
|
||||
import type { LinkDefinition } from '../commonmark/link-syntax.ts'
|
||||
import { backslashEscape, decodeTextEscapes, inlineHtmlConstruct, readBracketedAutolink, readEmailAutolink, trimTrailingSpace } from '../commonmark/grammar.ts'
|
||||
import { backtickRun, closingBacktickRun } from '../commonmark/backtick-runs.ts'
|
||||
import { centeredImage, externalMedia } from '../external-image.ts'
|
||||
import { commonMarkLink, linkHref } from '../mark-spellings.ts'
|
||||
import { delimiterFlags, matchEmphasis, runLength } from '../commonmark/emphasis-matching.ts'
|
||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
@@ -309,11 +310,10 @@ function linkedAlt(piece: Piece): Piece {
|
||||
}
|
||||
|
||||
function mediaSingle({ alt, definition, link }: Image): AdfNode {
|
||||
const attrs = alt === '' ? { type: 'external', url: definition.destination } : { alt, type: 'external', url: definition.destination }
|
||||
const media: AdfNode = link === undefined ? { attrs, type: 'media' } : { attrs, marks: [linkMark(link)], type: 'media' }
|
||||
const media = externalMedia(alt, definition.destination)
|
||||
const { title } = definition
|
||||
const caption: AdfNode[] = title === undefined || title === '' ? [] : [{ content: [{ text: title, type: 'text' }], type: 'caption' }]
|
||||
return { attrs: { layout: 'center' }, content: [media, ...caption], type: 'mediaSingle' }
|
||||
return centeredImage([link === undefined ? media : { ...media, marks: [linkMark(link)] }, ...caption])
|
||||
}
|
||||
|
||||
function linkMark({ destination, title }: LinkDefinition): AdfMark {
|
||||
|
||||
@@ -1027,9 +1027,11 @@ test('reads an image no paragraph holds alone as its alt text, linked to its des
|
||||
assert.deepEqual(firstContent('# \n'), [marked('a', link('/u'))])
|
||||
assert.deepEqual(firstContent('!adf:paragraph {localId=p}\n\n!adf:/paragraph\n'), [marked('a', link('/u'))])
|
||||
assert.deepEqual(firstContent(' d\n'), [text('![a '), marked('b', link('/c')), text(' d')])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('![a]\n')), [paragraph('![a]')])
|
||||
assert.deepEqual(content(losslessMarkdownToAdf('a ! b\n')), [paragraph('a ! b')])
|
||||
})
|
||||
|
||||
test('reads an image beside other content in a link as its alt text, the link\'s own text', () => {
|
||||
test('reads an image inside a link not alone in its paragraph as plain alt text the link marks, dropping its own destination', () => {
|
||||
assert.deepEqual(firstContent('See [](/v).\n'), [text('See '), marked('a', link('/v')), text('.')])
|
||||
assert.deepEqual(firstContent('See [](/v).\n'), [text('See '), marked('/v', link('/v')), text('.')])
|
||||
assert.deepEqual(firstContent('[b ](/v)\n'), [marked('b a', link('/v'))])
|
||||
|
||||
@@ -21,9 +21,9 @@ import { spellStringAttribute, unsupportedNodeShape } from '../directive-syntax.
|
||||
import { spellsEmpty } from '../empty-keys.ts'
|
||||
|
||||
// A paragraph a marker opens: it shares its markdown paragraph with the marker, so no image there stands alone.
|
||||
type MarkedParagraph = { kind: 'markedParagraph'; position: SourcePosition; text: string }
|
||||
type MarkerParagraph = { kind: 'markerParagraph'; position: SourcePosition; text: string }
|
||||
|
||||
type ReadBlock = Block | MarkedParagraph
|
||||
type ReadBlock = Block | MarkerParagraph
|
||||
|
||||
// `inExpand` is whether an expand holds the blocks, which makes a folded callout a nestedExpand.
|
||||
type Reading = { claims: Claims; definitions: LinkDefinitions; inExpand: boolean; memo: SpellingMemo }
|
||||
@@ -123,7 +123,7 @@ function readBlock(block: ReadBlock, reading: Reading, path: ConvertErrorPath, d
|
||||
return contentNode({ attrs: { level: block.level }, type: 'heading' }, block.text, reading, path, 'heading')
|
||||
case 'html':
|
||||
return failure('unmappable-html', `no raw HTML converts at this version: ${block.construct}`, path)
|
||||
case 'markedParagraph':
|
||||
case 'markerParagraph':
|
||||
return contentNode({ type: 'paragraph' }, block.text, reading, path, 'paragraph')
|
||||
case 'orderedList':
|
||||
return listNode({ attrs: { order: block.start }, type: 'orderedList' }, block.items, reading, path, depth)
|
||||
@@ -149,8 +149,8 @@ function markerLine(text: string): { line: string; rest: string } {
|
||||
return { line: (hardBreak ? line.slice(0, -1) : line).replace(/^[ \t]+/, ''), rest: lineEnd === -1 ? '' : text.slice(lineEnd + 1) }
|
||||
}
|
||||
|
||||
function paragraphsOf(position: SourcePosition, ...texts: string[]): MarkedParagraph[] {
|
||||
return texts.filter((text) => text !== '').map((text) => ({ kind: 'markedParagraph', position, text }))
|
||||
function markerParagraphs(position: SourcePosition, ...texts: string[]): MarkerParagraph[] {
|
||||
return texts.filter((text) => text !== '').map((text) => ({ kind: 'markerParagraph', position, text }))
|
||||
}
|
||||
|
||||
function quoteNode(blocks: readonly Block[], reading: Reading, path: ConvertErrorPath, depth: number): Result<AdfNode> {
|
||||
@@ -159,12 +159,12 @@ function quoteNode(blocks: readonly Block[], reading: Reading, path: ConvertErro
|
||||
if (led === undefined) return containerNode({ type: 'blockquote' }, blocks, reading, path, depth)
|
||||
const { folded, panelType } = led.marker
|
||||
const { line, rest } = markerLine(led.text)
|
||||
if (!folded) return filledNode({ attrs: { panelType }, type: 'panel' }, readBlocks([...paragraphsOf(led.position, line, rest), ...body], reading, path, depth + 1))
|
||||
if (!folded) return filledNode({ attrs: { panelType }, type: 'panel' }, readBlocks([...markerParagraphs(led.position, line, rest), ...body], reading, path, depth + 1))
|
||||
const title = parseInlineContent(line, reading.definitions, path, 'paragraph', reading.claims)
|
||||
if (!title.ok) return title
|
||||
const text = titleText(title.value.nodes)
|
||||
const type = reading.inExpand ? 'nestedExpand' : 'expand'
|
||||
return filledNode(text === '' ? { type } : { attrs: { title: text }, type }, readBlocks([...paragraphsOf(led.position, rest), ...body], { ...reading, inExpand: true }, path, depth + 1))
|
||||
return filledNode(text === '' ? { type } : { attrs: { title: text }, type }, readBlocks([...markerParagraphs(led.position, rest), ...body], { ...reading, inExpand: true }, path, depth + 1))
|
||||
}
|
||||
|
||||
// docs/decisions.md, A callout title keeps its link targets.
|
||||
@@ -203,7 +203,7 @@ function bulletNode(items: readonly Block[][], reading: Reading, path: ConvertEr
|
||||
}
|
||||
const tasks: AdfNode[] = []
|
||||
for (const [index, { marker, others, position, text }] of led.entries()) {
|
||||
const read = readBlocks([...paragraphsOf(position, text.replace(/^(?:[ \t\n]|\\\n)+/, '')), ...others], reading, [...path, 'content', index], depth + 1)
|
||||
const read = readBlocks([...markerParagraphs(position, text.replace(/^(?:[ \t\n]|\\\n)+/, '')), ...others], reading, [...path, 'content', index], depth + 1)
|
||||
if (!read.ok) return read
|
||||
let beside = read.value.length
|
||||
while (read.value[beside - 1]?.type === 'taskList') beside -= 1
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
||||
import { adfDocumentFault, nodeAttrs, nodeContent } from '../../adf/document.ts'
|
||||
import { blockNodeModel } from '../../adf/block-nodes.ts'
|
||||
import { centeredImage, externalMedia } from '../external-image.ts'
|
||||
import { commonMarkSpelling, largestListMarker, writeMarkdown, type SpellingMemo } from '../emit/adf-to-markdown.ts'
|
||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||
import { flavourClaims, taskMarker } from './conventions.ts'
|
||||
@@ -405,7 +406,5 @@ function reduceMedia(media: AdfNode, reduction: Reduction): Result<AdfNode[]> {
|
||||
if (attrs['type'] !== 'external' || typeof url !== 'string') return paragraphOfNode(media, reduction)
|
||||
const held = attrs['alt']
|
||||
const alt = typeof held === 'string' ? oneLine(held).trim() : ''
|
||||
const external: AdfNode = { attrs: alt === '' ? { type: 'external', url: writableHref(url) } : { alt, type: 'external', url: writableHref(url) }, type: 'media' }
|
||||
const image: AdfNode = { attrs: { layout: 'center' }, content: [external], type: 'mediaSingle' }
|
||||
return success([image])
|
||||
return success([centeredImage([externalMedia(alt, writableHref(url))])])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user