mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-15 13:00:26 +01:00
Give each markdown input its own reader: commonMarkToAdf, losslessMarkdownToAdf and portableMarkdownToAdf, each claiming only its flavour
This commit is contained in:
@@ -3,14 +3,15 @@ import test from 'node:test'
|
||||
|
||||
import type { Block } from './blocks.ts'
|
||||
import type { LinkDefinition } from '../commonmark/link-syntax.ts'
|
||||
import { flavourClaims } from '../portable/conventions.ts'
|
||||
import { parseBlocks } from './blocks.ts'
|
||||
|
||||
function definitions(markdown: string): [string, LinkDefinition][] {
|
||||
return [...parseBlocks(markdown).definitions]
|
||||
return [...parseBlocks(markdown, flavourClaims.lossless).definitions]
|
||||
}
|
||||
|
||||
function kinds(markdown: string): string[] {
|
||||
return parseBlocks(markdown).blocks.map((block) => block.kind)
|
||||
return parseBlocks(markdown, flavourClaims.lossless).blocks.map((block) => block.kind)
|
||||
}
|
||||
|
||||
function faults(markdown: string): string[] {
|
||||
@@ -23,7 +24,7 @@ function faults(markdown: string): string[] {
|
||||
if (block.kind === 'bulletList' || block.kind === 'orderedList') for (const item of block.items) walk(item)
|
||||
}
|
||||
}
|
||||
walk(parseBlocks(markdown).blocks)
|
||||
walk(parseBlocks(markdown, flavourClaims.lossless).blocks)
|
||||
return messages
|
||||
}
|
||||
|
||||
@@ -90,7 +91,7 @@ test('opens a container where the content model takes content, and holds it open
|
||||
assert.deepEqual(faults('!adf:panel info\n> Part.\n!adf:/panel\n'), [])
|
||||
assert.deepEqual(faults('!adf:panel info\n- !adf:panel warning\n Part.\n !adf:/panel\n!adf:/panel\n'), [])
|
||||
assert.deepEqual(faults('!adf:panel info\n```\n!adf:/panel\n```\n!adf:/panel\n'), [])
|
||||
assert.deepEqual(parseBlocks('!adf:panel info {panelColor="#ff0000"}\nPart.\n!adf:/panel\n').blocks, [
|
||||
assert.deepEqual(parseBlocks('!adf:panel info {panelColor="#ff0000"}\nPart.\n!adf:/panel\n', flavourClaims.lossless).blocks, [
|
||||
{
|
||||
argument: 'info',
|
||||
attributes: new Map([['panelColor', { decoded: '#ff0000', spelling: '"#ff0000"' }]]),
|
||||
@@ -100,7 +101,7 @@ test('opens a container where the content model takes content, and holds it open
|
||||
position: { line: 1, offset: 0 },
|
||||
},
|
||||
])
|
||||
assert.deepEqual(parseBlocks('!adf:rule\n').blocks, [
|
||||
assert.deepEqual(parseBlocks('!adf:rule\n', flavourClaims.lossless).blocks, [
|
||||
{ argument: undefined, attributes: new Map(), blocks: undefined, kind: 'directive', name: 'rule', position: { line: 1, offset: 0 } },
|
||||
])
|
||||
})
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { Claims } from '../portable/conventions.ts'
|
||||
import type { ConvertFault, SourcePosition } from '../../result.ts'
|
||||
import type { DirectiveAttributes, DirectiveLine } from '../directive-syntax.ts'
|
||||
import type { LinkDefinition } from '../commonmark/link-syntax.ts'
|
||||
@@ -73,6 +74,7 @@ type ContainerStack = {
|
||||
}
|
||||
|
||||
type Walk = ParsedBlocks & {
|
||||
claims: Claims
|
||||
leaf: OpenLeaf | undefined
|
||||
leafOpeners: Map<Block[], Map<string, LeafOpener>>
|
||||
position: SourcePosition
|
||||
@@ -83,9 +85,10 @@ const indentedCodeColumns = 4
|
||||
const largestOpenerIndentation = 3
|
||||
const tabStop = 4
|
||||
|
||||
export function parseBlocks(markdown: string): ParsedBlocks {
|
||||
export function parseBlocks(markdown: string, claims: Claims): ParsedBlocks {
|
||||
const walk: Walk = {
|
||||
blocks: [],
|
||||
claims,
|
||||
definitions: new Map(),
|
||||
leaf: undefined,
|
||||
leafOpeners: new Map(),
|
||||
@@ -336,7 +339,7 @@ function continuesLazily(walk: Walk, line: Line): boolean {
|
||||
if (walk.leaf?.kind !== 'paragraph' || isBlankLine(line.text)) return false
|
||||
if (leadingColumns(line) >= indentedCodeColumns) return true
|
||||
const opener = removeColumns(line, largestOpenerIndentation).text
|
||||
if (readDirectiveLine(opener) !== undefined || claimsPipeLine(opener) || isThematicBreak(opener)) return false
|
||||
if ((walk.claims.directives && readDirectiveLine(opener) !== undefined) || (walk.claims.pipeTables && claimsPipeLine(opener)) || isThematicBreak(opener)) return false
|
||||
return atxHeading(opener) === undefined && openingCodeFence(opener) === undefined && openingHtmlBlock(opener, true) === undefined
|
||||
}
|
||||
|
||||
@@ -388,14 +391,14 @@ function readIndentedCodeLine(leaf: Extract<OpenLeaf, { kind: 'indented-code' }>
|
||||
|
||||
function openLeaf(walk: Walk, line: Line): void {
|
||||
const opener = removeColumns(line, largestOpenerIndentation).text
|
||||
const directive = readDirectiveLine(opener)
|
||||
const directive = walk.claims.directives ? readDirectiveLine(opener) : undefined
|
||||
if (directive !== undefined) {
|
||||
closeLeaf(walk)
|
||||
if (directive.fault === undefined) applyDirectiveLine(walk, directive.value)
|
||||
else pushFault(walk, directive.fault)
|
||||
return
|
||||
}
|
||||
const cells = pipeCells(opener)
|
||||
const cells = walk.claims.pipeTables ? pipeCells(opener) : undefined
|
||||
if (cells !== undefined) {
|
||||
closeLeaf(walk)
|
||||
walk.leaf = { kind: 'pipe-table', position: walk.position, rows: [cells] }
|
||||
@@ -424,7 +427,7 @@ function readLineBlock(walk: Walk, opener: string): boolean {
|
||||
if (level !== undefined) {
|
||||
const paragraph = takeParagraph(walk)
|
||||
if (paragraph !== undefined) {
|
||||
currentBlocks(walk).push(bareTableFault(paragraph) ?? { kind: 'heading', level, position: paragraph.position, text: paragraph.text })
|
||||
currentBlocks(walk).push(bareTableFault(paragraph, walk.claims) ?? { kind: 'heading', level, position: paragraph.position, text: paragraph.text })
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -456,7 +459,7 @@ function closeLeaf(walk: Walk): void {
|
||||
if (leaf === undefined) return
|
||||
if (leaf.kind === 'paragraph') {
|
||||
const paragraph = takeParagraph(walk)
|
||||
if (paragraph !== undefined) currentBlocks(walk).push(bareTableFault(paragraph) ?? paragraph)
|
||||
if (paragraph !== undefined) currentBlocks(walk).push(bareTableFault(paragraph, walk.claims) ?? paragraph)
|
||||
return
|
||||
}
|
||||
walk.leaf = undefined
|
||||
@@ -480,7 +483,8 @@ function pipeTableBlock(rows: readonly [string[], ...string[][]], position: Sour
|
||||
}
|
||||
|
||||
// spec/flavour.md, Tables: GFM's table without the leading pipes, which no line of it claims.
|
||||
function bareTableFault(paragraph: Extract<Block, { kind: 'paragraph' }>): Block | undefined {
|
||||
function bareTableFault(paragraph: Extract<Block, { kind: 'paragraph' }>, claims: Claims): Block | undefined {
|
||||
if (!claims.pipeTables) return undefined
|
||||
let header: string[] | undefined
|
||||
for (const line of paragraph.text.split('\n')) {
|
||||
const cells = barePipeCells(line)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import type { Claims } from '../portable/conventions.ts'
|
||||
import type { DirectiveSpan, NestedSpans } from '../directive-syntax.ts'
|
||||
import type { EmphasisPairing } from '../commonmark/emphasis-matching.ts'
|
||||
import type { Flavour } from '../plain/conventions.ts'
|
||||
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'
|
||||
@@ -9,7 +9,7 @@ import { backtickRun, closingBacktickRun } from '../commonmark/backtick-runs.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'
|
||||
import { highlightDelimiter, highlightFlanking } from '../plain/conventions.ts'
|
||||
import { highlightDelimiter, highlightFlanking } from '../portable/conventions.ts'
|
||||
import { identicalMarks, mergeAdjacentText, nodeAttrs, nodeMarks } from '../../adf/document.ts'
|
||||
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
|
||||
import { joinsWhenRead, textBreakName, textBreakSpelling } from '../adjacent-text.ts'
|
||||
@@ -54,11 +54,11 @@ type Run = { canClose: boolean; canOpen: boolean; character: string; index: numb
|
||||
|
||||
// `container` is `undefined` inside a directive's content slot, the emitter's `bracketed`.
|
||||
type Scan = {
|
||||
claims: Claims
|
||||
container: LineContainer | undefined
|
||||
// Pieces below this have been walked for openers to deactivate: an image close folds the link-marked piece into alt text, leaving this the only record that the brackets around it are doomed.
|
||||
deactivatedBefore: number
|
||||
definitions: LinkDefinitions
|
||||
highlights: boolean
|
||||
openingSpellableLink: boolean
|
||||
path: ConvertErrorPath
|
||||
pending: string
|
||||
@@ -81,8 +81,8 @@ const spellableLink = 'link takes the directive form only where CommonMark canno
|
||||
const textBreak: TextBreak = { kind: 'textBreak' }
|
||||
|
||||
// The outermost content: only here does a text break see both its neighbours.
|
||||
export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer, flavour: Flavour): Result<InlineContent> {
|
||||
const scan = freshScan(source, { definitions, path }, { container, highlights: flavour === 'plain', spans: noSpans })
|
||||
export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer, claims: Claims): Result<InlineContent> {
|
||||
const scan = freshScan(source, { definitions, path }, { claims, container, spans: noSpans })
|
||||
const scanned = scanInline(scan)
|
||||
if (!scanned.ok) return scanned
|
||||
if (scanned.value.image !== undefined) return success({ image: scanned.value.image })
|
||||
@@ -96,7 +96,7 @@ export function parseInlineContent(source: string, definitions: LinkDefinitions,
|
||||
return success({ nodes: nodes.value })
|
||||
}
|
||||
|
||||
function freshScan(source: string, shared: SharedScan, own: Pick<Scan, 'container' | 'highlights' | 'spans'>): Scan {
|
||||
function freshScan(source: string, shared: SharedScan, own: Pick<Scan, 'claims' | 'container' | 'spans'>): Scan {
|
||||
return { ...shared, ...own, deactivatedBefore: 0, openingSpellableLink: false, pending: '', pieces: [], source }
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ function scanInline(scan: Scan): Result<Scanned> {
|
||||
break
|
||||
}
|
||||
case '!': {
|
||||
const directive = readDirective(scan, index)
|
||||
const directive = scan.claims.directives ? readDirective(scan, index) : undefined
|
||||
if (directive === undefined) {
|
||||
index = openBracket(scan, index)
|
||||
break
|
||||
@@ -276,8 +276,8 @@ function refuseLinkDirective(scan: Scan, mark: AdfMark, nodes: readonly Inline[]
|
||||
|
||||
function slotContent(scan: Scan, span: DirectiveSpan): Result<SlotContent | undefined> {
|
||||
if (span.content === undefined) return success(undefined)
|
||||
const { definitions, path } = scan
|
||||
const parsed = scanInline(freshScan(span.content, { definitions, path }, { container: undefined, highlights: false, spans: span.spans }))
|
||||
const { claims, definitions, path } = scan
|
||||
const parsed = scanInline(freshScan(span.content, { definitions, path }, { claims, container: undefined, spans: span.spans }))
|
||||
if (!parsed.ok) return parsed
|
||||
if (parsed.value.image !== undefined) return failure('unmappable-image', imageAlone, scan.path)
|
||||
return success(parsed.value)
|
||||
@@ -357,7 +357,7 @@ function readDelimiterRun(scan: Scan, index: number): number {
|
||||
const character = scan.source.charAt(index)
|
||||
const length = runLength(scan.source, index)
|
||||
const flags = delimiterFlags(character, scan.source.charAt(index - 1), scan.source.charAt(index + length))
|
||||
if ((character === '~' && length !== 2) || (!flags.canOpen && !flags.canClose)) scan.pending += scan.source.slice(index, index + length)
|
||||
if ((character === '~' && (!scan.claims.strikethrough || length !== 2)) || (!flags.canOpen && !flags.canClose)) scan.pending += scan.source.slice(index, index + length)
|
||||
else {
|
||||
flush(scan, false)
|
||||
scan.pieces.push({ canClose: flags.canClose, canOpen: flags.canOpen, character, kind: 'run', length })
|
||||
@@ -366,7 +366,7 @@ function readDelimiterRun(scan: Scan, index: number): number {
|
||||
}
|
||||
|
||||
function readEquals(scan: Scan, index: number): number {
|
||||
if (!scan.highlights || !scan.source.startsWith(highlightDelimiter, index)) {
|
||||
if (!scan.claims.highlights || !scan.source.startsWith(highlightDelimiter, index)) {
|
||||
scan.pending += '='
|
||||
return index + 1
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
||||
import type { Block, DirectiveBlock } from './blocks.ts'
|
||||
import type { BlockDirectiveNode } from './directive-nodes.ts'
|
||||
import type { Claims } from '../portable/conventions.ts'
|
||||
import type { ConvertFault } from '../../result.ts'
|
||||
import type { Flavour } from '../plain/conventions.ts'
|
||||
import type { LineContainer } from '../line-container.ts'
|
||||
import type { LinkDefinitions } from './inline-content.ts'
|
||||
import { carryFenceType, readCarriedBlock } from '../opaque-carry.ts'
|
||||
@@ -10,11 +10,10 @@ import { commonMarkSpelling, type SpellingMemo } from '../emit/adf-to-markdown.t
|
||||
import { documentAttribute, documentName, documentSpelling, listBreakName, listBreakSpelling } from '../block-directive.ts'
|
||||
import { emptyKeys, nodeAttrs, nodeContent } from '../../adf/document.ts'
|
||||
import { failure, faulted, positioned, success, type ConvertErrorPath, type ParseError, type Result, type SourcePosition } from '../../result.ts'
|
||||
import { inlineLeaves } from '../plain/inline-reduction.ts'
|
||||
import { flavourClaims, leadingMarker, readAlertMarker, readTaskMarker } from '../portable/conventions.ts'
|
||||
import { languageSlot } from '../code-language.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { leadingMarker, readAlertMarker, readTaskMarker } from '../plain/conventions.ts'
|
||||
import { mintTaskIds } from '../plain/task-ids.ts'
|
||||
import { mintTaskIds } from '../portable/task-ids.ts'
|
||||
import { parseBlocks } from './blocks.ts'
|
||||
import { parseInlineContent } from './inline-content.ts'
|
||||
import { readBlockDirectiveNode } from './directive-nodes.ts'
|
||||
@@ -24,32 +23,36 @@ import { spellsEmpty } from '../empty-keys.ts'
|
||||
type Paragraph = Extract<Block, { kind: 'paragraph' }>
|
||||
|
||||
// `inExpand` is whether an expand holds the blocks, which makes a folded callout a nestedExpand.
|
||||
type Reading = { carried: Set<AdfNode>; definitions: LinkDefinitions; flavour: Flavour; inExpand: boolean; memo: SpellingMemo }
|
||||
type Reading = { claims: Claims; definitions: LinkDefinitions; inExpand: boolean; memo: SpellingMemo }
|
||||
|
||||
const imageAfterMarker = 'an image fits only as a paragraph of its own: this one continues the paragraph a marker opens, which a blank line before it ends'
|
||||
const imageOnMarkerLine = 'an image fits only as a paragraph of its own: this one shares a line with a marker'
|
||||
|
||||
export function markdownToAdf(markdown: string): Result<AdfDocument, ParseError> {
|
||||
return readDocument(markdown, 'lossless')
|
||||
export function commonMarkToAdf(markdown: string): Result<AdfDocument, ParseError> {
|
||||
return readDocument(markdown, flavourClaims.commonmark)
|
||||
}
|
||||
|
||||
export function plainMarkdownToAdf(markdown: string): Result<AdfDocument, ParseError> {
|
||||
return readDocument(markdown, 'plain')
|
||||
export function losslessMarkdownToAdf(markdown: string): Result<AdfDocument, ParseError> {
|
||||
return readDocument(markdown, flavourClaims.lossless)
|
||||
}
|
||||
|
||||
function readDocument(markdown: string, flavour: Flavour): Result<AdfDocument, ParseError> {
|
||||
export function portableMarkdownToAdf(markdown: string): Result<AdfDocument, ParseError> {
|
||||
return readDocument(markdown, flavourClaims.portable)
|
||||
}
|
||||
|
||||
function readDocument(markdown: string, claims: Claims): Result<AdfDocument, ParseError> {
|
||||
if (typeof markdown !== 'string') return positioned(failure('not-a-string', `markdown is a string: found ${typeName(markdown)}`, []), { line: 1, offset: 0 })
|
||||
const parsed = parseBlocks(markdown)
|
||||
const parsed = parseBlocks(markdown, claims)
|
||||
const [only] = parsed.blocks
|
||||
if (parsed.blocks.length === 1 && only?.kind === 'directive' && only.name === documentName) {
|
||||
const fault = documentFault(only)
|
||||
return fault === undefined ? success({ type: 'doc', version: 1 }) : positioned(faulted(fault, []), only.position)
|
||||
}
|
||||
const reading: Reading = { carried: new Set(), definitions: parsed.definitions, flavour, inExpand: false, memo: new Map() }
|
||||
const reading: Reading = { claims, definitions: parsed.definitions, inExpand: false, memo: new Map() }
|
||||
const content = positioned(readBlocks(parsed.blocks, reading, [], 0), { line: 1, offset: 0 })
|
||||
if (!content.ok) return content
|
||||
const document: AdfDocument = { content: content.value, type: 'doc', version: 1 }
|
||||
if (flavour === 'plain') mintTaskIds(document, markdown, reading.carried)
|
||||
if (claims.taskMarkers) mintTaskIds(document, markdown)
|
||||
return success(document)
|
||||
}
|
||||
|
||||
@@ -107,9 +110,9 @@ function partsFault(): ConvertFault {
|
||||
function readBlock(block: Block, reading: Reading, path: ConvertErrorPath, depth: number): Result<AdfNode> {
|
||||
switch (block.kind) {
|
||||
case 'blockquote':
|
||||
return reading.flavour === 'plain' ? quoteNode(block.blocks, reading, path, depth) : containerNode({ type: 'blockquote' }, block.blocks, reading, path, depth)
|
||||
return reading.claims.alerts ? quoteNode(block.blocks, reading, path, depth) : containerNode({ type: 'blockquote' }, block.blocks, reading, path, depth)
|
||||
case 'bulletList':
|
||||
return reading.flavour === 'plain' ? bulletNode(block.items, reading, path, depth) : listNode({ type: 'bulletList' }, block.items, reading, path, depth)
|
||||
return reading.claims.taskMarkers ? bulletNode(block.items, reading, path, depth) : listNode({ type: 'bulletList' }, block.items, reading, path, depth)
|
||||
case 'code':
|
||||
return codeBlockNode(block.language, block.text, reading, path, depth)
|
||||
case 'directive':
|
||||
@@ -155,16 +158,10 @@ function quoteNode(blocks: readonly Block[], reading: Reading, path: ConvertErro
|
||||
const { folded, panelType } = led.marker
|
||||
const { line, rest } = markerLine(led.text)
|
||||
if (!folded) return filledNode({ attrs: { panelType }, type: 'panel' }, readMarked(paragraphsOf(led.position, line, rest), line !== '', body, reading, path, depth))
|
||||
const title = parseInlineContent(line, reading.definitions, path, 'paragraph', 'lossless')
|
||||
const title = parseInlineContent(line, reading.definitions, path, 'paragraph', reading.claims)
|
||||
if (!title.ok) return title
|
||||
if (title.value.image !== undefined) return failure('unmappable-image', imageOnMarkerLine, path)
|
||||
const leaves: AdfNode[] = []
|
||||
for (const [index, node] of title.value.nodes.entries()) {
|
||||
const held = node.text === undefined ? inlineLeaves([node], 'paragraph', [...path, 'content', index], depth) : success([node])
|
||||
if (!held.ok) return held
|
||||
for (const leaf of held.value) leaves.push(leaf)
|
||||
}
|
||||
const text = titleText(leaves)
|
||||
const text = titleText(title.value.nodes)
|
||||
const type = reading.inExpand ? 'nestedExpand' : 'expand'
|
||||
return filledNode(text === '' ? { type } : { attrs: { title: text }, type }, readMarked(paragraphsOf(led.position, rest), false, body, { ...reading, inExpand: true }, path, depth))
|
||||
}
|
||||
@@ -231,8 +228,7 @@ function bulletNode(items: readonly Block[][], reading: Reading, path: ConvertEr
|
||||
function directiveNode(block: DirectiveBlock, reading: Reading, path: ConvertErrorPath, depth: number): Result<AdfNode> {
|
||||
const read = readBlockDirectiveNode(block.name, block.argument, block.attributes, path)
|
||||
if (!read.ok) return read
|
||||
const inExpand = reading.inExpand || read.value.node.type === 'expand' || read.value.node.type === 'nestedExpand'
|
||||
const built = directiveBody(read.value, block.blocks, { ...reading, inExpand }, path, depth)
|
||||
const built = directiveBody(read.value, block.blocks, reading, path, depth)
|
||||
if (!built.ok) return built
|
||||
const readable = commonMarkSpelling(built.value, path, depth, { flavour: 'lossless', memo: reading.memo })
|
||||
if (readable === undefined) return built
|
||||
@@ -263,7 +259,7 @@ function codeDirectiveNode(node: AdfNode, blocks: readonly Block[], path: Conver
|
||||
if (fences.length > 1 && fences.some((fence) => fence.text === '')) return failure('unsupported-node-shape', `delete the empty fence: a fence beside another holds code, and this one holds none`, path)
|
||||
const attribute = nodeAttrs(node)['language']
|
||||
const fromFence = first.language !== ''
|
||||
const slot = languageSlot(fromFence ? first.language : attribute)
|
||||
const slot = languageSlot(fromFence ? first.language : attribute, flavourClaims.lossless)
|
||||
if (!fromFence && slot.kind === 'fence') {
|
||||
return failure('unsupported-node-shape', `move language=${spellStringAttribute(slot.info)} to the fences' info strings: they can spell this language`, path)
|
||||
}
|
||||
@@ -321,12 +317,10 @@ function listNode(node: AdfNode, items: readonly Block[][], reading: Reading, pa
|
||||
}
|
||||
|
||||
function codeBlockNode(language: string, text: string, reading: Reading, path: ConvertErrorPath, depth: number): Result<AdfNode> {
|
||||
const carriedType = carryFenceType(language)
|
||||
const carriedType = reading.claims.carryFence ? carryFenceType(language) : undefined
|
||||
if (carriedType !== undefined) {
|
||||
const carried = readCarriedBlock(carriedType, text, depth)
|
||||
if (carried.fault !== undefined) return faulted(carried.fault, path)
|
||||
reading.carried.add(carried.value)
|
||||
return success(carried.value)
|
||||
return carried.fault === undefined ? success(carried.value) : faulted(carried.fault, path)
|
||||
}
|
||||
const node: AdfNode = language === '' ? { type: 'codeBlock' } : { attrs: { language }, type: 'codeBlock' }
|
||||
return success(text === '' ? node : { ...node, content: [{ text, type: 'text' }] })
|
||||
@@ -334,14 +328,14 @@ function codeBlockNode(language: string, text: string, reading: Reading, path: C
|
||||
|
||||
// spec/flavour.md, The CommonMark image: only a plain paragraph gives an image the block it needs.
|
||||
function paragraphNode(text: string, reading: Reading, path: ConvertErrorPath): Result<AdfNode> {
|
||||
const content = parseInlineContent(text, reading.definitions, path, 'paragraph', reading.flavour)
|
||||
const content = parseInlineContent(text, reading.definitions, path, 'paragraph', reading.claims)
|
||||
if (!content.ok) return content
|
||||
const image = content.value.image
|
||||
return success(image === undefined ? withContent({ type: 'paragraph' }, content.value.nodes) : image)
|
||||
}
|
||||
|
||||
function contentNode(node: AdfNode, text: string, reading: Reading, path: ConvertErrorPath, container: LineContainer): Result<AdfNode> {
|
||||
const content = parseInlineContent(text, reading.definitions, path, container, reading.flavour)
|
||||
const content = parseInlineContent(text, reading.definitions, path, container, reading.claims)
|
||||
if (!content.ok) return content
|
||||
if (content.value.image !== undefined) return failure('unmappable-image', `no ADF node carries an image inside a ${node.type}`, path)
|
||||
return success(withContent(node, content.value.nodes))
|
||||
|
||||
+12
-40
@@ -2,10 +2,9 @@ import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
|
||||
import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||
import { adfToMarkdown } from '../emit/adf-to-markdown.ts'
|
||||
import { adfToPlainMarkdown } from '../plain/adf-to-plain-markdown.ts'
|
||||
import { adfToPortableMarkdown } from '../portable/adf-to-portable-markdown.ts'
|
||||
import { largestNesting } from '../../nesting.ts'
|
||||
import { markdownToAdf, plainMarkdownToAdf } from './markdown-to-adf.ts'
|
||||
import { portableMarkdownToAdf } from './markdown-to-adf.ts'
|
||||
|
||||
const code: AdfMark = { type: 'code' }
|
||||
const em: AdfMark = { type: 'em' }
|
||||
@@ -15,7 +14,7 @@ const strong: AdfMark = { type: 'strong' }
|
||||
const taskTypes = ['blockTaskItem', 'taskItem', 'taskList']
|
||||
|
||||
function read(markdown: string): readonly AdfNode[] | string {
|
||||
const parsed = plainMarkdownToAdf(markdown)
|
||||
const parsed = portableMarkdownToAdf(markdown)
|
||||
if (!parsed.ok) return parsed.error.code
|
||||
const blocks = parsed.value.content ?? []
|
||||
const pending = [...blocks]
|
||||
@@ -35,7 +34,7 @@ function document(...content: AdfNode[]): AdfDocument {
|
||||
}
|
||||
|
||||
function roundTripped(...content: AdfNode[]): readonly AdfNode[] | string {
|
||||
const markdown = adfToPlainMarkdown(document(...content))
|
||||
const markdown = adfToPortableMarkdown(document(...content))
|
||||
return markdown.ok ? read(markdown.value) : markdown.error.code
|
||||
}
|
||||
|
||||
@@ -119,22 +118,15 @@ test('reads a folded callout to an expand titled by the rest of its marker line,
|
||||
assert.deepEqual(read('> [!faq]- [a **b**](u "t")[c](u) and [d](v)\n'), [node('expand', { title: 'a bc (u) and d (v)' }, paragraph())])
|
||||
assert.deepEqual(read('> [!faq]- <http://y> or <a@b.c> or <http://a\\b>\n'), [node('expand', { title: 'http://y or a@b.c or http://a\\b' }, paragraph())])
|
||||
assert.deepEqual(read('> [!faq]- [a b](u)\n'), [node('expand', { title: 'a\nb (u)' }, paragraph())])
|
||||
assert.deepEqual(read('> [!faq]- [!adf:mention[@M]{id=5}](u) !adf:inlineCard{url="http://y"}\n'), [node('expand', { title: '@M (u) http://y' }, paragraph())])
|
||||
assert.deepEqual(read('> [!NOTE]- Set ==x== here\n'), [node('expand', { title: 'Set ==x== here' }, paragraph())])
|
||||
assert.deepEqual(read('> [!faq]- !adf:status[x]{color=red} ~~y~~\n'), [node('expand', { title: '!adf:status[x]{color=red} y' }, paragraph())])
|
||||
assert.deepEqual(read('> [!NOTE]- Set ==x== here\n'), [node('expand', { title: 'Set x here' }, paragraph())])
|
||||
assert.deepEqual(read('> [!NOTE]-\n>\n> Line.\n'), [bare('expand', said('Line.'))])
|
||||
})
|
||||
|
||||
test("reads a folded callout's title leaves as a walk rather than as one call's arguments", () => {
|
||||
const children = Array.from({ length: 130000 }, () => '{\\"text\\":\\"a\\",\\"type\\":\\"text\\"}').join(',')
|
||||
const parsed = plainMarkdownToAdf(`> [!NOTE]- !adf:carry{json="{\\"content\\":[${children}],\\"type\\":\\"unknownInline\\"}"}\n`)
|
||||
assert.equal(parsed.ok && parsed.value.content?.[0]?.attrs?.['title'], 'a'.repeat(130000))
|
||||
})
|
||||
|
||||
test('reads a folded callout inside an expand to a nested expand', () => {
|
||||
const markdown = '> [!NOTE]- Outer\n>\n> > [!NOTE]- Inner\n> >\n> > Deep.\n>\n> > [!TIP]\n> >\n> > > [!NOTE]-\n'
|
||||
assert.deepEqual(read(markdown), [node('expand', { title: 'Outer' }, node('nestedExpand', { title: 'Inner' }, said('Deep.')), panel('tip', bare('nestedExpand', paragraph())))])
|
||||
assert.deepEqual(read('- > [!NOTE]-\n'), [bare('bulletList', bare('listItem', bare('expand', paragraph())))])
|
||||
assert.deepEqual(read('!adf:expand\n> [!NOTE]- Inner\n!adf:/expand\n'), [bare('expand', node('nestedExpand', { title: 'Inner' }, paragraph()))])
|
||||
})
|
||||
|
||||
test('reads a bullet list whose every item leads with a task marker to a task list', () => {
|
||||
@@ -142,7 +134,7 @@ test('reads a bullet list whose every item leads with a task marker to a task li
|
||||
bare('taskList', task('DONE', text('Write the spec')), task('TODO', text('Ship '), text('it', strong)), task('DONE', text('Tell'))),
|
||||
])
|
||||
assert.deepEqual(read('- [x]\n- [ ]\\\n after\n'), [bare('taskList', task('DONE'), task('TODO', text('after')))])
|
||||
const minted = plainMarkdownToAdf('- [x] Parent\n - [ ] Child\n')
|
||||
const minted = portableMarkdownToAdf('- [x] Parent\n - [ ] Child\n')
|
||||
assert.deepEqual(minted.ok ? minted.value.content : minted.error.code, [
|
||||
node(
|
||||
'taskList',
|
||||
@@ -234,27 +226,12 @@ test('reads text the writer kept from reading as a marker back as text', () => {
|
||||
assert.deepEqual(roundTripped(bare('taskList', task('DONE', text('[x] ==a==')))), [bare('taskList', task('DONE', text('[x] ==a==')))])
|
||||
})
|
||||
|
||||
test('keeps what markdownToAdf reads that no row reads, and refuses only what it refuses', () => {
|
||||
assert.deepEqual(read('!adf:panel warning\n- [x] a\n!adf:/panel\n'), [panel('warning', bare('taskList', task('DONE', text('a'))))])
|
||||
assert.deepEqual(read('!adf:taskList\n!adf:taskItem TODO\nb\n!adf:/taskItem\n!adf:/taskList\n'), [bare('taskList', task('TODO', text('b')))])
|
||||
const listed = plainMarkdownToAdf('!adf:taskList {localId=01a0eeb2-be48-7ea7-8587-db5e013c374a}\n- [ ] b\n!adf:/taskList\n')
|
||||
assert.deepEqual(listed.ok ? listed.value.content?.[0]?.attrs : listed.error.code, { localId: '01a0eeb2-be48-7ea7-8587-db5e013c374a' })
|
||||
const future = bare('futureBlock', text('==x=='))
|
||||
const carried = adfToMarkdown(document(future))
|
||||
assert.deepEqual(carried.ok ? read(carried.value) : carried.error.code, [future])
|
||||
const uncarriable = bare('taskList', node('taskItem', { extra: { a: 1 }, state: 'TODO' }, text('a')))
|
||||
const carriedPanel = adfToMarkdown(document(node('panel', { extra: true, panelType: 'info' }, uncarriable)))
|
||||
const restored = carriedPanel.ok ? plainMarkdownToAdf(carriedPanel.value) : carriedPanel
|
||||
assert.deepEqual(restored.ok ? restored.value.content : restored.error.code, [node('panel', { extra: true, panelType: 'info' }, uncarriable)])
|
||||
const red: AdfMark = { attrs: { color: '#ff0000' }, type: 'backgroundColor' }
|
||||
const held = paragraph(text('a ==b== c', red), text(' ==d '), { attrs: { note: 'x' }, text: 'e==f', type: 'text' }, text(' g=='))
|
||||
const spelled = adfToMarkdown(document(held))
|
||||
assert.deepEqual(spelled.ok ? read(spelled.value) : spelled.error.code, [paragraph(text('a ==b== c', red), text(' '), text('d ', highlight), { attrs: { note: 'x' }, text: 'e==f', type: 'text' }, text(' g', highlight))])
|
||||
const lossless = markdownToAdf('> [!NOTE]\n\n- [x] ==a==\n')
|
||||
assert.deepEqual(lossless.ok ? lossless.value.content : lossless.error.code, [bare('blockquote', said('[!NOTE]')), bare('bulletList', bare('listItem', said('[x] ==a==')))])
|
||||
assert.equal(read('!adf:panel\n'), 'malformed-directive')
|
||||
test('reads what CommonMark reads where no row reads, a directive and a carry fence among it, and refuses only what it refuses', () => {
|
||||
assert.deepEqual(read('!adf:panel\n'), [said('!adf:panel')])
|
||||
assert.deepEqual(read('a !adf:carry{json="{}"} \\!adf:x\n'), [said('a !adf:carry{json="{}"} !adf:x')])
|
||||
assert.deepEqual(read('```adf:blockCard\n{}\n```\n'), [node('codeBlock', { language: 'adf:blockCard' }, text('{}'))])
|
||||
const refusal = (markdown: string): unknown => {
|
||||
const parsed = plainMarkdownToAdf(markdown)
|
||||
const parsed = portableMarkdownToAdf(markdown)
|
||||
return parsed.ok ? parsed.value : [parsed.error.code, parsed.error.message]
|
||||
}
|
||||
for (const markdown of ['> [!tip] \n', '> [!NOTE]- \n', '- [x] \n']) {
|
||||
@@ -267,8 +244,3 @@ test('keeps what markdownToAdf reads that no row reads, and refuses only what it
|
||||
for (let level = 0; level < largestNesting; level += 1) deep = `> ${deep}`
|
||||
assert.equal(typeof read(deep), 'object')
|
||||
})
|
||||
|
||||
test('joins a carried text node to no neighbour, inside a highlight too', () => {
|
||||
const carried = '!adf:carry{json="{\\"marks\\":[],\\"text\\":\\"b\\",\\"type\\":\\"text\\"}"}'
|
||||
assert.deepEqual(read(`==a${carried}==\n`), [paragraph(text('a', highlight), { marks: [], text: 'b', type: 'text' })])
|
||||
})
|
||||
Reference in New Issue
Block a user