4.1: editor-normal and the node accessors #69
+21
-9
@@ -52,8 +52,8 @@ export function attributeNestingMessage(key: string, type: string, levels: numbe
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function carriesOnly(node: AdfNode, attributes: readonly string[]): boolean {
|
export function carriesOnly(node: AdfNode, attributes: readonly string[]): boolean {
|
||||||
if ((node.marks ?? []).length > 0 || node.text !== undefined) return false
|
if (nodeMarks(node).length > 0 || node.text !== undefined) return false
|
||||||
return holdsOnly(node.attrs ?? {}, attributes)
|
return holdsOnly(nodeAttrs(node), attributes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Depth is the walks' business, not the shape's: the guard waves a deep document through as blocks and marks do.
|
// Depth is the walks' business, not the shape's: the guard waves a deep document through as blocks and marks do.
|
||||||
@@ -72,6 +72,18 @@ export function isAdfMark(value: unknown): value is AdfMark {
|
|||||||
return !('attrs' in value) || isAttributes(value['attrs'])
|
return !('attrs' in value) || isAttributes(value['attrs'])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function nodeAttrs(node: { attrs?: AdfAttributes }): AdfAttributes {
|
||||||
|
return node.attrs ?? {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nodeContent(node: { content?: AdfNode[] }): readonly AdfNode[] {
|
||||||
|
return node.content ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
export function nodeMarks(node: { marks?: AdfMark[] }): readonly AdfMark[] {
|
||||||
|
return node.marks ?? []
|
||||||
|
}
|
||||||
|
|
||||||
function isNodeArray(value: readonly unknown[]): value is readonly AdfNode[] {
|
function isNodeArray(value: readonly unknown[]): value is readonly AdfNode[] {
|
||||||
const pending: unknown[] = [...value]
|
const pending: unknown[] = [...value]
|
||||||
while (pending.length > 0) {
|
while (pending.length > 0) {
|
||||||
@@ -95,23 +107,23 @@ function nestingFault(nodes: readonly AdfNode[]): ConvertFault | undefined {
|
|||||||
while (pending.length > 0) {
|
while (pending.length > 0) {
|
||||||
const node = pending.pop()
|
const node = pending.pop()
|
||||||
if (node === undefined) continue
|
if (node === undefined) continue
|
||||||
const fault = attributesFault(node.attrs, node.type) ?? marksFault(node.marks)
|
const fault = attributesFault(nodeAttrs(node), node.type) ?? marksFault(nodeMarks(node))
|
||||||
if (fault !== undefined) return fault
|
if (fault !== undefined) return fault
|
||||||
pending.push(...(node.content ?? []))
|
pending.push(...nodeContent(node))
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function marksFault(marks: readonly AdfMark[] | undefined): ConvertFault | undefined {
|
function marksFault(marks: readonly AdfMark[]): ConvertFault | undefined {
|
||||||
for (const mark of marks ?? []) {
|
for (const mark of marks) {
|
||||||
const fault = attributesFault(mark.attrs, mark.type, markAttributeNesting)
|
const fault = attributesFault(nodeAttrs(mark), mark.type, markAttributeNesting)
|
||||||
if (fault !== undefined) return fault
|
if (fault !== undefined) return fault
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
function attributesFault(attrs: AdfAttributes | undefined, type: string, levels: number = largestNesting): ConvertFault | undefined {
|
function attributesFault(attrs: AdfAttributes, type: string, levels: number = largestNesting): ConvertFault | undefined {
|
||||||
for (const [key, value] of Object.entries(attrs ?? {})) {
|
for (const [key, value] of Object.entries(attrs)) {
|
||||||
if (overNested(value, levels)) return { code: 'unsupported-nesting-depth', message: attributeNestingMessage(key, type, levels) }
|
if (overNested(value, levels)) return { code: 'unsupported-nesting-depth', message: attributeNestingMessage(key, type, levels) }
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import type { AdfNode } from './document.ts'
|
||||||
|
import { toEditorNormal } from './editor-normal.ts'
|
||||||
|
|
||||||
|
test('merges adjacent text nodes carrying identical marks, at every level', () => {
|
||||||
|
const content: AdfNode[] = [
|
||||||
|
{ text: 'a', type: 'text' },
|
||||||
|
{ text: 'b', type: 'text' },
|
||||||
|
{ marks: [{ type: 'strong' }], text: 'c', type: 'text' },
|
||||||
|
{ marks: [{ attrs: {}, type: 'strong' }], text: 'd', type: 'text' },
|
||||||
|
{ type: 'hardBreak' },
|
||||||
|
{ text: 'e', type: 'text' },
|
||||||
|
{ attrs: { localId: '01a0a06b-5281-7f27-9022-8d3a74b0ab0d' }, text: 'f', type: 'text' },
|
||||||
|
{ text: 'g', type: 'text' },
|
||||||
|
]
|
||||||
|
assert.deepEqual(toEditorNormal({ content: [{ attrs: { panelType: 'info' }, content: [{ content, type: 'paragraph' }], type: 'panel' }], type: 'doc', version: 1 }), {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
attrs: { panelType: 'info' },
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
content: [
|
||||||
|
{ text: 'ab', type: 'text' },
|
||||||
|
{ marks: [{ type: 'strong' }], text: 'cd', type: 'text' },
|
||||||
|
{ type: 'hardBreak' },
|
||||||
|
{ text: 'e', type: 'text' },
|
||||||
|
{ attrs: { localId: '01a0a06b-5281-7f27-9022-8d3a74b0ab0d' }, text: 'f', type: 'text' },
|
||||||
|
{ text: 'g', type: 'text' },
|
||||||
|
],
|
||||||
|
type: 'paragraph',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'panel',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
type: 'doc',
|
||||||
|
version: 1,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('reads negative zero as zero, as JSON does', () => {
|
||||||
|
const marks = [{ attrs: { size: -0 }, type: 'border' }]
|
||||||
|
assert.deepEqual(
|
||||||
|
toEditorNormal({ content: [{ attrs: { a: -0, b: [{ c: -0 }, null, 'd'] }, content: [{ marks, text: 'x', type: 'text' }], type: 'paragraph' }], type: 'doc', version: -0 }),
|
||||||
|
{ content: [{ attrs: { a: 0, b: [{ c: 0 }, null, 'd'] }, content: [{ marks: [{ attrs: { size: 0 }, type: 'border' }], text: 'x', type: 'text' }], type: 'paragraph' }], type: 'doc', version: 0 },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('reads an empty attrs object, marks array or content array as the absent key', () => {
|
||||||
|
const paragraph: AdfNode = { attrs: {}, content: [{ attrs: {}, marks: [], text: 'a', type: 'text' }, { marks: [{ attrs: {}, type: 'em' }], text: 'b', type: 'text' }], marks: [], type: 'paragraph' }
|
||||||
|
assert.deepEqual(toEditorNormal({ content: [paragraph, { content: [], type: 'rule' }], type: 'doc', version: 1 }), {
|
||||||
|
content: [{ content: [{ text: 'a', type: 'text' }, { marks: [{ type: 'em' }], text: 'b', type: 'text' }], type: 'paragraph' }, { type: 'rule' }],
|
||||||
|
type: 'doc',
|
||||||
|
version: 1,
|
||||||
|
})
|
||||||
|
assert.deepEqual(toEditorNormal({ content: [], type: 'doc', version: 1 }), { type: 'doc', version: 1 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('normalizes a document nesting far past the levels a recursive walk survives', () => {
|
||||||
|
const levels = 100000
|
||||||
|
let node: AdfNode = { content: [], type: 'paragraph' }
|
||||||
|
for (let level = 0; level < levels; level += 1) node = { content: [node], type: 'blockquote' }
|
||||||
|
let normal = toEditorNormal({ content: [node], type: 'doc', version: 1 }).content?.[0]
|
||||||
|
let depth = 0
|
||||||
|
for (; normal?.content !== undefined; depth += 1) normal = normal.content[0]
|
||||||
|
assert.equal(depth, levels)
|
||||||
|
assert.deepEqual(normal, { type: 'paragraph' })
|
||||||
|
})
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
import type { AdfMark, AdfNode } from './document.ts'
|
import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from './document.ts'
|
||||||
|
import type { JsonValue } from '../json-value.ts'
|
||||||
|
import { nodeAttrs, nodeContent, nodeMarks } from './document.ts'
|
||||||
import { serializeCanonicalJson } from '../canonical-json.ts'
|
import { serializeCanonicalJson } from '../canonical-json.ts'
|
||||||
|
|
||||||
|
type JsonContainer = JsonValue[] | { [key: string]: JsonValue }
|
||||||
|
|
||||||
|
type NodeHolder = { content?: AdfNode[] }
|
||||||
|
|
||||||
export function sameMark(candidate: AdfMark, mark: AdfMark): boolean {
|
export function sameMark(candidate: AdfMark, mark: AdfMark): boolean {
|
||||||
return markKey(candidate) === markKey(mark)
|
return markKey(candidate) === markKey(mark)
|
||||||
}
|
}
|
||||||
@@ -19,12 +25,61 @@ export function mergeAdjacentText(nodes: readonly AdfNode[]): AdfNode[] {
|
|||||||
return merged
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function toEditorNormal(document: AdfDocument): AdfDocument {
|
||||||
|
const normal: AdfDocument = { type: document.type, version: Object.is(document.version, -0) ? 0 : document.version }
|
||||||
|
const pending: { holder: NodeHolder; source: NodeHolder }[] = [{ holder: normal, source: document }]
|
||||||
|
for (let entry = pending.pop(); entry !== undefined; entry = pending.pop()) {
|
||||||
|
const content = mergeAdjacentText(nodeContent(entry.source))
|
||||||
|
if (content.length === 0) continue
|
||||||
|
entry.holder.content = content.map((source) => {
|
||||||
|
const holder = normalNode(source)
|
||||||
|
pending.push({ holder, source })
|
||||||
|
return holder
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return normal
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalNode(node: AdfNode): AdfNode {
|
||||||
|
const normal: AdfNode = { type: node.type }
|
||||||
|
const attrs = normalAttributes(nodeAttrs(node))
|
||||||
|
if (attrs !== undefined) normal.attrs = attrs
|
||||||
|
const marks = nodeMarks(node).map(normalMark)
|
||||||
|
if (marks.length > 0) normal.marks = marks
|
||||||
|
if (node.text !== undefined) normal.text = node.text
|
||||||
|
return normal
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalMark(mark: AdfMark): AdfMark {
|
||||||
|
const attrs = normalAttributes(nodeAttrs(mark))
|
||||||
|
return attrs === undefined ? { type: mark.type } : { attrs, type: mark.type }
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalAttributes(attrs: AdfAttributes): AdfAttributes | undefined {
|
||||||
|
if (Object.keys(attrs).length === 0) return undefined
|
||||||
|
const normal = { ...attrs }
|
||||||
|
const pending: JsonContainer[] = [normal]
|
||||||
|
for (let held = pending.pop(); held !== undefined; held = pending.pop()) {
|
||||||
|
if (Array.isArray(held)) for (const [index, value] of held.entries()) held[index] = normalValue(value, pending)
|
||||||
|
else for (const [key, value] of Object.entries(held)) held[key] = normalValue(value, pending)
|
||||||
|
}
|
||||||
|
return normal
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalValue(value: JsonValue, pending: JsonContainer[]): JsonValue {
|
||||||
|
if (Object.is(value, -0)) return 0
|
||||||
|
if (value === null || typeof value !== 'object') return value
|
||||||
|
const copy = Array.isArray(value) ? [...value] : { ...value }
|
||||||
|
pending.push(copy)
|
||||||
|
return copy
|
||||||
|
}
|
||||||
|
|
||||||
function mergesText(node: AdfNode): boolean {
|
function mergesText(node: AdfNode): boolean {
|
||||||
return node.type === 'text' && Object.keys(node.attrs ?? {}).length === 0
|
return node.type === 'text' && Object.keys(nodeAttrs(node)).length === 0
|
||||||
}
|
}
|
||||||
|
|
||||||
function sameMarks(previous: AdfNode, node: AdfNode): boolean {
|
function sameMarks(previous: AdfNode, node: AdfNode): boolean {
|
||||||
return marksKey(previous.marks ?? []) === marksKey(node.marks ?? [])
|
return marksKey(nodeMarks(previous)) === marksKey(nodeMarks(node))
|
||||||
}
|
}
|
||||||
|
|
||||||
function marksKey(marks: readonly AdfMark[]): string {
|
function marksKey(marks: readonly AdfMark[]): string {
|
||||||
@@ -32,5 +87,5 @@ function marksKey(marks: readonly AdfMark[]): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function markKey(mark: AdfMark): string {
|
function markKey(mark: AdfMark): string {
|
||||||
return `${mark.type} ${serializeCanonicalJson(mark.attrs ?? {}, 'compact')}`
|
return `${mark.type} ${serializeCanonicalJson(nodeAttrs(mark), 'compact')}`
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-3
@@ -9,6 +9,7 @@ import { isAdfDocument } from './adf/document.ts'
|
|||||||
import { isJsonValue } from './json-value.ts'
|
import { isJsonValue } from './json-value.ts'
|
||||||
import { markdownToAdf } from './markdown/parse/markdown-to-adf.ts'
|
import { markdownToAdf } from './markdown/parse/markdown-to-adf.ts'
|
||||||
import { serializeCanonicalJson } from './canonical-json.ts'
|
import { serializeCanonicalJson } from './canonical-json.ts'
|
||||||
|
import { toEditorNormal } from './adf/editor-normal.ts'
|
||||||
|
|
||||||
const corpusRoot = join(dirname(fileURLToPath(import.meta.url)), '..', 'corpus')
|
const corpusRoot = join(dirname(fileURLToPath(import.meta.url)), '..', 'corpus')
|
||||||
const errorsRoot = join(corpusRoot, 'errors')
|
const errorsRoot = join(corpusRoot, 'errors')
|
||||||
@@ -95,7 +96,7 @@ for (const directory of roundTripDirectories) {
|
|||||||
assert.ok(isAdfDocument(expected), `${name}.json is not an ADF document`)
|
assert.ok(isAdfDocument(expected), `${name}.json is not an ADF document`)
|
||||||
const result = markdownToAdf(readFileSync(join(roundTripRoot, directory, `${name}.md`), 'utf8'))
|
const result = markdownToAdf(readFileSync(join(roundTripRoot, directory, `${name}.md`), 'utf8'))
|
||||||
assert.ok(result.ok, result.ok ? '' : `${result.error.code}: ${result.error.message}`)
|
assert.ok(result.ok, result.ok ? '' : `${result.error.code}: ${result.error.message}`)
|
||||||
assert.deepEqual(result.value, expected)
|
assert.deepEqual(toEditorNormal(result.value), toEditorNormal(expected))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -184,12 +185,12 @@ for (const name of pairedNames(normalizationRoot, '.md', '.json')) {
|
|||||||
assert.ok(isAdfDocument(expected), `${name}.json is not an ADF document`)
|
assert.ok(isAdfDocument(expected), `${name}.json is not an ADF document`)
|
||||||
const result = markdownToAdf(readFileSync(join(normalizationRoot, `${name}.md`), 'utf8'))
|
const result = markdownToAdf(readFileSync(join(normalizationRoot, `${name}.md`), 'utf8'))
|
||||||
assert.ok(result.ok, result.ok ? '' : `${result.error.code}: ${result.error.message}`)
|
assert.ok(result.ok, result.ok ? '' : `${result.error.code}: ${result.error.message}`)
|
||||||
assert.deepEqual(result.value, expected)
|
assert.deepEqual(toEditorNormal(result.value), toEditorNormal(expected))
|
||||||
const emitted = adfToMarkdown(result.value)
|
const emitted = adfToMarkdown(result.value)
|
||||||
assert.ok(emitted.ok, emitted.ok ? '' : `${emitted.error.code}: ${emitted.error.message}`)
|
assert.ok(emitted.ok, emitted.ok ? '' : `${emitted.error.code}: ${emitted.error.message}`)
|
||||||
const again = markdownToAdf(emitted.value)
|
const again = markdownToAdf(emitted.value)
|
||||||
assert.ok(again.ok, again.ok ? '' : `${again.error.code}: ${again.error.message}`)
|
assert.ok(again.ok, again.ok ? '' : `${again.error.code}: ${again.error.message}`)
|
||||||
assert.deepEqual(again.value, expected)
|
assert.deepEqual(toEditorNormal(again.value), toEditorNormal(expected))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { AdfMark } from '../adf/document.ts'
|
import type { AdfMark } from '../adf/document.ts'
|
||||||
import type { JsonValue } from '../json-value.ts'
|
import type { JsonValue } from '../json-value.ts'
|
||||||
import { isAdfMark } from '../adf/document.ts'
|
import { isAdfMark, nodeAttrs } from '../adf/document.ts'
|
||||||
import { serializeCanonicalJson } from '../canonical-json.ts'
|
import { serializeCanonicalJson } from '../canonical-json.ts'
|
||||||
|
|
||||||
export const marksAttribute = 'marks'
|
export const marksAttribute = 'marks'
|
||||||
|
|
||||||
export function markValues(marks: readonly AdfMark[]): JsonValue {
|
export function markValues(marks: readonly AdfMark[]): JsonValue {
|
||||||
return marks.map((mark) => {
|
return marks.map((mark) => {
|
||||||
const attrs = mark.attrs ?? {}
|
const attrs = nodeAttrs(mark)
|
||||||
return Object.keys(attrs).length === 0 ? { type: mark.type } : { attrs, type: mark.type }
|
return Object.keys(attrs).length === 0 ? { type: mark.type } : { attrs, type: mark.type }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { JsonValue } from '../../json-value.ts'
|
|||||||
import type { Result } from '../../result.ts'
|
import type { Result } from '../../result.ts'
|
||||||
import { adfToMarkdown, markdownToAdf } from '../../index.ts'
|
import { adfToMarkdown, markdownToAdf } from '../../index.ts'
|
||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
|
import { toEditorNormal } from '../../adf/editor-normal.ts'
|
||||||
|
|
||||||
function document(...content: AdfNode[]): AdfDocument {
|
function document(...content: AdfNode[]): AdfDocument {
|
||||||
return { content, type: 'doc', version: 1 }
|
return { content, type: 'doc', version: 1 }
|
||||||
@@ -353,7 +354,9 @@ test('refuses marks and attributes nested deeper than the emitter carries', () =
|
|||||||
const roundTrips = (node: AdfNode): void => {
|
const roundTrips = (node: AdfNode): void => {
|
||||||
const spelled = adfToMarkdown(document(node))
|
const spelled = adfToMarkdown(document(node))
|
||||||
assert.ok(spelled.ok, spelled.ok ? '' : spelled.error.message)
|
assert.ok(spelled.ok, spelled.ok ? '' : spelled.error.message)
|
||||||
assert.deepEqual(markdownToAdf(spelled.value), { ok: true, value: document(node) })
|
const read = markdownToAdf(spelled.value)
|
||||||
|
assert.ok(read.ok, read.ok ? '' : read.error.message)
|
||||||
|
assert.deepEqual(toEditorNormal(read.value), toEditorNormal(document(node)))
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.equal(markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs, type: 'em' }], text: 'x', type: 'text' })))), deeper('depth', 'em', largestNesting - 3))
|
assert.equal(markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs, type: 'em' }], text: 'x', type: 'text' })))), deeper('depth', 'em', largestNesting - 3))
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
||||||
import type { BlockDirective } from '../../adf/block-directives.ts'
|
import type { BlockDirective } from '../../adf/block-directives.ts'
|
||||||
import { adfDocumentFault, carriesOnly } from '../../adf/document.ts'
|
import { adfDocumentFault, carriesOnly, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||||
import { blockDirective } from '../../adf/block-directives.ts'
|
import { blockDirective } from '../../adf/block-directives.ts'
|
||||||
import { carriedBlock } from '../opaque-carry.ts'
|
import { carriedBlock } from '../opaque-carry.ts'
|
||||||
import { emitInlineLine } from './inline-line.ts'
|
import { emitInlineLine } from './inline-line.ts'
|
||||||
@@ -26,7 +26,7 @@ export function adfToMarkdown(document: AdfDocument): Result<string> {
|
|||||||
const fault = adfDocumentFault(document)
|
const fault = adfDocumentFault(document)
|
||||||
if (fault !== undefined) return faulted(fault, [])
|
if (fault !== undefined) return faulted(fault, [])
|
||||||
if (document.version !== 1) return failure('unsupported-document-version', `no markdown spelling carries ADF version ${document.version}`, [])
|
if (document.version !== 1) return failure('unsupported-document-version', `no markdown spelling carries ADF version ${document.version}`, [])
|
||||||
const blocks = emitBlocks(document.content ?? [], 'document', [], 0)
|
const blocks = emitBlocks(nodeContent(document), 'document', [], 0)
|
||||||
if (!blocks.ok) return blocks
|
if (!blocks.ok) return blocks
|
||||||
return success(blocks.value.text === '' ? '' : `${blocks.value.text}\n`)
|
return success(blocks.value.text === '' ? '' : `${blocks.value.text}\n`)
|
||||||
}
|
}
|
||||||
@@ -63,8 +63,8 @@ function separationBetween(previous: PlacedBlock, next: PlacedBlock, container:
|
|||||||
}
|
}
|
||||||
|
|
||||||
function interruptsParagraph(node: AdfNode): boolean {
|
function interruptsParagraph(node: AdfNode): boolean {
|
||||||
const items = node.content ?? []
|
const items = nodeContent(node)
|
||||||
const empty = (items[0]?.content ?? []).length === 0
|
const empty = items[0] === undefined || nodeContent(items[0]).length === 0
|
||||||
if (node.type !== 'orderedList') return markerInterruptsParagraph(undefined, empty)
|
if (node.type !== 'orderedList') return markerInterruptsParagraph(undefined, empty)
|
||||||
return markerInterruptsParagraph(listStart(node, items.length) ?? 0, empty)
|
return markerInterruptsParagraph(listStart(node, items.length) ?? 0, empty)
|
||||||
}
|
}
|
||||||
@@ -110,7 +110,7 @@ function commonMarkText(text: string): EmittedBlock {
|
|||||||
|
|
||||||
function emitDirectiveBlock(node: AdfNode, directive: BlockDirective, path: ConvertErrorPath, depth: number): Result<EmittedBlock> {
|
function emitDirectiveBlock(node: AdfNode, directive: BlockDirective, path: ConvertErrorPath, depth: number): Result<EmittedBlock> {
|
||||||
if (node.text !== undefined) return failure('unsupported-node-shape', `a ${node.type} carries no text: this one holds text`, path)
|
if (node.text !== undefined) return failure('unsupported-node-shape', `a ${node.type} carries no text: this one holds text`, path)
|
||||||
const content = node.content ?? []
|
const content = nodeContent(node)
|
||||||
if (directive.contentModel === 'none' && content.length > 0) return failure('unsupported-node-shape', `a ${node.type} holds no content: this one holds some`, path)
|
if (directive.contentModel === 'none' && content.length > 0) return failure('unsupported-node-shape', `a ${node.type} holds no content: this one holds some`, path)
|
||||||
if (directive.contentModel === 'code') return emitCodeDirective(node, directive, path, depth)
|
if (directive.contentModel === 'code') return emitCodeDirective(node, directive, path, depth)
|
||||||
const header = spellDirectiveHeader(node, directive)
|
const header = spellDirectiveHeader(node, directive)
|
||||||
@@ -134,7 +134,7 @@ function emitInlineBody(content: readonly AdfNode[], path: ConvertErrorPath): Re
|
|||||||
|
|
||||||
function emitBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number): Result<EmittedBlock> | undefined {
|
function emitBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number): Result<EmittedBlock> | undefined {
|
||||||
if (!carriesOnly(node, [])) return undefined
|
if (!carriesOnly(node, [])) return undefined
|
||||||
const inner = emitBlocks(node.content ?? [], 'document', path, depth + 1)
|
const inner = emitBlocks(nodeContent(node), 'document', path, depth + 1)
|
||||||
if (!inner.ok) return inner
|
if (!inner.ok) return inner
|
||||||
const text = inner.value.text
|
const text = inner.value.text
|
||||||
.split('\n')
|
.split('\n')
|
||||||
@@ -145,7 +145,7 @@ function emitBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number): R
|
|||||||
|
|
||||||
function emitCodeBlock(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
function emitCodeBlock(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
||||||
if (!carriesOnly(node, ['language'])) return undefined
|
if (!carriesOnly(node, ['language'])) return undefined
|
||||||
const slot = languageSlot(node.attrs?.['language'])
|
const slot = languageSlot(nodeAttrs(node)['language'])
|
||||||
if (slot.kind === 'attribute') return undefined
|
if (slot.kind === 'attribute') return undefined
|
||||||
const text = codeBlockText(node, path)
|
const text = codeBlockText(node, path)
|
||||||
if (!text.ok) return text
|
if (!text.ok) return text
|
||||||
@@ -153,7 +153,7 @@ function emitCodeBlock(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlo
|
|||||||
}
|
}
|
||||||
|
|
||||||
function emitCodeDirective(node: AdfNode, directive: BlockDirective, path: ConvertErrorPath, depth: number): Result<EmittedBlock> {
|
function emitCodeDirective(node: AdfNode, directive: BlockDirective, path: ConvertErrorPath, depth: number): Result<EmittedBlock> {
|
||||||
const slot = languageSlot(node.attrs?.['language'])
|
const slot = languageSlot(nodeAttrs(node)['language'])
|
||||||
const header = spellDirectiveHeader(node, directive, slot.kind === 'attribute' ? [] : ['language'])
|
const header = spellDirectiveHeader(node, directive, slot.kind === 'attribute' ? [] : ['language'])
|
||||||
if (header === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
if (header === undefined) return commonMarkLine(carriedBlock(node, path, depth))
|
||||||
const text = codeBlockText(node, path)
|
const text = codeBlockText(node, path)
|
||||||
@@ -164,15 +164,15 @@ function emitCodeDirective(node: AdfNode, directive: BlockDirective, path: Conve
|
|||||||
|
|
||||||
function codeBlockText(node: AdfNode, path: ConvertErrorPath): Result<string> {
|
function codeBlockText(node: AdfNode, path: ConvertErrorPath): Result<string> {
|
||||||
let text = ''
|
let text = ''
|
||||||
for (const [index, child] of (node.content ?? []).entries()) {
|
for (const [index, child] of nodeContent(node).entries()) {
|
||||||
const childPath = [...path, 'content', index]
|
const childPath = [...path, 'content', index]
|
||||||
if (
|
if (
|
||||||
child.type !== 'text' ||
|
child.type !== 'text' ||
|
||||||
typeof child.text !== 'string' ||
|
typeof child.text !== 'string' ||
|
||||||
child.text === '' ||
|
child.text === '' ||
|
||||||
(child.content ?? []).length > 0 ||
|
nodeContent(child).length > 0 ||
|
||||||
(child.marks ?? []).length > 0 ||
|
nodeMarks(child).length > 0 ||
|
||||||
Object.keys(child.attrs ?? {}).length > 0
|
Object.keys(nodeAttrs(child)).length > 0
|
||||||
) {
|
) {
|
||||||
return failure('unsupported-node-shape', `a codeBlock holds plain text nodes only: this ${child.type} node is not one`, childPath)
|
return failure('unsupported-node-shape', `a codeBlock holds plain text nodes only: this ${child.type} node is not one`, childPath)
|
||||||
}
|
}
|
||||||
@@ -185,10 +185,10 @@ function codeBlockText(node: AdfNode, path: ConvertErrorPath): Result<string> {
|
|||||||
|
|
||||||
function emitHeading(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
function emitHeading(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
||||||
if (!carriesOnly(node, ['level'])) return undefined
|
if (!carriesOnly(node, ['level'])) return undefined
|
||||||
const level = node.attrs?.['level']
|
const level = nodeAttrs(node)['level']
|
||||||
if (typeof level !== 'number' || !Number.isInteger(level) || level < 1 || level > 6) return undefined
|
if (typeof level !== 'number' || !Number.isInteger(level) || level < 1 || level > 6) return undefined
|
||||||
const hashes = '#'.repeat(level)
|
const hashes = '#'.repeat(level)
|
||||||
const content = node.content ?? []
|
const content = nodeContent(node)
|
||||||
if (content.length === 0) return success(commonMarkText(hashes))
|
if (content.length === 0) return success(commonMarkText(hashes))
|
||||||
const line = emitInlineLine(content, 'heading', path)
|
const line = emitInlineLine(content, 'heading', path)
|
||||||
if (!line.ok) return line
|
if (!line.ok) return line
|
||||||
@@ -198,7 +198,7 @@ function emitHeading(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock
|
|||||||
function emitList(node: AdfNode, path: ConvertErrorPath, depth: number): Result<EmittedBlock> | undefined {
|
function emitList(node: AdfNode, path: ConvertErrorPath, depth: number): Result<EmittedBlock> | undefined {
|
||||||
const ordered = node.type === 'orderedList'
|
const ordered = node.type === 'orderedList'
|
||||||
if (!carriesOnly(node, ordered ? ['order'] : [])) return undefined
|
if (!carriesOnly(node, ordered ? ['order'] : [])) return undefined
|
||||||
const items = node.content ?? []
|
const items = nodeContent(node)
|
||||||
const start = listStart(node, items.length)
|
const start = listStart(node, items.length)
|
||||||
if (start === undefined || items.length === 0) return undefined
|
if (start === undefined || items.length === 0) return undefined
|
||||||
if (items.some((item) => item.type !== 'listItem' || !carriesOnly(item, []))) return undefined
|
if (items.some((item) => item.type !== 'listItem' || !carriesOnly(item, []))) return undefined
|
||||||
@@ -216,13 +216,13 @@ function emitList(node: AdfNode, path: ConvertErrorPath, depth: number): Result<
|
|||||||
|
|
||||||
function listStart(node: AdfNode, items: number): number | undefined {
|
function listStart(node: AdfNode, items: number): number | undefined {
|
||||||
if (node.type !== 'orderedList') return 0
|
if (node.type !== 'orderedList') return 0
|
||||||
const start = node.attrs?.['order']
|
const start = nodeAttrs(node)['order']
|
||||||
if (typeof start !== 'number' || !Number.isInteger(start) || start < 0 || start > largestListMarker) return undefined
|
if (typeof start !== 'number' || !Number.isInteger(start) || start < 0 || start > largestListMarker) return undefined
|
||||||
return start + items - 1 > largestListMarker ? undefined : start
|
return start + items - 1 > largestListMarker ? undefined : start
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitListItem(item: AdfNode, marker: string, path: ConvertErrorPath, depth: number): Result<EmittedBody> | undefined {
|
function emitListItem(item: AdfNode, marker: string, path: ConvertErrorPath, depth: number): Result<EmittedBody> | undefined {
|
||||||
const inner = emitBlocks(item.content ?? [], 'list-item', path, depth + 1)
|
const inner = emitBlocks(nodeContent(item), 'list-item', path, depth + 1)
|
||||||
if (!inner.ok) return inner
|
if (!inner.ok) return inner
|
||||||
if (inner.value.text === '') return success({ fenceColons: 0, text: marker.trimEnd() })
|
if (inner.value.text === '') return success({ fenceColons: 0, text: marker.trimEnd() })
|
||||||
const indent = ' '.repeat(marker.length)
|
const indent = ' '.repeat(marker.length)
|
||||||
@@ -232,7 +232,7 @@ function emitListItem(item: AdfNode, marker: string, path: ConvertErrorPath, dep
|
|||||||
}
|
}
|
||||||
|
|
||||||
function emitParagraph(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
function emitParagraph(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
||||||
const content = node.content ?? []
|
const content = nodeContent(node)
|
||||||
if (content.length === 0 || !carriesOnly(node, [])) return undefined
|
if (content.length === 0 || !carriesOnly(node, [])) return undefined
|
||||||
const line = emitInlineLine(content, 'paragraph', path)
|
const line = emitInlineLine(content, 'paragraph', path)
|
||||||
if (!line.ok) return line
|
if (!line.ok) return line
|
||||||
@@ -240,6 +240,6 @@ function emitParagraph(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlo
|
|||||||
}
|
}
|
||||||
|
|
||||||
function emitRule(node: AdfNode): Result<EmittedBlock> | undefined {
|
function emitRule(node: AdfNode): Result<EmittedBlock> | undefined {
|
||||||
if (!carriesOnly(node, []) || (node.content ?? []).length > 0) return undefined
|
if (!carriesOnly(node, []) || nodeContent(node).length > 0) return undefined
|
||||||
return success(commonMarkText('---'))
|
return success(commonMarkText('---'))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { BlockDirective } from '../../adf/block-directives.ts'
|
|||||||
import { blockArgument } from '../block-directive-arguments.ts'
|
import { blockArgument } from '../block-directive-arguments.ts'
|
||||||
import { isBareToken, spellAttributes, spellJsonAttribute, spellVocabulary } from '../directive-syntax.ts'
|
import { isBareToken, spellAttributes, spellJsonAttribute, spellVocabulary } from '../directive-syntax.ts'
|
||||||
import { markValues, marksAttribute } from '../block-directive-marks.ts'
|
import { markValues, marksAttribute } from '../block-directive-marks.ts'
|
||||||
|
import { nodeAttrs, nodeMarks } from '../../adf/document.ts'
|
||||||
import { vocabularyPairs } from '../../adf/attribute-vocabulary.ts'
|
import { vocabularyPairs } from '../../adf/attribute-vocabulary.ts'
|
||||||
|
|
||||||
export function spellDirectiveHeader(node: AdfNode, directive: BlockDirective, spelledByBody: readonly string[] = []): string | undefined {
|
export function spellDirectiveHeader(node: AdfNode, directive: BlockDirective, spelledByBody: readonly string[] = []): string | undefined {
|
||||||
@@ -10,17 +11,17 @@ export function spellDirectiveHeader(node: AdfNode, directive: BlockDirective, s
|
|||||||
const argument = spellArgument(node, argumentAttribute)
|
const argument = spellArgument(node, argumentAttribute)
|
||||||
if (argument === undefined) return undefined
|
if (argument === undefined) return undefined
|
||||||
const spelled = argumentAttribute === undefined ? spelledByBody : [argumentAttribute, ...spelledByBody]
|
const spelled = argumentAttribute === undefined ? spelledByBody : [argumentAttribute, ...spelledByBody]
|
||||||
const pairs = vocabularyPairs(node.attrs ?? {}, directive.attributes, spelled)
|
const pairs = vocabularyPairs(nodeAttrs(node), directive.attributes, spelled)
|
||||||
if (pairs === undefined) return undefined
|
if (pairs === undefined) return undefined
|
||||||
const spelledPairs = spellVocabulary(pairs)
|
const spelledPairs = spellVocabulary(pairs)
|
||||||
const marks = node.marks ?? []
|
const marks = nodeMarks(node)
|
||||||
if (marks.length > 0) spelledPairs.push([marksAttribute, spellJsonAttribute(markValues(marks))])
|
if (marks.length > 0) spelledPairs.push([marksAttribute, spellJsonAttribute(markValues(marks))])
|
||||||
const attributes = spellAttributes(spelledPairs)
|
const attributes = spellAttributes(spelledPairs)
|
||||||
return `${node.type}${argument}${attributes === '' ? '' : ` ${attributes}`}`
|
return `${node.type}${argument}${attributes === '' ? '' : ` ${attributes}`}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function spellArgument(node: AdfNode, argumentAttribute: string | undefined): string | undefined {
|
function spellArgument(node: AdfNode, argumentAttribute: string | undefined): string | undefined {
|
||||||
const value = argumentAttribute === undefined ? undefined : node.attrs?.[argumentAttribute]
|
const value = argumentAttribute === undefined ? undefined : nodeAttrs(node)[argumentAttribute]
|
||||||
if (value === undefined) return ''
|
if (value === undefined) return ''
|
||||||
if (typeof value !== 'string' || !isBareToken(value)) return undefined
|
if (typeof value !== 'string' || !isBareToken(value)) return undefined
|
||||||
return ` ${value}`
|
return ` ${value}`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { AdfNode } from '../../adf/document.ts'
|
import type { AdfNode } from '../../adf/document.ts'
|
||||||
import { carriesOnly } from '../../adf/document.ts'
|
import { carriesOnly, nodeAttrs, nodeContent } from '../../adf/document.ts'
|
||||||
import type { ConvertErrorPath } from '../../result.ts'
|
import type { ConvertErrorPath } from '../../result.ts'
|
||||||
import { serializeCanonicalJson } from '../../canonical-json.ts'
|
import { serializeCanonicalJson } from '../../canonical-json.ts'
|
||||||
import { tryImageLine } from './inline-line.ts'
|
import { tryImageLine } from './inline-line.ts'
|
||||||
@@ -14,11 +14,11 @@ export function tryImage(node: AdfNode, path: ConvertErrorPath): string | undefi
|
|||||||
}
|
}
|
||||||
|
|
||||||
function imageShape(node: AdfNode): { alt: string | undefined; url: string } | undefined {
|
function imageShape(node: AdfNode): { alt: string | undefined; url: string } | undefined {
|
||||||
const content = node.content ?? []
|
const content = nodeContent(node)
|
||||||
const media = content[0]
|
const media = content[0]
|
||||||
if (!carriesOnly(node, ['layout']) || serializeCanonicalJson(node.attrs ?? {}, 'compact') !== centeredMediaSingle) return undefined
|
if (!carriesOnly(node, ['layout']) || serializeCanonicalJson(nodeAttrs(node), 'compact') !== centeredMediaSingle) return undefined
|
||||||
if (media === undefined || content.length !== 1 || media.type !== 'media' || !carriesOnly(media, imageAttributes) || (media.content ?? []).length > 0) return undefined
|
if (media === undefined || content.length !== 1 || media.type !== 'media' || !carriesOnly(media, imageAttributes) || nodeContent(media).length > 0) return undefined
|
||||||
const attrs = media.attrs ?? {}
|
const attrs = nodeAttrs(media)
|
||||||
const alt = attrs['alt']
|
const alt = attrs['alt']
|
||||||
const url = attrs['url']
|
const url = attrs['url']
|
||||||
if (attrs['type'] !== 'external' || typeof url !== 'string') return undefined
|
if (attrs['type'] !== 'external' || typeof url !== 'string') return undefined
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import type { AdfNode } from '../../adf/document.ts'
|
import type { AdfNode } from '../../adf/document.ts'
|
||||||
import type { InlineDirective } from '../../adf/inline-directives.ts'
|
import type { InlineDirective } from '../../adf/inline-directives.ts'
|
||||||
|
import { nodeAttrs } from '../../adf/document.ts'
|
||||||
import { spellAttributes, spellVocabulary } from '../directive-syntax.ts'
|
import { spellAttributes, spellVocabulary } from '../directive-syntax.ts'
|
||||||
import { vocabularyPairs } from '../../adf/attribute-vocabulary.ts'
|
import { vocabularyPairs } from '../../adf/attribute-vocabulary.ts'
|
||||||
|
|
||||||
export function spellInlineNodeAttributes(node: AdfNode, directive: InlineDirective): string | undefined {
|
export function spellInlineNodeAttributes(node: AdfNode, directive: InlineDirective): string | undefined {
|
||||||
const pairs = vocabularyPairs(node.attrs ?? {}, directive.attributes, directive.textAttribute === undefined ? [] : [directive.textAttribute])
|
const pairs = vocabularyPairs(nodeAttrs(node), directive.attributes, directive.textAttribute === undefined ? [] : [directive.textAttribute])
|
||||||
return pairs === undefined ? undefined : spellAttributes(spellVocabulary(pairs))
|
return pairs === undefined ? undefined : spellAttributes(spellVocabulary(pairs))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { inlineDirective } from '../../adf/inline-directives.ts'
|
|||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
import { longestBacktickRun } from '../backtick-runs.ts'
|
import { longestBacktickRun } from '../backtick-runs.ts'
|
||||||
import { markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
import { markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
||||||
|
import { nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
|
||||||
import { sameMark } from '../../adf/editor-normal.ts'
|
import { sameMark } from '../../adf/editor-normal.ts'
|
||||||
import { slotLineEndingFault, spellLeafDirective } from '../directive-syntax.ts'
|
import { slotLineEndingFault, spellLeafDirective } from '../directive-syntax.ts'
|
||||||
import { spellDestination, spellTitle } from '../link-syntax.ts'
|
import { spellDestination, spellTitle } from '../link-syntax.ts'
|
||||||
@@ -127,7 +128,7 @@ function syntax(text: string): InlineSegment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function refuseContentAndText(node: AdfNode, path: ConvertErrorPath): Result<null> {
|
function refuseContentAndText(node: AdfNode, path: ConvertErrorPath): Result<null> {
|
||||||
const holdsContent = (node.content ?? []).length > 0
|
const holdsContent = nodeContent(node).length > 0
|
||||||
if (holdsContent || node.text !== undefined) {
|
if (holdsContent || node.text !== undefined) {
|
||||||
const held = holdsContent ? 'content' : 'text'
|
const held = holdsContent ? 'content' : 'text'
|
||||||
return failure('unsupported-node-shape', `a ${node.type} node holds neither content nor text: this one holds ${held}`, path)
|
return failure('unsupported-node-shape', `a ${node.type} node holds neither content nor text: this one holds ${held}`, path)
|
||||||
@@ -156,7 +157,7 @@ function inlineRuns(nodes: readonly AdfNode[], depth: number, firstIndex: number
|
|||||||
for (const [offset, node] of nodes.entries()) {
|
for (const [offset, node] of nodes.entries()) {
|
||||||
const index = firstIndex + offset
|
const index = firstIndex + offset
|
||||||
// spec/flavour.md, Marks.
|
// spec/flavour.md, Marks.
|
||||||
const mark = carries(node, carried, index) ? undefined : (node.marks ?? [])[depth]
|
const mark = carries(node, carried, index) ? undefined : nodeMarks(node)[depth]
|
||||||
if (mark === undefined) {
|
if (mark === undefined) {
|
||||||
runs.push({ index, kind: 'plain', node })
|
runs.push({ index, kind: 'plain', node })
|
||||||
continue
|
continue
|
||||||
@@ -184,7 +185,7 @@ function emitLeaf(node: AdfNode, context: InlineContext, index: number): Result<
|
|||||||
if (!carried.ok) return carried
|
if (!carried.ok) return carried
|
||||||
return success({ segments: [syntax(carried.value)] })
|
return success({ segments: [syntax(carried.value)] })
|
||||||
}
|
}
|
||||||
const types = (node.marks ?? []).map((mark) => mark.type)
|
const types = nodeMarks(node).map((mark) => mark.type)
|
||||||
if (new Set(types).size !== types.length) return failure('unsupported-node-shape', `a ${node.type} node carries one mark type twice`, path)
|
if (new Set(types).size !== types.length) return failure('unsupported-node-shape', `a ${node.type} node carries one mark type twice`, path)
|
||||||
const directive = inlineDirective(node.type)
|
const directive = inlineDirective(node.type)
|
||||||
if (directive === undefined) return emitText(node, context, index, path)
|
if (directive === undefined) return emitText(node, context, index, path)
|
||||||
@@ -206,7 +207,7 @@ function emitInlineDirective(node: AdfNode, directive: InlineDirective, index: n
|
|||||||
if (!empty.ok) return empty
|
if (!empty.ok) return empty
|
||||||
const attributes = spellInlineNodeAttributes(node, directive)
|
const attributes = spellInlineNodeAttributes(node, directive)
|
||||||
if (attributes === undefined) return success({ carry: { first: index, last: index } })
|
if (attributes === undefined) return success({ carry: { first: index, last: index } })
|
||||||
const slot = directive.textAttribute === undefined ? undefined : node.attrs?.[directive.textAttribute]
|
const slot = directive.textAttribute === undefined ? undefined : nodeAttrs(node)[directive.textAttribute]
|
||||||
if (slot === undefined) return success({ segments: [syntax(spellLeafDirective(node.type, attributes))] })
|
if (slot === undefined) return success({ segments: [syntax(spellLeafDirective(node.type, attributes))] })
|
||||||
if (typeof slot !== 'string') return success({ carry: { first: index, last: index } })
|
if (typeof slot !== 'string') return success({ carry: { first: index, last: index } })
|
||||||
const spans = slotLineEndingFault(node.type, slot)
|
const spans = slotLineEndingFault(node.type, slot)
|
||||||
@@ -217,9 +218,9 @@ function emitInlineDirective(node: AdfNode, directive: InlineDirective, index: n
|
|||||||
}
|
}
|
||||||
|
|
||||||
function emitText(node: AdfNode, context: InlineContext, index: number, path: ConvertErrorPath): Result<Emission> {
|
function emitText(node: AdfNode, context: InlineContext, index: number, path: ConvertErrorPath): Result<Emission> {
|
||||||
if (Object.keys(node.attrs ?? {}).length > 0) return success({ carry: { first: index, last: index } })
|
if (Object.keys(nodeAttrs(node)).length > 0) return success({ carry: { first: index, last: index } })
|
||||||
if (typeof node.text !== 'string' || node.text === '') return failure('unsupported-node-shape', 'a text node holds text: this one has none', path)
|
if (typeof node.text !== 'string' || node.text === '') return failure('unsupported-node-shape', 'a text node holds text: this one has none', path)
|
||||||
if ((node.content ?? []).length > 0) return failure('unsupported-node-shape', 'a text node holds no content: this one holds some', path)
|
if (nodeContent(node).length > 0) return failure('unsupported-node-shape', 'a text node holds no content: this one holds some', path)
|
||||||
if (/\r/.test(node.text)) return failure('unspellable-character', 'a text node holds a carriage return CommonMark rewrites', path)
|
if (/\r/.test(node.text)) return failure('unspellable-character', 'a text node holds a carriage return CommonMark rewrites', path)
|
||||||
if (holdsNullCharacter(node.text)) return failure('unspellable-character', 'a text node holds a null character CommonMark replaces', path)
|
if (holdsNullCharacter(node.text)) return failure('unspellable-character', 'a text node holds a null character CommonMark replaces', path)
|
||||||
const escaping: InlineEscaping = context.bracketed ? 'bracketed' : 'backslash'
|
const escaping: InlineEscaping = context.bracketed ? 'bracketed' : 'backslash'
|
||||||
@@ -260,9 +261,9 @@ function emitEmphasis(nodes: readonly AdfNode[], spelling: string, depth: number
|
|||||||
function emitCodeSpan(nodes: readonly AdfNode[], depth: number, range: NodeRange, path: ConvertErrorPath): Result<Emission> {
|
function emitCodeSpan(nodes: readonly AdfNode[], depth: number, range: NodeRange, path: ConvertErrorPath): Result<Emission> {
|
||||||
let text = ''
|
let text = ''
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
if (node.type !== 'text' || (node.marks ?? []).length !== depth + 1) return success({ carry: range })
|
if (node.type !== 'text' || nodeMarks(node).length !== depth + 1) return success({ carry: range })
|
||||||
if (typeof node.text !== 'string' || node.text === '') return failure('unsupported-node-shape', 'a text node holds text: this one has none', path)
|
if (typeof node.text !== 'string' || node.text === '') return failure('unsupported-node-shape', 'a text node holds text: this one has none', path)
|
||||||
if ((node.content ?? []).length > 0) return failure('unsupported-node-shape', 'a text node holds no content: this one holds some', path)
|
if (nodeContent(node).length > 0) return failure('unsupported-node-shape', 'a text node holds no content: this one holds some', path)
|
||||||
text += node.text
|
text += node.text
|
||||||
}
|
}
|
||||||
if (/[\n\r]/.test(text)) return success({ carry: range })
|
if (/[\n\r]/.test(text)) return success({ carry: range })
|
||||||
@@ -278,11 +279,11 @@ function needsPadding(text: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range: NodeRange, context: InlineContext, path: ConvertErrorPath): Result<Emission> {
|
function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range: NodeRange, context: InlineContext, path: ConvertErrorPath): Result<Emission> {
|
||||||
const href = mark.attrs?.['href']
|
const href = nodeAttrs(mark)['href']
|
||||||
const title = mark.attrs?.['title']
|
const title = nodeAttrs(mark)['title']
|
||||||
if (typeof href !== 'string') return success({ carry: range })
|
if (typeof href !== 'string') return success({ carry: range })
|
||||||
const node = nodes[0]
|
const node = nodes[0]
|
||||||
const bare = nodes.length === 1 && node !== undefined && node.type === 'text' && node.text === href && (node.marks ?? []).length === depth + 1
|
const bare = nodes.length === 1 && node !== undefined && node.type === 'text' && node.text === href && nodeMarks(node).length === depth + 1
|
||||||
if (bare && title === undefined && isAutolink(href) && !holdsEntityReference(href)) return success({ segments: [syntax(`<${href}>`)] })
|
if (bare && title === undefined && isAutolink(href) && !holdsEntityReference(href)) return success({ segments: [syntax(`<${href}>`)] })
|
||||||
const destination = spellDestination(href, path)
|
const destination = spellDestination(href, path)
|
||||||
if (!destination.ok) return destination
|
if (!destination.ok) return destination
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { AdfNode } from '../../adf/document.ts'
|
import type { AdfNode } from '../../adf/document.ts'
|
||||||
import { carriesOnly } from '../../adf/document.ts'
|
import { carriesOnly, nodeContent } from '../../adf/document.ts'
|
||||||
import { spellPipeDelimiter, spellPipeRow } from '../pipe-table-syntax.ts'
|
import { spellPipeDelimiter, spellPipeRow } from '../pipe-table-syntax.ts'
|
||||||
import { tryPipeCell } from './inline-line.ts'
|
import { tryPipeCell } from './inline-line.ts'
|
||||||
import type { ConvertErrorPath } from '../../result.ts'
|
import type { ConvertErrorPath } from '../../result.ts'
|
||||||
@@ -11,7 +11,7 @@ export function tryPipeTable(node: AdfNode, path: ConvertErrorPath): string | un
|
|||||||
for (const [rowIndex, row] of rows.entries()) {
|
for (const [rowIndex, row] of rows.entries()) {
|
||||||
const cells: string[] = []
|
const cells: string[] = []
|
||||||
for (const [cellIndex, paragraph] of row.entries()) {
|
for (const [cellIndex, paragraph] of row.entries()) {
|
||||||
const content = paragraph.content ?? []
|
const content = nodeContent(paragraph)
|
||||||
const line = content.length === 0 ? '' : tryPipeCell(content, [...path, 'content', rowIndex, 'content', cellIndex, 'content', 0])
|
const line = content.length === 0 ? '' : tryPipeCell(content, [...path, 'content', rowIndex, 'content', cellIndex, 'content', 0])
|
||||||
if (line === undefined) return undefined
|
if (line === undefined) return undefined
|
||||||
cells.push(line)
|
cells.push(line)
|
||||||
@@ -23,12 +23,12 @@ export function tryPipeTable(node: AdfNode, path: ConvertErrorPath): string | un
|
|||||||
}
|
}
|
||||||
|
|
||||||
function pipeRows(node: AdfNode): AdfNode[][] | undefined {
|
function pipeRows(node: AdfNode): AdfNode[][] | undefined {
|
||||||
const rows = node.content ?? []
|
const rows = nodeContent(node)
|
||||||
const columns = (rows[0]?.content ?? []).length
|
const columns = rows[0] === undefined ? 0 : nodeContent(rows[0]).length
|
||||||
if (!carriesOnly(node, []) || columns === 0) return undefined
|
if (!carriesOnly(node, []) || columns === 0) return undefined
|
||||||
const grid: AdfNode[][] = []
|
const grid: AdfNode[][] = []
|
||||||
for (const [index, row] of rows.entries()) {
|
for (const [index, row] of rows.entries()) {
|
||||||
const cells = row.content ?? []
|
const cells = nodeContent(row)
|
||||||
if (row.type !== 'tableRow' || !carriesOnly(row, []) || cells.length !== columns) return undefined
|
if (row.type !== 'tableRow' || !carriesOnly(row, []) || cells.length !== columns) return undefined
|
||||||
const wanted = index === 0 ? 'tableHeader' : 'tableCell'
|
const wanted = index === 0 ? 'tableHeader' : 'tableCell'
|
||||||
const paragraphs: AdfNode[] = []
|
const paragraphs: AdfNode[] = []
|
||||||
@@ -43,7 +43,7 @@ function pipeRows(node: AdfNode): AdfNode[][] | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function plainParagraph(cell: AdfNode): AdfNode | undefined {
|
function plainParagraph(cell: AdfNode): AdfNode | undefined {
|
||||||
const content = cell.content ?? []
|
const content = nodeContent(cell)
|
||||||
const paragraph = content[0]
|
const paragraph = content[0]
|
||||||
if (paragraph === undefined || content.length !== 1 || paragraph.type !== 'paragraph' || !carriesOnly(paragraph, [])) return undefined
|
if (paragraph === undefined || content.length !== 1 || paragraph.type !== 'paragraph' || !carriesOnly(paragraph, [])) return undefined
|
||||||
return paragraph
|
return paragraph
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { AdfMark } from '../adf/document.ts'
|
|||||||
import type { AttributeVocabulary } from '../adf/attribute-vocabulary.ts'
|
import type { AttributeVocabulary } from '../adf/attribute-vocabulary.ts'
|
||||||
import type { MarkType } from '../adf/mark-attributes.ts'
|
import type { MarkType } from '../adf/mark-attributes.ts'
|
||||||
import { isMarkType, markAttributes } from '../adf/mark-attributes.ts'
|
import { isMarkType, markAttributes } from '../adf/mark-attributes.ts'
|
||||||
|
import { nodeAttrs } from '../adf/document.ts'
|
||||||
import { spellAttributes, spellVocabulary } from './directive-syntax.ts'
|
import { spellAttributes, spellVocabulary } from './directive-syntax.ts'
|
||||||
import { vocabularyPairs } from '../adf/attribute-vocabulary.ts'
|
import { vocabularyPairs } from '../adf/attribute-vocabulary.ts'
|
||||||
|
|
||||||
@@ -30,6 +31,6 @@ export function markSpelling(type: string): MarkSpelling | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function spellMarkAttributes(mark: AdfMark, vocabulary: AttributeVocabulary): string | undefined {
|
export function spellMarkAttributes(mark: AdfMark, vocabulary: AttributeVocabulary): string | undefined {
|
||||||
const pairs = vocabularyPairs(mark.attrs ?? {}, vocabulary, [])
|
const pairs = vocabularyPairs(nodeAttrs(mark), vocabulary, [])
|
||||||
return pairs === undefined ? undefined : spellAttributes(spellVocabulary(pairs))
|
return pairs === undefined ? undefined : spellAttributes(spellVocabulary(pairs))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { BlockDirective } from '../../adf/block-directives.ts'
|
|||||||
import type { ConvertFault } from '../../result.ts'
|
import type { ConvertFault } from '../../result.ts'
|
||||||
import type { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts'
|
import type { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts'
|
||||||
import type { Elsewhere } from './directive-attributes.ts'
|
import type { Elsewhere } from './directive-attributes.ts'
|
||||||
import { attributeNestingMessage } from '../../adf/document.ts'
|
import { attributeNestingMessage, nodeMarks } from '../../adf/document.ts'
|
||||||
import { attributeValue, directiveLineEscape, inlineDirectiveEscape, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
|
import { attributeValue, directiveLineEscape, inlineDirectiveEscape, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
|
||||||
import { blockArgument } from '../block-directive-arguments.ts'
|
import { blockArgument } from '../block-directive-arguments.ts'
|
||||||
import { blockDirective } from '../../adf/block-directives.ts'
|
import { blockDirective } from '../../adf/block-directives.ts'
|
||||||
@@ -89,7 +89,7 @@ function blockSpellingFault(name: string): ConvertFault | undefined {
|
|||||||
function slotText(content: readonly AdfNode[]): string | undefined {
|
function slotText(content: readonly AdfNode[]): string | undefined {
|
||||||
if (content.length === 0) return ''
|
if (content.length === 0) return ''
|
||||||
const only = content.length === 1 ? content[0] : undefined
|
const only = content.length === 1 ? content[0] : undefined
|
||||||
if (only?.type !== 'text' || (only.marks ?? []).length > 0 || typeof only.text !== 'string') return undefined
|
if (only?.type !== 'text' || nodeMarks(only).length > 0 || typeof only.text !== 'string') return undefined
|
||||||
return only.text
|
return only.text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { delimiterFlags, matchEmphasis, runLength } from '../emphasis-matching.t
|
|||||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
import { inlineDirective } from '../../adf/inline-directives.ts'
|
import { inlineDirective } from '../../adf/inline-directives.ts'
|
||||||
import { mergeAdjacentText } from '../../adf/editor-normal.ts'
|
import { mergeAdjacentText } from '../../adf/editor-normal.ts'
|
||||||
|
import { nodeAttrs, nodeMarks } from '../../adf/document.ts'
|
||||||
import { normalizeLabel, readInlineTarget, readLabel } from '../link-syntax.ts'
|
import { normalizeLabel, readInlineTarget, readLabel } from '../link-syntax.ts'
|
||||||
import { readCarriedInline } from '../opaque-carry.ts'
|
import { readCarriedInline } from '../opaque-carry.ts'
|
||||||
import { readDirectiveMark } from './directive-marks.ts'
|
import { readDirectiveMark } from './directive-marks.ts'
|
||||||
@@ -337,7 +338,7 @@ function imageAlt(inner: readonly Piece[], path: ConvertErrorPath): Result<strin
|
|||||||
function altText(node: AdfNode): string {
|
function altText(node: AdfNode): string {
|
||||||
if (node.type === 'hardBreak') return ' '
|
if (node.type === 'hardBreak') return ' '
|
||||||
const slot = inlineDirective(node.type)?.textAttribute
|
const slot = inlineDirective(node.type)?.textAttribute
|
||||||
const spelled = slot === undefined ? undefined : node.attrs?.[slot]
|
const spelled = slot === undefined ? undefined : nodeAttrs(node)[slot]
|
||||||
return typeof spelled === 'string' ? spelled : (node.text ?? '')
|
return typeof spelled === 'string' ? spelled : (node.text ?? '')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,7 +410,7 @@ function markType(character: string, used: number): string {
|
|||||||
// A node cannot carry one mark type twice (AGENTS.md §14).
|
// A node cannot carry one mark type twice (AGENTS.md §14).
|
||||||
function applyMark(nodes: readonly AdfNode[], mark: AdfMark): AdfNode[] {
|
function applyMark(nodes: readonly AdfNode[], mark: AdfMark): AdfNode[] {
|
||||||
return nodes.map((node) => {
|
return nodes.map((node) => {
|
||||||
const marks = node.marks ?? []
|
const marks = nodeMarks(node)
|
||||||
return marks.some((carried) => carried.type === mark.type) ? node : { ...node, marks: [mark, ...marks] }
|
return marks.some((carried) => carried.type === mark.type) ? node : { ...node, marks: [mark, ...marks] }
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { failure, faulted, positioned, success, type ConvertErrorPath, type Pars
|
|||||||
import { languageSlot } from '../code-language.ts'
|
import { languageSlot } from '../code-language.ts'
|
||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
import { listBreakName, listBreakSpelling } from '../list-break.ts'
|
import { listBreakName, listBreakSpelling } from '../list-break.ts'
|
||||||
|
import { nodeAttrs } from '../../adf/document.ts'
|
||||||
import { parseBlocks } from './blocks.ts'
|
import { parseBlocks } from './blocks.ts'
|
||||||
import { parseInlineContent } from './inline-content.ts'
|
import { parseInlineContent } from './inline-content.ts'
|
||||||
import { readBlockDirectiveNode } from './directive-nodes.ts'
|
import { readBlockDirectiveNode } from './directive-nodes.ts'
|
||||||
@@ -106,7 +107,7 @@ function directiveBody(read: BlockDirectiveNode, blocks: Block[] | undefined, de
|
|||||||
function codeDirectiveNode(node: AdfNode, blocks: readonly Block[], path: ConvertErrorPath): Result<AdfNode> {
|
function codeDirectiveNode(node: AdfNode, blocks: readonly Block[], path: ConvertErrorPath): Result<AdfNode> {
|
||||||
const only = blocks.length === 1 ? blocks[0] : undefined
|
const only = blocks.length === 1 ? blocks[0] : undefined
|
||||||
if (only?.kind !== 'code') return failure('unsupported-node-shape', `${node.type} takes one code block as its body: this body is not one`, path)
|
if (only?.kind !== 'code') return failure('unsupported-node-shape', `${node.type} takes one code block as its body: this body is not one`, path)
|
||||||
const attribute = node.attrs?.['language']
|
const attribute = nodeAttrs(node)['language']
|
||||||
const fromFence = only.language !== ''
|
const fromFence = only.language !== ''
|
||||||
const slot = languageSlot(fromFence ? only.language : attribute)
|
const slot = languageSlot(fromFence ? only.language : attribute)
|
||||||
if ((slot.kind === 'fence') !== fromFence || (fromFence && attribute !== undefined)) {
|
if ((slot.kind === 'fence') !== fromFence || (fromFence && attribute !== undefined)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user