Settle one failure policy for the trial spellings, and lift the attribute vocabulary above the markdown layer
CI / gate (push) Successful in 5s
CI / gate (push) Successful in 5s
This commit is contained in:
@@ -4,6 +4,8 @@ export type AdfAttributes = { [key: string]: JsonValue }
|
||||
|
||||
export type AttributeKind = 'boolean' | 'json' | 'number' | 'string'
|
||||
|
||||
export type AttributeVocabulary = Readonly<Record<string, AttributeKind>>
|
||||
|
||||
export type AdfMark = {
|
||||
attrs?: AdfAttributes
|
||||
type: string
|
||||
|
||||
+11
-7
@@ -2,9 +2,9 @@ import type { AdfDocument, AdfNode } from './adf-document.ts'
|
||||
import type { BlockDirective } from './block-directives.ts'
|
||||
import type { JsonValue } from './json-value.ts'
|
||||
import { blockDirective, spellDirectiveHeader } from './block-directives.ts'
|
||||
import { emitImage } from './markdown-image.ts'
|
||||
import { tryImage } from './markdown-image.ts'
|
||||
import { emitInlineLine } from './markdown-inline.ts'
|
||||
import { emitPipeTable } from './markdown-pipe-table.ts'
|
||||
import { tryPipeTable } from './markdown-pipe-table.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from './result.ts'
|
||||
import { holdsNullCharacter, isThematicBreak } from './commonmark-grammar.ts'
|
||||
import { isAdfDocument } from './adf-document.ts'
|
||||
@@ -98,7 +98,11 @@ function emitBlock(node: AdfNode, path: ConvertErrorPath, depth: number): Result
|
||||
|
||||
function commonMarkLine(text: Result<string>): Result<EmittedBlock> {
|
||||
if (!text.ok) return text
|
||||
return success({ fenceColons: 0, spelling: 'commonmark', text: text.value })
|
||||
return success(commonMarkText(text.value))
|
||||
}
|
||||
|
||||
function commonMarkText(text: string): EmittedBlock {
|
||||
return { fenceColons: 0, spelling: 'commonmark', text }
|
||||
}
|
||||
|
||||
function commonMarkContainer(body: Result<EmittedBody>): Result<EmittedBlock> {
|
||||
@@ -131,15 +135,15 @@ function emitInlineBody(content: readonly AdfNode[], path: ConvertErrorPath): Re
|
||||
}
|
||||
|
||||
function emitMediaSingle(node: AdfNode, directive: BlockDirective, path: ConvertErrorPath, depth: number): Result<EmittedBlock> {
|
||||
const image = emitImage(node, path)
|
||||
const image = tryImage(node, path)
|
||||
if (image === undefined) return emitDirectiveBlock(node, directive, path, depth)
|
||||
return commonMarkLine(image)
|
||||
return success(commonMarkText(image))
|
||||
}
|
||||
|
||||
function emitTable(node: AdfNode, directive: BlockDirective, path: ConvertErrorPath, depth: number): Result<EmittedBlock> {
|
||||
const pipe = emitPipeTable(node, path)
|
||||
const pipe = tryPipeTable(node, path)
|
||||
if (pipe === undefined) return emitDirectiveBlock(node, directive, path, depth)
|
||||
return commonMarkLine(pipe)
|
||||
return success(commonMarkText(pipe))
|
||||
}
|
||||
|
||||
function emitBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number): Result<EmittedBody> {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { AdfMark, AdfNode } from './adf-document.ts'
|
||||
import type { AttributeVocabulary } from './directive-attributes.ts'
|
||||
import type { AdfMark, AdfNode, AttributeVocabulary } from './adf-document.ts'
|
||||
import type { JsonValue } from './json-value.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from './result.ts'
|
||||
import { attributeFailure, isBareToken, spellAttributes, spellJsonAttribute, vocabularyPairs } from './directive-attributes.ts'
|
||||
@@ -84,11 +83,11 @@ export function blockDirective(type: string): BlockDirective | undefined {
|
||||
export function spellDirectiveHeader(node: AdfNode, directive: BlockDirective, path: ConvertErrorPath): Result<string> {
|
||||
const argument = spellArgument(node, directive, path)
|
||||
if (!argument.ok) return argument
|
||||
const pairs = vocabularyPairs(node.attrs ?? {}, directive.attributes, directive.argument)
|
||||
if (!Array.isArray(pairs)) return attributeFailure(node.type, pairs, path)
|
||||
const spelled = vocabularyPairs(node.attrs ?? {}, directive.attributes, directive.argument)
|
||||
if (spelled.fault !== undefined) return attributeFailure(node.type, spelled.fault, path)
|
||||
const marks = node.marks ?? []
|
||||
if (marks.length > 0) pairs.push(['marks', spellJsonAttribute(markValues(marks))])
|
||||
const attributes = spellAttributes(pairs)
|
||||
if (marks.length > 0) spelled.pairs.push(['marks', spellJsonAttribute(markValues(marks))])
|
||||
const attributes = spellAttributes(spelled.pairs)
|
||||
return success(`${node.type}${argument.value}${attributes === '' ? '' : ` ${attributes}`}`)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { AdfAttributes, AttributeKind } from './adf-document.ts'
|
||||
import type { AdfAttributes, AttributeKind, AttributeVocabulary } from './adf-document.ts'
|
||||
import type { JsonValue } from './json-value.ts'
|
||||
import { failure, type ConvertErrorPath, type Result } from './result.ts'
|
||||
import { serializeCanonicalJson } from './canonical-json.ts'
|
||||
|
||||
export type AttributeFault = { key: string; kind: AttributeKind | undefined }
|
||||
|
||||
export type AttributeVocabulary = Readonly<Record<string, AttributeKind>>
|
||||
export type SpelledPairs = { fault: AttributeFault; pairs?: undefined } | { fault?: undefined; pairs: [string, string][] }
|
||||
|
||||
const bareToken = /^[A-Za-z0-9_-]+$/
|
||||
|
||||
@@ -18,17 +18,17 @@ export function attributeFailure<T>(type: string, fault: AttributeFault, path: C
|
||||
return failure('unsupported-node-shape', `the ${type} attribute ${fault.key} holds no ${fault.kind}`, path)
|
||||
}
|
||||
|
||||
export function vocabularyPairs(attrs: AdfAttributes, vocabulary: AttributeVocabulary, slot: string | undefined): AttributeFault | [string, string][] {
|
||||
export function vocabularyPairs(attrs: AdfAttributes, vocabulary: AttributeVocabulary, slot: string | undefined): SpelledPairs {
|
||||
const pairs: [string, string][] = []
|
||||
for (const [key, value] of Object.entries(attrs)) {
|
||||
if (key === slot) continue
|
||||
const kind = Object.hasOwn(vocabulary, key) ? vocabulary[key] : undefined
|
||||
if (kind === undefined) return { key, kind: undefined }
|
||||
if (kind === undefined) return { fault: { key, kind: undefined } }
|
||||
const spelled = spellAttributeValue(value, kind)
|
||||
if (spelled === undefined) return { key, kind }
|
||||
if (spelled === undefined) return { fault: { key, kind } }
|
||||
pairs.push([key, spelled])
|
||||
}
|
||||
return pairs
|
||||
return { pairs }
|
||||
}
|
||||
|
||||
export function spellAttributes(pairs: readonly (readonly [string, string])[]): string {
|
||||
|
||||
+13
-11
@@ -1,5 +1,5 @@
|
||||
import type { AdfMark, AdfNode } from './adf-document.ts'
|
||||
import type { AttributeVocabulary } from './directive-attributes.ts'
|
||||
import type { AdfMark, AdfNode, AttributeVocabulary } from './adf-document.ts'
|
||||
import type { AttributeFault } from './directive-attributes.ts'
|
||||
import { failure, success, type ConvertErrorPath, type Result } from './result.ts'
|
||||
import { attributeFailure, spellAttributes, vocabularyPairs } from './directive-attributes.ts'
|
||||
|
||||
@@ -45,16 +45,18 @@ export function markDirective(type: string): AttributeVocabulary | undefined {
|
||||
}
|
||||
|
||||
export function spellInlineNodeAttributes(node: AdfNode, directive: InlineDirective, path: ConvertErrorPath): Result<string> {
|
||||
const pairs = vocabularyPairs(node.attrs ?? {}, directive.attributes, directive.slot)
|
||||
if (!Array.isArray(pairs)) return attributeFailure(node.type, pairs, path)
|
||||
return success(spellAttributes(pairs))
|
||||
const spelled = vocabularyPairs(node.attrs ?? {}, directive.attributes, directive.slot)
|
||||
if (spelled.fault !== undefined) return attributeFailure(node.type, spelled.fault, path)
|
||||
return success(spellAttributes(spelled.pairs))
|
||||
}
|
||||
|
||||
export function spellMarkAttributes(mark: AdfMark, vocabulary: AttributeVocabulary, path: ConvertErrorPath): Result<string> {
|
||||
const pairs = vocabularyPairs(mark.attrs ?? {}, vocabulary, undefined)
|
||||
if (!Array.isArray(pairs)) {
|
||||
if (pairs.kind === undefined) return failure('unspellable-mark', `the ${mark.type} spelling holds no ${pairs.key} attribute`, path)
|
||||
return failure('unspellable-mark', `the ${mark.type} attribute ${pairs.key} holds no ${pairs.kind}`, path)
|
||||
}
|
||||
return success(spellAttributes(pairs))
|
||||
const spelled = vocabularyPairs(mark.attrs ?? {}, vocabulary, undefined)
|
||||
if (spelled.fault !== undefined) return markFailure(mark.type, spelled.fault, path)
|
||||
return success(spellAttributes(spelled.pairs))
|
||||
}
|
||||
|
||||
function markFailure<T>(type: string, fault: AttributeFault, path: ConvertErrorPath): Result<T> {
|
||||
if (fault.kind === undefined) return failure('unspellable-mark', `the ${type} spelling holds no ${fault.key} attribute`, path)
|
||||
return failure('unspellable-mark', `the ${type} attribute ${fault.key} holds no ${fault.kind}`, path)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
import type { AdfNode } from './adf-document.ts'
|
||||
import type { ConvertErrorPath, Result } from './result.ts'
|
||||
import { emitImageLine } from './markdown-inline.ts'
|
||||
import type { ConvertErrorPath } from './result.ts'
|
||||
import { serializeCanonicalJson } from './canonical-json.ts'
|
||||
import { tryImageLine } from './markdown-inline.ts'
|
||||
|
||||
const centeredMediaSingle = '{"layout":"center"}'
|
||||
const imageAttributes = ['alt', 'type', 'url']
|
||||
|
||||
export function emitImage(node: AdfNode, path: ConvertErrorPath): Result<string> | undefined {
|
||||
export function tryImage(node: AdfNode, path: ConvertErrorPath): string | undefined {
|
||||
const image = imageShape(node)
|
||||
if (image === undefined) return undefined
|
||||
const line = emitImageLine(image.alt, image.url, [...path, 'content', 0])
|
||||
return line.ok ? line : undefined
|
||||
return tryImageLine(image.alt, image.url, [...path, 'content', 0])
|
||||
}
|
||||
|
||||
function imageShape(node: AdfNode): { alt: string | undefined; url: string } | undefined {
|
||||
|
||||
+18
-16
@@ -7,7 +7,7 @@ import { claimsLine, holdsControlCharacter, holdsEntityReference, holdsNullChara
|
||||
import { failure, success, type ConvertErrorPath, type Result } from './result.ts'
|
||||
import { longestBacktickRun } from './backtick-runs.ts'
|
||||
import { serializeCanonicalJson } from './canonical-json.ts'
|
||||
import { spellStringAttribute } from './directive-attributes.ts'
|
||||
import { spellAttributes, spellStringAttribute } from './directive-attributes.ts'
|
||||
|
||||
type Brackets = 'directive' | 'link' | 'none'
|
||||
|
||||
@@ -29,23 +29,21 @@ export function emitInlineLine(nodes: readonly AdfNode[], container: LineContain
|
||||
return finishLine(segments.value, container, path)
|
||||
}
|
||||
|
||||
// undefined where the cell holds a pipe no backslash reaches, leaving the table its directive form.
|
||||
export function emitPipeCell(nodes: readonly AdfNode[], path: ConvertErrorPath): Result<string> | undefined {
|
||||
export function tryPipeCell(nodes: readonly AdfNode[], path: ConvertErrorPath): string | undefined {
|
||||
const segments = lineSegments(nodes, 'table-cell', path)
|
||||
if (!segments.ok) return segments
|
||||
if (!segments.ok) return undefined
|
||||
if (segments.value.some((segment) => segment.escaping === 'none' && segment.text.includes('|'))) return undefined
|
||||
return finishLine(segments.value, 'table-cell', path)
|
||||
const line = finishLine(segments.value, 'table-cell', path)
|
||||
return line.ok ? line.value : undefined
|
||||
}
|
||||
|
||||
export function emitImageLine(alt: string | undefined, href: string, path: ConvertErrorPath): Result<string> {
|
||||
if (alt !== undefined && /^[ \t]|[ \t]$|[\n\r]/.test(alt)) {
|
||||
return failure('unspellable-whitespace', 'a media alt holds whitespace no image description spells', path)
|
||||
}
|
||||
if (alt !== undefined && holdsNullCharacter(alt)) return failure('unspellable-character', 'a media alt holds a null character CommonMark replaces', path)
|
||||
export function tryImageLine(alt: string | undefined, href: string, path: ConvertErrorPath): string | undefined {
|
||||
if (alt !== undefined && (/^[ \t]|[ \t]$|[\n\r]/.test(alt) || holdsNullCharacter(alt))) return undefined
|
||||
const destination = spellDestination(href, path)
|
||||
if (!destination.ok) return destination
|
||||
if (!destination.ok) return undefined
|
||||
const description: InlineSegment[] = alt === undefined ? [] : [{ escaping: 'bracketed', text: alt }]
|
||||
return finishLine([syntax('`)], 'paragraph', path)
|
||||
const line = finishLine([syntax('`)], 'paragraph', path)
|
||||
return line.ok ? line.value : undefined
|
||||
}
|
||||
|
||||
function lineSegments(nodes: readonly AdfNode[], container: LineContainer, path: ConvertErrorPath): Result<InlineSegment[]> {
|
||||
@@ -71,7 +69,7 @@ function finishLine(segments: readonly InlineSegment[], container: LineContainer
|
||||
return success(line)
|
||||
}
|
||||
|
||||
// spec/flavour.md, Inline nodes: whitespace CommonMark strips at a line edge rides the reserved text directive.
|
||||
// spec/flavour.md, Inline nodes: whitespace CommonMark strips.
|
||||
function carryStrippedWhitespace(segments: readonly InlineSegment[]): InlineSegment[] {
|
||||
const carried: InlineSegment[] = []
|
||||
for (const [index, segment] of segments.entries()) {
|
||||
@@ -98,7 +96,11 @@ function carryEdges(segment: InlineSegment, leading: boolean, trailing: boolean)
|
||||
}
|
||||
|
||||
function carriedText(text: string): InlineSegment {
|
||||
return { escaping: 'attribute', text: `:text{text=${spellStringAttribute(text)}}` }
|
||||
return { escaping: 'attribute', text: spellLeafDirective('text', spellAttributes([['text', spellStringAttribute(text)]])) }
|
||||
}
|
||||
|
||||
function spellLeafDirective(name: string, attributes: string): string {
|
||||
return `:${name}${attributes === '' ? '{}' : attributes}`
|
||||
}
|
||||
|
||||
function syntax(text: string): InlineSegment {
|
||||
@@ -164,7 +166,7 @@ function emitHardBreak(node: AdfNode, context: InlineContext, path: ConvertError
|
||||
const empty = refuseContentAndText(node, path)
|
||||
if (!empty.ok) return empty
|
||||
if (context.container === 'paragraph' && !context.atBlockEnd && context.brackets !== 'directive') return success([syntax('\\\n')])
|
||||
return success([syntax(':hardBreak{}')])
|
||||
return success([syntax(spellLeafDirective('hardBreak', ''))])
|
||||
}
|
||||
|
||||
function emitInlineDirective(node: AdfNode, directive: InlineDirective, path: ConvertErrorPath): Result<InlineSegment[]> {
|
||||
@@ -173,7 +175,7 @@ function emitInlineDirective(node: AdfNode, directive: InlineDirective, path: Co
|
||||
const attributes = spellInlineNodeAttributes(node, directive, path)
|
||||
if (!attributes.ok) return attributes
|
||||
const slot = directive.slot === undefined ? undefined : node.attrs?.[directive.slot]
|
||||
if (slot === undefined) return success([syntax(`:${node.type}${attributes.value === '' ? '{}' : attributes.value}`)])
|
||||
if (slot === undefined) return success([syntax(spellLeafDirective(node.type, attributes.value))])
|
||||
if (typeof slot !== 'string') return failure('unsupported-node-shape', `the ${node.type} attribute ${directive.slot} holds no string`, path)
|
||||
if (/[\n\r]/.test(slot)) return failure('unspellable-whitespace', `a ${node.type} content slot holds a newline no inline directive spans`, path)
|
||||
if (holdsNullCharacter(slot)) return failure('unspellable-character', `a ${node.type} content slot holds a null character CommonMark replaces`, path)
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { AdfNode } from './adf-document.ts'
|
||||
import { emitPipeCell } from './markdown-inline.ts'
|
||||
import { success, type ConvertErrorPath, type Result } from './result.ts'
|
||||
import { tryPipeCell } from './markdown-inline.ts'
|
||||
import type { ConvertErrorPath } from './result.ts'
|
||||
|
||||
export function emitPipeTable(node: AdfNode, path: ConvertErrorPath): Result<string> | undefined {
|
||||
export function tryPipeTable(node: AdfNode, path: ConvertErrorPath): string | undefined {
|
||||
const rows = pipeRows(node)
|
||||
if (rows === undefined) return undefined
|
||||
const lines: string[] = []
|
||||
@@ -10,15 +10,14 @@ export function emitPipeTable(node: AdfNode, path: ConvertErrorPath): Result<str
|
||||
const cells: string[] = []
|
||||
for (const [cellIndex, paragraph] of row.entries()) {
|
||||
const content = paragraph.content ?? []
|
||||
const line = content.length === 0 ? success('') : emitPipeCell(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.ok) return line
|
||||
cells.push(line.value)
|
||||
cells.push(line)
|
||||
}
|
||||
lines.push(`| ${cells.join(' | ')} |`)
|
||||
if (rowIndex === 0) lines.push(`| ${cells.map(() => '---').join(' | ')} |`)
|
||||
}
|
||||
return success(lines.join('\n'))
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
function pipeRows(node: AdfNode): AdfNode[][] | undefined {
|
||||
|
||||
Reference in New Issue
Block a user