3
0
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:
2026-10-09 02:30:41 +02:00
committed by Lillem4n
parent 80e1124f39
commit 23ec45febd
30 changed files with 1374 additions and 1387 deletions
+6 -5
View File
@@ -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 } },
])
})
+11 -7
View File
@@ -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)
+11 -11
View File
@@ -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
+26 -32
View File
@@ -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))
@@ -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&#10;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] ![a](u)\n', '> [!NOTE]- ![a](u)\n', '- [x] ![a](u)\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' })])
})