5b3: merge the code list's duplicate causes, spell the list separator, claim the bare pipe table
This commit is contained in:
@@ -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&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&b=2'))), 'unspellable-link')
|
||||
assert.equal(code(adfToMarkdown(link('https://example.com/a\nb'))), 'unspellable-link')
|
||||
const entity = 'https://example.com/?a=1&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')
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 !== '')
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user