mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-13 12:18:28 +01:00
Answer architecture round 1: the portable writer builds the linked image through externalMedia, the properties generate the image shapes, linkDefinition sits beside linkMark, the changelog drops a refusal no release met, and items 99 and 100 file the inline linked image and the caption reading
This commit is contained in:
@@ -6,8 +6,10 @@ import type { AdfAttributes, AdfDocument, AdfMark, AdfNode, EmptyKey } from '../
|
||||
import type { Arbitrary } from 'fast-check'
|
||||
import type { AttributeKind, AttributeVocabulary } from '../adf/attribute-vocabulary.ts'
|
||||
import type { JsonValue } from '../json-value.ts'
|
||||
import type { LinkDefinition } from '../markdown/commonmark/link-syntax.ts'
|
||||
import { blockArgument } from '../markdown/block-directive.ts'
|
||||
import { blockNodes } from '../adf/block-nodes.ts'
|
||||
import { centeredImage, externalMedia } from '../markdown/external-image.ts'
|
||||
import { directivePrefix } from '../markdown/directive-syntax.ts'
|
||||
import { emptyKeys, mergeAdjacentText, nodeContent } from '../adf/document.ts'
|
||||
import { inlineNodes } from '../adf/inline-nodes.ts'
|
||||
@@ -91,6 +93,10 @@ function occasionallyApart(arbitrary: Arbitrary<AdfNode[]>): Arbitrary<AdfNode[]
|
||||
return fc.tuple(arbitrary, fc.nat({ max: 5 })).map(([nodes, roll]) => (roll === 0 ? nodes : mergeAdjacentText(nodes, joinsWhenRead)))
|
||||
}
|
||||
|
||||
function externalImage({ alt, link, url }: { alt: string | undefined; link: LinkDefinition | undefined; url: string }): AdfNode {
|
||||
return centeredImage([externalMedia(alt ?? '', url, link)])
|
||||
}
|
||||
|
||||
function pipeTable({ body, header }: { body: AdfNode[][]; header: AdfNode[] }): AdfNode {
|
||||
const rows = [header, ...body.map((cells) => header.map((_, index) => cells[index] ?? emptyCell))]
|
||||
return { content: rows.map((content): AdfNode => ({ content, type: 'tableRow' })), type: 'table' }
|
||||
@@ -155,6 +161,7 @@ const positions = fc.letrec<Positions>((tie) => {
|
||||
occasionallyEmpty(fc.record({ content: inlineContent, level: fc.integer({ max: 6, min: 1 }) }).map(({ content, level }): AdfNode => ({ attrs: { level }, content, type: 'heading' }))),
|
||||
paragraph,
|
||||
fc.record({ body: fc.array(fc.array(cell('tableCell'), { maxLength: 3 }), { maxLength: 2 }), header: fc.array(cell('tableHeader'), { maxLength: 3, minLength: 1 }) }).map(pipeTable),
|
||||
fc.record({ alt: fc.option(text, { nil: undefined }), link: fc.option(fc.record({ destination: text, title: text }, { requiredKeys: ['destination'] }), { nil: undefined }), url: text }).map(externalImage),
|
||||
]
|
||||
const task = (type: string, content: Arbitrary<AdfNode[]>) =>
|
||||
occasionallyEmpty(fc.record({ content, state: fc.constantFrom('DONE', 'TODO') }).map(({ content: held, state }): AdfNode => ({ attrs: { state }, content: held, type })))
|
||||
|
||||
@@ -644,6 +644,7 @@ test('spells a media only a link marks as the image the link wraps', () => {
|
||||
assert.ok(fallback(linked('m', link({ href: '/d' }), { attrs: { color: '#000', size: 1 }, type: 'border' })))
|
||||
assert.ok(fallback(linked('m', link({ href: 'a\\b' }))))
|
||||
assert.ok(fallback(linked('m', link({ title: 't' }))))
|
||||
assert.ok(fallback(linked('m', link({ href: '/d', title: 4 }))))
|
||||
const captioned: AdfNode = { attrs: { layout: 'center' }, content: [{ attrs: { alt: 'm', type: 'external', url }, type: 'media' }, { content: [{ text: 'c', type: 'text' }], type: 'caption' }], type: 'mediaSingle' }
|
||||
assert.ok(fallback(document(captioned)))
|
||||
})
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AdfNode } from '../../adf/document.ts'
|
||||
import type { LinkDefinition } from '../commonmark/link-syntax.ts'
|
||||
import { centeredImage, externalMedia } from '../external-image.ts'
|
||||
import { linkDefinition } from '../mark-spellings.ts'
|
||||
import { nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||
import { serializeCanonicalJson } from '../../canonical-json.ts'
|
||||
import { tryImageLine } from './inline-line.ts'
|
||||
@@ -16,9 +16,3 @@ export function tryImage(node: AdfNode): string | undefined {
|
||||
const read = centeredImage([externalMedia(alt ?? '', url, link)])
|
||||
return serializeCanonicalJson(node, 'compact') === serializeCanonicalJson(read, 'compact') ? tryImageLine(alt, url, link) : undefined
|
||||
}
|
||||
|
||||
function linkDefinition({ href, title }: Readonly<Record<string, unknown>>): LinkDefinition | undefined {
|
||||
if (typeof href !== 'string') return undefined
|
||||
if (title === undefined) return { destination: href }
|
||||
return typeof title === 'string' ? { destination: href, title } : undefined
|
||||
}
|
||||
|
||||
@@ -69,16 +69,15 @@ export function tryPipeCell(nodes: readonly AdfNode[], path: ConvertErrorPath, f
|
||||
}
|
||||
|
||||
// `link` wraps the image as the link's whole text: `[](href)`.
|
||||
export function tryImageLine(alt: string | undefined, href: string, link?: LinkDefinition): string | undefined {
|
||||
export function tryImageLine(alt: string | undefined, url: string, link?: LinkDefinition): string | undefined {
|
||||
if (alt !== undefined && (/^[ \t]|[ \t]$|[\n\r]/.test(alt) || holdsNullCharacter(alt))) return undefined
|
||||
const destination = spellDestination(href)
|
||||
const destination = spellDestination(url)
|
||||
if (destination === undefined) return undefined
|
||||
const description: InlineSegment[] = alt === undefined ? [] : [{ escaping: 'bracketed', text: alt }]
|
||||
if (link === undefined) return attemptLine([syntax('`)], 'paragraph', 'lossless').line
|
||||
const target = spellLinkTarget(link.destination, link.title)
|
||||
if (target === undefined) return undefined
|
||||
const image = [syntax('[, { escaping: 'bracketed-link-target', text: escapeUnbalanced(destination, '[', ']') } satisfies InlineSegment, syntax(')')]
|
||||
return attemptLine([...image, syntax(`](${target})`)], 'paragraph', 'lossless').line
|
||||
return attemptLine([syntax('[, bracketedLinkTarget(destination), syntax(`)](${target})`)], 'paragraph', 'lossless').line
|
||||
}
|
||||
|
||||
function emitLine(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath, flavour: WrittenFlavour): Result<EmittedLine> {
|
||||
@@ -170,6 +169,10 @@ function syntax(text: string): InlineSegment {
|
||||
return { escaping: 'none', text }
|
||||
}
|
||||
|
||||
function bracketedLinkTarget(target: string): InlineSegment {
|
||||
return { escaping: 'bracketed-link-target', text: escapeUnbalanced(target, '[', ']') }
|
||||
}
|
||||
|
||||
function refuseContentAndText(node: AdfNode, path: ConvertErrorPath): Result<null> {
|
||||
const holdsContent = nodeContent(node).length > 0
|
||||
if (holdsContent || node.text !== undefined) {
|
||||
@@ -344,7 +347,7 @@ function tryLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range:
|
||||
const inner = emitRun(nodes, depth + 1, range.first, { ...context, bracketed: true })
|
||||
if (!inner.ok) return inner
|
||||
if (inner.value.carry !== undefined) return inner
|
||||
const target: InlineSegment = context.bracketed ? { escaping: 'bracketed-link-target', text: escapeUnbalanced(commonMark.target, '[', ']') } : syntax(commonMark.target)
|
||||
const target = context.bracketed ? bracketedLinkTarget(commonMark.target) : syntax(commonMark.target)
|
||||
return success({ segments: [{ escaping: 'none', nodes: range, text: '[' }, ...inner.value.segments, syntax(']('), target, syntax(')')] })
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AdfAttributes, AdfMark, AdfNode } from '../adf/document.ts'
|
||||
import type { AttributeVocabulary } from '../adf/attribute-vocabulary.ts'
|
||||
import type { MarkType } from '../adf/mark-attributes.ts'
|
||||
import type { LinkDefinition } from './commonmark/link-syntax.ts'
|
||||
import type { MarkType } from '../adf/mark-attributes.ts'
|
||||
import { escapeUnbalanced, spellLinkTarget } from './commonmark/link-syntax.ts'
|
||||
import { holdsDirectivePrefix, spellAttributes, spellVocabulary } from './directive-syntax.ts'
|
||||
import { holdsEntityReference } from './commonmark/entity-references.ts'
|
||||
@@ -41,6 +41,14 @@ export function linkMark({ destination, title }: LinkDefinition): AdfMark {
|
||||
return { attrs: title === undefined ? { href: destination } : { href: destination, title }, type: 'link' }
|
||||
}
|
||||
|
||||
export function linkDefinition(attrs: AdfAttributes): LinkDefinition | undefined {
|
||||
const destination = linkHref(attrs)
|
||||
const title = attrs['title']
|
||||
if (destination === undefined) return undefined
|
||||
if (title === undefined) return { destination }
|
||||
return typeof title === 'string' ? { destination, title } : undefined
|
||||
}
|
||||
|
||||
export function linkHref(attrs: AdfAttributes): string | undefined {
|
||||
const href = attrs['href']
|
||||
return typeof href === 'string' ? href : undefined
|
||||
|
||||
@@ -8,6 +8,7 @@ import { flavourClaims, taskMarker } from './conventions.ts'
|
||||
import { inlineLeaves, isBlockNodeType, oneLine, portableLink, reduceInline, writableHref } from './inline-reduction.ts'
|
||||
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||
import { languageSlot } from '../code-language.ts'
|
||||
import { linkDefinition } from '../mark-spellings.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { splitTaskList } from './task-list.ts'
|
||||
import { toEditorNormal } from './editor-normal.ts'
|
||||
@@ -350,8 +351,7 @@ 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 image = externalMedia(alt, writableHref(url))
|
||||
const mark = nodeMarks(media).find((held) => held.type === 'link')
|
||||
const link = mark === undefined ? undefined : portableLink(mark, 'paragraph')
|
||||
return success([centeredImage([link === undefined ? image : { ...image, marks: [link] }])])
|
||||
return success([centeredImage([externalMedia(alt, writableHref(url), link === undefined ? undefined : linkDefinition(nodeAttrs(link)))])])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user