5b3: merge the code list's duplicate causes, spell the list separator, claim the bare pipe table

This commit is contained in:
2026-09-03 19:05:23 +02:00
parent 3e4f596eb1
commit d2ed9c8219
21 changed files with 384 additions and 81 deletions
+27 -15
View File
@@ -81,13 +81,13 @@ test('spells a code block language no info string holds as an attribute', () =>
test('refuses a link destination CommonMark cannot spell', () => {
const link = (href: string): AdfDocument => document(paragraph({ marks: [{ attrs: { href }, type: 'link' }], text: 't', type: 'text' }))
assert.equal(code(adfToMarkdown(link('https://example.com/a b>c'))), 'unspellable-link-destination')
assert.equal(code(adfToMarkdown(link('<https://example.com/'))), 'unspellable-link-destination')
assert.equal(code(adfToMarkdown(link('https://example.com/a\\b'))), 'unspellable-link-destination')
assert.equal(code(adfToMarkdown(link('https://example.com/?a=1&amp;b=2'))), 'unspellable-link-destination')
assert.equal(code(adfToMarkdown(link('https://example.com/a\nb'))), 'unspellable-link-destination')
assert.equal(code(adfToMarkdown(link('https://example.com/a b>c'))), 'unspellable-link')
assert.equal(code(adfToMarkdown(link('<https://example.com/'))), 'unspellable-link')
assert.equal(code(adfToMarkdown(link('https://example.com/a\\b'))), 'unspellable-link')
assert.equal(code(adfToMarkdown(link('https://example.com/?a=1&amp;b=2'))), 'unspellable-link')
assert.equal(code(adfToMarkdown(link('https://example.com/a\nb'))), 'unspellable-link')
const entity = 'https://example.com/?a=1&amp;b=2'
assert.equal(code(adfToMarkdown(document(paragraph({ marks: [{ attrs: { href: entity }, type: 'link' }], text: entity, type: 'text' })))), 'unspellable-link-destination')
assert.equal(code(adfToMarkdown(document(paragraph({ marks: [{ attrs: { href: entity }, type: 'link' }], text: entity, type: 'text' })))), 'unspellable-link')
})
test('escapes the parenthesis a link destination leaves unbalanced, and no other', () => {
@@ -103,8 +103,8 @@ test('escapes the quote a link title holds, and refuses the rest', () => {
const titled = (title: string): AdfDocument =>
document(paragraph({ marks: [{ attrs: { href: 'https://example.com/', title }, type: 'link' }], text: 't', type: 'text' }))
assert.equal(markdown(adfToMarkdown(titled('He said "hi"'))), '[t](https://example.com/ "He said \\"hi\\"")\n')
assert.equal(code(adfToMarkdown(titled('a\nb'))), 'unspellable-link-title')
assert.equal(code(adfToMarkdown(titled('a\\b'))), 'unspellable-link-title')
assert.equal(code(adfToMarkdown(titled('a\nb'))), 'unspellable-link')
assert.equal(code(adfToMarkdown(titled('a\\b'))), 'unspellable-link')
})
test('carries a link mark the link spelling cannot write', () => {
@@ -134,17 +134,25 @@ test('carries a mark the canonical spellings cannot nest', () => {
)
})
test('refuses whitespace CommonMark cannot hold', () => {
assert.equal(code(adfToMarkdown(document(paragraph({ text: 'a\rb', type: 'text' })))), 'unspellable-whitespace')
})
test('refuses a line whose start block parsing would claim', () => {
assert.equal(code(adfToMarkdown(document(paragraph({ marks: [{ type: 'code' }], text: '```', type: 'text' })))), 'unspellable-line-start')
})
test('refuses two adjacent lists of the same kind, the marker spelling being what merges', () => {
test('escapes the delimiter row a hard break leaves opening a pipe table with no leading pipe', () => {
const broken = (second: string): string => markdown(adfToMarkdown(document(paragraph({ text: 'a | b', type: 'text' }, { type: 'hardBreak' }, { text: second, type: 'text' }))))
assert.equal(broken('--- | ---'), 'a | b\\\n\\--- | ---\n')
assert.equal(broken(':--- | ---:'), 'a | b\\\n\\:--- | ---:\n')
assert.equal(broken('c | d'), 'a | b\\\nc | d\n')
})
test('parts two adjacent lists of the same kind, the marker spelling being what merges', () => {
const list: AdfNode = { content: [{ content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }], type: 'bulletList' }
assert.equal(code(adfToMarkdown(document(list, list))), 'unspellable-adjacent-lists')
assert.equal(markdown(adfToMarkdown(document(list, list))), '- x\n\n::listBreak\n\n- x\n')
const ordered: AdfNode = { attrs: { order: 1 }, content: [{ content: [paragraph({ text: 'x', type: 'text' })], type: 'listItem' }], type: 'orderedList' }
assert.equal(markdown(adfToMarkdown(document(ordered, ordered))), '1. x\n\n::listBreak\n\n1. x\n')
assert.equal(markdown(adfToMarkdown(document({ attrs: { panelType: 'info' }, content: [list, list], type: 'panel' }))), ':::panel info\n- x\n::listBreak\n- x\n:::\n')
const nested: AdfNode = { content: [{ content: [list, list], type: 'listItem' }], type: 'bulletList' }
assert.equal(markdown(adfToMarkdown(document(nested))), '- - x\n\n ::listBreak\n\n - x\n')
const carried: AdfNode = { ...list, attrs: { unknown: 'x' } }
assert.ok(markdown(adfToMarkdown(document(carried, carried))).includes('```\n\n```adf\n'))
assert.ok(markdown(adfToMarkdown(document(carried, list))).endsWith('```\n\n- x\n'))
@@ -394,7 +402,11 @@ test('spells a list item whose marker completes a thematic break as a directive'
test('refuses the characters CommonMark rewrites', () => {
assert.equal(
markdown(adfToMarkdown(document({ content: [{ text: 'a\rb', type: 'text' }], type: 'codeBlock' }))),
'unspellable-whitespace: a codeBlock holds no carriage return CommonMark keeps: this text holds one',
'unspellable-character: a codeBlock holds no carriage return CommonMark keeps: this text holds one',
)
assert.equal(
markdown(adfToMarkdown(document(paragraph({ text: 'a\rb', type: 'text' })))),
'unspellable-character: a text node holds a carriage return CommonMark rewrites',
)
assert.equal(code(adfToMarkdown(document(paragraph({ text: 'a\u0000b', type: 'text' })))), 'unspellable-character')
assert.equal(code(adfToMarkdown(document({ content: [{ text: 'a\u0000b', type: 'text' }], type: 'codeBlock' }))), 'unspellable-character')
+11 -14
View File
@@ -9,6 +9,7 @@ import { fencedCodeBlock } from '../backtick-runs.ts'
import { holdsNullCharacter, isThematicBreak, markerInterruptsParagraph } from '../commonmark-grammar.ts'
import { languageSlot } from '../code-language.ts'
import { largestNesting } from '../../nesting.ts'
import { listBreakSpelling } from '../list-break.ts'
import { spellDirectiveHeader } from './block-directive-spelling.ts'
import { tryImage } from './image.ts'
import { tryPipeTable } from './pipe-table.ts'
@@ -17,7 +18,7 @@ type BlockContainer = 'directive' | 'document' | 'list-item'
type BlockSpelling = 'commonmark' | 'directive' | 'list'
type EmittedBody = { fenceColons: number; text: string }
type EmittedBlock = EmittedBody & { spelling: BlockSpelling }
type PlacedBlock = EmittedBlock & { node: AdfNode; path: ConvertErrorPath }
type PlacedBlock = EmittedBlock & { node: AdfNode }
const largestListMarker = 999999999
@@ -34,35 +35,31 @@ function emitBlocks(nodes: readonly AdfNode[], container: BlockContainer, path:
if (depth > largestNesting) return failure('unsupported-nesting-depth', `the document nests deeper than the ${largestNesting} levels the emitter carries`, path)
const blocks: PlacedBlock[] = []
for (const [index, node] of nodes.entries()) {
const nodePath = [...path, 'content', index]
const block = emitBlock(node, nodePath, depth)
const block = emitBlock(node, [...path, 'content', index], depth)
if (!block.ok) return block
blocks.push({ ...block.value, node, path: nodePath })
blocks.push({ ...block.value, node })
}
let fenceColons = 0
let text = ''
for (const [index, block] of blocks.entries()) {
const previous = blocks[index - 1]
if (previous !== undefined) {
const separation = separationBetween(previous, block, container)
if (!separation.ok) return separation
text += separation.value
}
if (previous !== undefined) text += separationBetween(previous, block, container)
fenceColons = Math.max(fenceColons, block.fenceColons)
text += block.text
}
return success({ fenceColons, text })
}
function separationBetween(previous: PlacedBlock, next: PlacedBlock, container: BlockContainer): Result<string> {
function separationBetween(previous: PlacedBlock, next: PlacedBlock, container: BlockContainer): string {
const plainPair = previous.spelling !== 'directive' && next.spelling !== 'directive'
if (plainPair && next.spelling === 'list') {
if (previous.spelling === 'list' && previous.node.type === next.node.type) {
return failure('unspellable-adjacent-lists', `two adjacent ${next.node.type} nodes read back as one list`, next.path)
const gap = container === 'directive' ? '\n' : '\n\n'
return `${gap}${listBreakSpelling}${gap}`
}
if (container === 'list-item') return success(interruptsParagraph(next.node) ? '\n' : '\n\n')
if (container === 'list-item') return interruptsParagraph(next.node) ? '\n' : '\n\n'
}
return success(container === 'directive' && !plainPair ? '\n' : '\n\n')
return container === 'directive' && !plainPair ? '\n' : '\n\n'
}
function interruptsParagraph(node: AdfNode): boolean {
@@ -179,7 +176,7 @@ function codeBlockText(node: AdfNode, path: ConvertErrorPath): Result<string> {
) {
return failure('unsupported-node-shape', `a codeBlock holds plain text nodes only: this ${child.type} node is not one`, childPath)
}
if (/\r/.test(child.text)) return failure('unspellable-whitespace', 'a codeBlock holds no carriage return CommonMark keeps: this text holds one', childPath)
if (/\r/.test(child.text)) return failure('unspellable-character', 'a codeBlock holds no carriage return CommonMark keeps: this text holds one', childPath)
if (holdsNullCharacter(child.text)) return failure('unspellable-character', 'a codeBlock holds a null character CommonMark replaces', childPath)
text += child.text
}
+1 -1
View File
@@ -220,7 +220,7 @@ function emitText(node: AdfNode, context: InlineContext, index: number, path: Co
if (Object.keys(node.attrs ?? {}).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 ((node.content ?? []).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-whitespace', '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)
const escaping: InlineEscaping = context.bracketed ? 'bracketed' : 'backslash'
const parts = node.text.split(/(\n+)/).filter((part) => part !== '')
+5 -1
View File
@@ -1,6 +1,7 @@
import { backtickRun, closingBacktickRun } from '../backtick-runs.ts'
import { delimiterFlags, isWordCharacter, matchEmphasis, runLength } from '../emphasis-matching.ts'
import { backslashEscape, escapesLineClaim, inlineHtmlConstruct, opensBracketedAutolink, opensEmailAutolink, type LinePosition } from '../commonmark-grammar.ts'
import { isBareDelimiterRow } from '../pipe-table-syntax.ts'
import { opensInlineDirective } from '../directive-syntax.ts'
import { readEntityReference } from '../entity-references.ts'
@@ -196,8 +197,11 @@ function opensConstruct(
return claimsCharacter(scan, linkClose, index, inBrackets, container, escaped)
}
// A hard break is the one spelling that puts a delimiter row under a row of its own, so only a later line claims.
function claimsLineStart(line: ScanLine, index: number, container: LineContainer): boolean {
return container === 'paragraph' && escapesLineClaim(line.text, index - line.start, line.position)
if (container !== 'paragraph') return false
if (index === line.start && line.position === 'later' && isBareDelimiterRow(line.text)) return true
return escapesLineClaim(line.text, index - line.start, line.position)
}
function scanLine(scan: string, start: number): ScanLine {
+7 -7
View File
@@ -91,26 +91,26 @@ export function skipLinkWhitespace(text: string, offset: number): number {
}
export function spellDestination(href: string, path: ConvertErrorPath): Result<string> {
if (holdsControlCharacter(href)) return failure('unspellable-link-destination', 'a link destination holds a control character', path)
if (href.includes('\\')) return failure('unspellable-link-destination', 'no canonical escape spells a backslash in a link destination', path)
if (holdsControlCharacter(href)) return failure('unspellable-link', 'a link destination holds a control character', path)
if (href.includes('\\')) return failure('unspellable-link', 'no canonical escape spells a backslash in a link destination', path)
if (holdsEntityReference(href)) {
return failure('unspellable-link-destination', 'a link destination holds an entity reference that decodes on the way back', path)
return failure('unspellable-link', 'a link destination holds an entity reference that decodes on the way back', path)
}
if (href.includes(' ')) {
if (/[<>]/.test(href)) {
return failure('unspellable-link-destination', 'no canonical escape spells an angle bracket beside a space in a link destination', path)
return failure('unspellable-link', 'no canonical escape spells an angle bracket beside a space in a link destination', path)
}
return success(`<${href}>`)
}
if (href.startsWith('<')) return failure('unspellable-link-destination', 'a bare link destination cannot begin with an angle bracket', path)
if (href.startsWith('<')) return failure('unspellable-link', 'a bare link destination cannot begin with an angle bracket', path)
return success(escapeUnbalanced(href))
}
export function spellTitle(title: string, path: ConvertErrorPath): Result<string> {
if (/[\n\r\\]/.test(title)) {
return failure('unspellable-link-title', 'no canonical escape spells a backslash or newline in a link title', path)
return failure('unspellable-link', 'no canonical escape spells a backslash or newline in a link title', path)
}
if (holdsEntityReference(title)) return failure('unspellable-link-title', 'a link title holds an entity reference that decodes on the way back', path)
if (holdsEntityReference(title)) return failure('unspellable-link', 'a link title holds an entity reference that decodes on the way back', path)
return success(` "${title.replaceAll('"', '\\"')}"`)
}
+5
View File
@@ -0,0 +1,5 @@
const name = 'listBreak'
export const listBreakName = name
export const listBreakSpelling = `::${name}`
+16 -3
View File
@@ -16,7 +16,7 @@ import {
setextHeadingLevel,
} from '../commonmark-grammar.ts'
import { directiveLineEscape, malformedDirective, readDirectiveLine } from '../directive-syntax.ts'
import { isPipeAlignment, isPipeDelimiter, malformedPipeTable, pipeCells } from '../pipe-table-syntax.ts'
import { barePipeCells, isDelimiterRow, isPipeAlignment, isPipeDelimiter, malformedPipeTable, pipeCells } from '../pipe-table-syntax.ts'
import { readLinkDefinitions } from './link-reference-definitions.ts'
export type Block = { position: SourcePosition } & (
@@ -358,7 +358,7 @@ function readLineBlock(walk: Walk, opener: string): boolean {
if (level !== undefined) {
const paragraph = takeParagraph(walk)
if (paragraph !== undefined) {
currentBlocks(walk).push({ kind: 'heading', level, position: paragraph.position, text: paragraph.text })
currentBlocks(walk).push(bareTableFault(paragraph) ?? { kind: 'heading', level, position: paragraph.position, text: paragraph.text })
return true
}
}
@@ -390,7 +390,7 @@ function closeLeaf(walk: Walk): void {
if (leaf === undefined) return
if (leaf.kind === 'paragraph') {
const paragraph = takeParagraph(walk)
if (paragraph !== undefined) currentBlocks(walk).push(paragraph)
if (paragraph !== undefined) currentBlocks(walk).push(bareTableFault(paragraph) ?? paragraph)
return
}
walk.leaf = undefined
@@ -413,6 +413,19 @@ function pipeTableBlock(rows: readonly [string[], ...string[][]], position: Sour
return { kind: 'table', position, rows: [header, ...body] }
}
// 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 {
let header: string[] | undefined
for (const line of paragraph.text.split('\n')) {
const cells = barePipeCells(line)
if (header !== undefined && cells !== undefined && cells.length === header.length && isDelimiterRow(cells)) {
return faultedBlock('a pipe table opens every row with `|`: this one does not; \\| keeps a pipe literal text', paragraph.position)
}
header = cells
}
return undefined
}
function cellCount(count: number): string {
return `${count} cell${count === 1 ? '' : 's'}`
}
+2
View File
@@ -10,6 +10,7 @@ import { carryName } from '../opaque-carry.ts'
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { inlineDirective } from '../../adf/inline-directives.ts'
import { inlineMarkSpellingFault } from './directive-marks.ts'
import { listBreakName, listBreakSpelling } from '../list-break.ts'
import { marksAttribute, readMarkValues } from '../block-directive-marks.ts'
import { readVocabulary } from './directive-attributes.ts'
import { slotLineEndingFault } from '../directive-syntax.ts'
@@ -76,6 +77,7 @@ function inlineSpellingFault(name: string): ConvertFault | undefined {
}
function blockSpellingFault(name: string): ConvertFault | undefined {
if (name === listBreakName) return { code: 'unsupported-node-shape', message: `${name} takes the block form, ${listBreakSpelling}, never the inline form` }
const directive = blockDirective(name)
if (directive === undefined) return undefined
const form = directive.contentModel === 'none' ? `::${name}` : `:::${name}`
+37 -1
View File
@@ -190,6 +190,42 @@ test('names the pipe table a claimed line does not spell', () => {
assert.deepEqual(path(markdownToAdf('Part.\n\n| a |\n')), ['content', 1])
})
test('names the pipe table whose rows open with no pipe', () => {
const bare = 'malformed-pipe-table: a pipe table opens every row with `|`: this one does not; \\| keeps a pipe literal text'
assert.equal(content(markdownToAdf('a | b\n--- | ---\n')), bare)
assert.equal(content(markdownToAdf('Intro.\na | b\n--- | ---\n')), bare)
assert.equal(content(markdownToAdf('a | b\n--- | ---\n===\n')), bare)
assert.equal(content(markdownToAdf('a | b\n:--- | ---:\n')), bare)
assert.deepEqual(content(markdownToAdf('a | b\nc | d\n')), [paragraph('a | b c | d')])
assert.deepEqual(content(markdownToAdf('a | b\n--- | --- | ---\n')), [paragraph('a | b --- | --- | ---')])
assert.deepEqual(content(markdownToAdf('a \\| b\n--- | ---\n')), [paragraph('a | b --- | ---')])
assert.deepEqual(content(markdownToAdf('a\n---\n')), [{ attrs: { level: 2 }, content: [text('a')], type: 'heading' }])
assert.deepEqual(path(markdownToAdf('Part.\n\na | b\n--- | ---\n')), ['content', 1])
})
test('reads the separator that parts two adjacent lists of one kind', () => {
const parted = [bulletList(item(paragraph('a'))), bulletList(item(paragraph('b')))]
assert.deepEqual(content(markdownToAdf('- a\n\n::listBreak\n\n- b\n')), parted)
assert.deepEqual(content(markdownToAdf('- a\n::listBreak\n- b\n')), parted)
assert.deepEqual(content(markdownToAdf('1. a\n\n::listBreak\n\n1. b\n')), [orderedList(1, item(paragraph('a'))), orderedList(1, item(paragraph('b')))])
assert.deepEqual(content(markdownToAdf('> - a\n> ::listBreak\n> - b\n')), [quote(...parted)])
assert.deepEqual(path(markdownToAdf('- a\n\n::listBreak\n\n- b\n\n| x |\n')), ['content', 2])
})
test('refuses the list separator that parts anything else', () => {
const parts = 'unsupported-node-shape: listBreak parts two adjacent lists of one type: this one parts something else'
assert.equal(content(markdownToAdf('::listBreak\n')), parts)
assert.equal(content(markdownToAdf('- a\n\n::listBreak\n')), parts)
assert.equal(content(markdownToAdf('- a\n\n::listBreak\n\n1. b\n')), parts)
assert.equal(content(markdownToAdf('Part.\n\n::listBreak\n\n- b\n')), parts)
const bare = 'unsupported-node-shape: listBreak spells the bare leaf form, ::listBreak: this one spells more'
assert.equal(content(markdownToAdf('- a\n\n::listBreak x\n\n- b\n')), bare)
assert.equal(content(markdownToAdf('- a\n\n::listBreak {id=x}\n\n- b\n')), bare)
assert.equal(content(markdownToAdf(':::listBreak\n- a\n:::\n')), bare)
assert.equal(content(markdownToAdf(':listBreak{}\n')), 'unsupported-node-shape: listBreak takes the block form, ::listBreak, never the inline form')
assert.deepEqual(path(markdownToAdf('Part.\n\n::listBreak\n')), ['content', 1])
})
test('refuses the image a pipe cell holds no ADF node for', () => {
assert.equal(content(markdownToAdf('| a |\n| --- |\n| ![x](/u) |\n')), 'unmappable-image: no ADF node carries an image inside a paragraph')
assert.deepEqual(path(markdownToAdf('| a |\n| --- |\n| ![x](/u) |\n')), ['content', 0, 'content', 1, 'content', 0, 'content', 0])
@@ -243,7 +279,7 @@ test('names the directive form a node CommonMark spells refuses', () => {
// The spelling the emitter refuses gives the emitter's own error, never a second name for it.
test('gives back the refusal the CommonMark spelling itself raises', () => {
const destination = ':::blockquote\n[t](https://example.com/a\\b)\n:::\n'
assert.equal(content(markdownToAdf(destination)), 'unspellable-link-destination: no canonical escape spells a backslash in a link destination')
assert.equal(content(markdownToAdf(destination)), 'unspellable-link: no canonical escape spells a backslash in a link destination')
})
test('names the directive name no node reads back to', () => {
+23 -1
View File
@@ -1,15 +1,18 @@
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
import type { Block, DirectiveBlock } from './blocks.ts'
import type { BlockDirectiveNode } from './directive-nodes.ts'
import type { ConvertFault } from '../../result.ts'
import type { LinkDefinitions } from './inline-content.ts'
import { carryName, readCarriedBlock } from '../opaque-carry.ts'
import { commonMarkSpelling } from '../emit/adf-to-markdown.ts'
import { failure, faulted, positioned, success, type ConvertErrorPath, type ParseError, type Result, type SourcePosition } from '../../result.ts'
import { languageSlot } from '../code-language.ts'
import { largestNesting } from '../../nesting.ts'
import { listBreakName, listBreakSpelling } from '../list-break.ts'
import { parseBlocks } from './blocks.ts'
import { parseInlineContent } from './inline-content.ts'
import { readBlockDirectiveNode } from './directive-nodes.ts'
import { unsupportedNodeShape } from '../directive-syntax.ts'
const documentStart: SourcePosition = { line: 1, offset: 0 }
@@ -24,13 +27,32 @@ function blockNodes(blocks: readonly Block[], definitions: LinkDefinitions, path
if (depth > largestNesting) return failure('unsupported-nesting-depth', `the input nests deeper than the ${largestNesting} levels the parser carries`, path)
const content: AdfNode[] = []
for (const [index, block] of blocks.entries()) {
const node = positioned(blockNode(block, definitions, [...path, 'content', index], depth), block.position)
const nodePath = [...path, 'content', content.length]
if (block.kind === 'directive' && block.name === listBreakName) {
const fault = listBreakFault(block, blocks[index - 1], blocks[index + 1])
if (fault !== undefined) return positioned(faulted(fault, nodePath), block.position)
continue
}
const node = positioned(blockNode(block, definitions, nodePath, depth), block.position)
if (!node.ok) return node
content.push(node.value)
}
return success(content)
}
// spec/flavour.md, Directives: the separator builds no node, so only the pair it parts spells it.
function listBreakFault(block: DirectiveBlock, previous: Block | undefined, next: Block | undefined): ConvertFault | undefined {
if (block.blocks !== undefined || block.argument !== undefined || block.attributes.size > 0) {
return unsupportedNodeShape(`${listBreakName} spells the bare leaf form, ${listBreakSpelling}: this one spells more`)
}
if (previous?.kind !== 'bulletList' && previous?.kind !== 'orderedList') return partsFault()
return previous.kind === next?.kind ? undefined : partsFault()
}
function partsFault(): ConvertFault {
return unsupportedNodeShape(`${listBreakName} parts two adjacent lists of one type: this one parts something else`)
}
function blockNode(block: Block, definitions: LinkDefinitions, path: ConvertErrorPath, depth: number): Result<AdfNode> {
switch (block.kind) {
case 'blockquote':
+36 -18
View File
@@ -4,6 +4,21 @@ import { backslashEscape, claimsPipeLine, trimSpace } from './commonmark-grammar
const alignmentCell = /^:-+:?$|^-+:$/
const delimiterCell = /^-+$/
// spec/flavour.md, Tables: the cells of a row the leading `|` no claim read, GFM's form without it.
export function barePipeCells(line: string): string[] | undefined {
const cells = splitPipeCells(line, 0)
return cells.length > 1 ? cells : undefined
}
export function isBareDelimiterRow(line: string): boolean {
const cells = barePipeCells(line)
return cells !== undefined && isDelimiterRow(cells)
}
export function isDelimiterRow(cells: readonly string[]): boolean {
return cells.every((cell) => isPipeDelimiter(cell) || isPipeAlignment(cell))
}
export function isPipeAlignment(cell: string): boolean {
return alignmentCell.test(cell)
}
@@ -18,24 +33,7 @@ export function malformedPipeTable(message: string): ConvertFault {
// spec/flavour.md, Tables: the cells of a claimed row, the closing `|` the spelling writes optional here.
export function pipeCells(line: string): string[] | undefined {
if (!claimsPipeLine(line)) return undefined
const cells: string[] = []
let start = 1
let index = 1
while (index < line.length) {
if (backslashEscape(line, index) !== undefined) {
index += 2
continue
}
if (line.charAt(index) === '|') {
cells.push(trimSpace(line.slice(start, index)))
start = index + 1
}
index += 1
}
cells.push(trimSpace(line.slice(start)))
if (cells.length > 1 && cells.at(-1) === '') cells.pop()
return cells
return claimsPipeLine(line) ? splitPipeCells(line, 1) : undefined
}
export function spellPipeDelimiter(columns: number): string {
@@ -45,3 +43,23 @@ export function spellPipeDelimiter(columns: number): string {
export function spellPipeRow(cells: readonly string[]): string {
return `| ${cells.join(' | ')} |`
}
function splitPipeCells(line: string, start: number): string[] {
const cells: string[] = []
let cellStart = start
let index = start
while (index < line.length) {
if (backslashEscape(line, index) !== undefined) {
index += 2
continue
}
if (line.charAt(index) === '|') {
cells.push(trimSpace(line.slice(cellStart, index)))
cellStart = index + 1
}
index += 1
}
cells.push(trimSpace(line.slice(cellStart)))
if (cells.length > 1 && cells.at(-1) === '') cells.pop()
return cells
}
+1 -3
View File
@@ -5,11 +5,9 @@ export type ConvertErrorCode =
| 'unknown-directive-name'
| 'unmappable-html'
| 'unmappable-image'
| 'unspellable-adjacent-lists'
| 'unspellable-character'
| 'unspellable-line-start'
| 'unspellable-link-destination'
| 'unspellable-link-title'
| 'unspellable-link'
| 'unspellable-whitespace'
| 'unsupported-document-version'
| 'unsupported-nesting-depth'