Read the block nodes back, and stop a marker change splitting a list #40
+5
-3
@@ -22,8 +22,9 @@ normalizes to it through the round-trip.
|
|||||||
between a nested list and a CommonMark block above it — one wherever the nested list's own
|
between a nested list and a CommonMark block above it — one wherever the nested list's own
|
||||||
marker cannot interrupt a paragraph (an ordered list whose first number is not 1, or a list
|
marker cannot interrupt a paragraph (an ordered list whose first number is not 1, or a list
|
||||||
whose first item is empty), whatever block sits above it. Blank lines between items normalize
|
whose first item is empty), whatever block sits above it. Blank lines between items normalize
|
||||||
away, and so does the marker change CommonMark starts a second list on: ADF records no
|
away, and no list opens beside one of its own kind — the marker change CommonMark starts a
|
||||||
tightness, and one `- ` spelling leaves two adjacent lists of a type no way back.
|
second list on merges instead: ADF records no tightness, and one `- ` spelling leaves two
|
||||||
|
adjacent lists of a kind no way back.
|
||||||
- Blockquotes prefix lines with `> `; a blank line inside a blockquote is a bare `>`.
|
- Blockquotes prefix lines with `> `; a blank line inside a blockquote is a bare `>`.
|
||||||
- ATX headings (`#` … `######`); setext input normalizes to ATX.
|
- ATX headings (`#` … `######`); setext input normalizes to ATX.
|
||||||
- Code fences ``` with the node's language as info string, the fence lengthened past any backtick
|
- Code fences ``` with the node's language as info string, the fence lengthened past any backtick
|
||||||
@@ -192,7 +193,8 @@ form.
|
|||||||
the reserved `adf`, or holding a backtick, a backslash, a control character, edge whitespace or
|
the reserved `adf`, or holding a backtick, a backslash, a control character, edge whitespace or
|
||||||
an entity reference — rides the `language` attribute instead and the fence carries no info
|
an entity reference — rides the `language` attribute instead and the fence carries no info
|
||||||
string; writing it in the slot that rule leaves empty, or in both, is a named error. The body is
|
string; writing it in the slot that rule leaves empty, or in both, is a named error. The body is
|
||||||
an ordinary fence, so its info string decodes escapes and entity references as any other does.
|
one ordinary code block, and a fence's info string decodes escapes and entity references as any
|
||||||
|
other does.
|
||||||
- `heading` — container, inline body. Attributes: `level` (number), `localId` (string). `level` is
|
- `heading` — container, inline body. Attributes: `level` (number), `localId` (string). `level` is
|
||||||
the `#` count, so a heading carrying none, or one that is no whole number from 1 to 6, has no
|
the `#` count, so a heading carrying none, or one that is no whole number from 1 to 6, has no
|
||||||
CommonMark spelling.
|
CommonMark spelling.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { holdsEntityReference } from './entity-references.ts'
|
|||||||
|
|
||||||
export type LanguageSlot = { info: string; kind: 'fence' } | { kind: 'attribute' } | { kind: 'none' }
|
export type LanguageSlot = { info: string; kind: 'fence' } | { kind: 'attribute' } | { kind: 'none' }
|
||||||
|
|
||||||
// spec/flavour.md, The CommonMark blocks: the one slot a codeBlock's language rides, both directions.
|
// spec/flavour.md, The CommonMark blocks: the one slot a codeBlock's language rides.
|
||||||
export function languageSlot(language: JsonValue | undefined): LanguageSlot {
|
export function languageSlot(language: JsonValue | undefined): LanguageSlot {
|
||||||
if (language === undefined) return { kind: 'none' }
|
if (language === undefined) return { kind: 'none' }
|
||||||
if (typeof language !== 'string' || language === '' || language === carryName) return { kind: 'attribute' }
|
if (typeof language !== 'string' || language === '' || language === carryName) return { kind: 'attribute' }
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ const firstCharacterOpeners = [atxHeadingOpener, /^>/, bulletListOpener, codeFen
|
|||||||
const emailNameSource = "[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+"
|
const emailNameSource = "[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+"
|
||||||
const emailLabelSource = '[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?'
|
const emailLabelSource = '[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?'
|
||||||
const emailAutolink = new RegExp(`<${emailNameSource}@${emailLabelSource}(?:\\.${emailLabelSource})*>`, 'y')
|
const emailAutolink = new RegExp(`<${emailNameSource}@${emailLabelSource}(?:\\.${emailLabelSource})*>`, 'y')
|
||||||
const orderedListOpener = /^(\d{1,9})([.)])(?:[ \t]|$)/
|
const orderedListOpener = /^(\d{1,9})(?:[.)])(?:[ \t]|$)/
|
||||||
const setextUnderline = /^(=+|-+)[ \t]*$/
|
const setextUnderline = /^(=+|-+)[ \t]*$/
|
||||||
const thematicBreak = /^(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/
|
const thematicBreak = /^(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/
|
||||||
const unicodeWhitespace = /[\t\n\f\r \p{Zs}]/u
|
const unicodeWhitespace = /[\t\n\f\r \p{Zs}]/u
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { carriesOnly, isAdfDocument } from '../../adf/document.ts'
|
|||||||
import { emitInlineLine } from './inline-line.ts'
|
import { emitInlineLine } from './inline-line.ts'
|
||||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
import { fencedCodeBlock } from '../backtick-runs.ts'
|
import { fencedCodeBlock } from '../backtick-runs.ts'
|
||||||
import { languageSlot } from '../code-language.ts'
|
|
||||||
import { holdsNullCharacter, isThematicBreak, markerInterruptsParagraph } from '../commonmark-grammar.ts'
|
import { holdsNullCharacter, isThematicBreak, markerInterruptsParagraph } from '../commonmark-grammar.ts'
|
||||||
|
import { languageSlot } from '../code-language.ts'
|
||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
import { spellDirectiveHeader } from './block-directive-spelling.ts'
|
import { spellDirectiveHeader } from './block-directive-spelling.ts'
|
||||||
import { tryImage } from './image.ts'
|
import { tryImage } from './image.ts'
|
||||||
|
|||||||
@@ -383,7 +383,6 @@ function closeLeaf(walk: Walk): void {
|
|||||||
else currentBlocks(walk).push({ kind: 'code', language: leaf.kind === 'fenced-code' ? decodeTextEscapes(leaf.info) : '', text: leaf.lines.join('\n') })
|
else currentBlocks(walk).push({ kind: 'code', language: leaf.kind === 'fenced-code' ? decodeTextEscapes(leaf.info) : '', text: leaf.lines.join('\n') })
|
||||||
}
|
}
|
||||||
|
|
||||||
// spec/flavour.md, Tables: the delimiter row underlines the header and leaves the body its cell count.
|
|
||||||
function pipeTableBlock(rows: readonly [string[], ...string[][]]): Block {
|
function pipeTableBlock(rows: readonly [string[], ...string[][]]): Block {
|
||||||
const [header, delimiter, ...body] = rows
|
const [header, delimiter, ...body] = rows
|
||||||
if (delimiter !== undefined && delimiter.some(isPipeAlignment)) {
|
if (delimiter !== undefined && delimiter.some(isPipeAlignment)) {
|
||||||
|
|||||||
@@ -135,6 +135,9 @@ test('reads the codeBlock directive body as the node content, the info string it
|
|||||||
assert.deepEqual(content(markdownToAdf(':::codeBlock {language=""}\n```\nx\n```\n:::\n')), [
|
assert.deepEqual(content(markdownToAdf(':::codeBlock {language=""}\n```\nx\n```\n:::\n')), [
|
||||||
{ attrs: { language: '' }, content: [text('x')], type: 'codeBlock' },
|
{ attrs: { language: '' }, content: [text('x')], type: 'codeBlock' },
|
||||||
])
|
])
|
||||||
|
assert.deepEqual(content(markdownToAdf(':::codeBlock {wrap=true}\n fn()\n:::\n')), [
|
||||||
|
{ attrs: { wrap: true }, content: [text('fn()')], type: 'codeBlock' },
|
||||||
|
])
|
||||||
// The body is a CommonMark fence, so its info string decodes escapes the way any other fence's does.
|
// The body is a CommonMark fence, so its info string decodes escapes the way any other fence's does.
|
||||||
assert.deepEqual(content(markdownToAdf(':::codeBlock {wrap=true}\n```\\#c\nx\n```\n:::\n')), [
|
assert.deepEqual(content(markdownToAdf(':::codeBlock {wrap=true}\n```\\#c\nx\n```\n:::\n')), [
|
||||||
{ attrs: { language: '#c', wrap: true }, content: [text('x')], type: 'codeBlock' },
|
{ attrs: { language: '#c', wrap: true }, content: [text('x')], type: 'codeBlock' },
|
||||||
@@ -305,7 +308,7 @@ test('names the argument and the body a node takes no reading for', () => {
|
|||||||
assert.equal(content(markdownToAdf(':::paragraph\n:::\n')), 'unsupported-node-shape: an empty paragraph takes the leaf form, ::')
|
assert.equal(content(markdownToAdf(':::paragraph\n:::\n')), 'unsupported-node-shape: an empty paragraph takes the leaf form, ::')
|
||||||
assert.equal(content(markdownToAdf(':::paragraph\nOne.\n\nTwo.\n:::\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body')
|
assert.equal(content(markdownToAdf(':::paragraph\nOne.\n\nTwo.\n:::\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body')
|
||||||
assert.equal(content(markdownToAdf(':::paragraph\n---\n:::\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body')
|
assert.equal(content(markdownToAdf(':::paragraph\n---\n:::\n')), 'unsupported-node-shape: paragraph takes one paragraph as its body')
|
||||||
assert.equal(content(markdownToAdf(':::codeBlock {wrap=true}\nx\n:::\n')), 'unsupported-node-shape: codeBlock takes one fenced code block as its body')
|
assert.equal(content(markdownToAdf(':::codeBlock {wrap=true}\nx\n:::\n')), 'unsupported-node-shape: codeBlock takes one code block as its body')
|
||||||
assert.equal(content(markdownToAdf(':::paragraph\n\n:::\n')), 'unmappable-image: no ADF node carries an image inside a paragraph')
|
assert.equal(content(markdownToAdf(':::paragraph\n\n:::\n')), 'unmappable-image: no ADF node carries an image inside a paragraph')
|
||||||
assert.equal(content(markdownToAdf('Part :date[now]{timestamp=1}.\n')), 'unsupported-node-shape: date takes no content')
|
assert.equal(content(markdownToAdf('Part :date[now]{timestamp=1}.\n')), 'unsupported-node-shape: date takes no content')
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -392,6 +395,7 @@ test('reads a bullet list, the marker width setting the continuation', () => {
|
|||||||
assert.deepEqual(content(markdownToAdf('- a\n\n+ b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))])
|
assert.deepEqual(content(markdownToAdf('- a\n\n+ b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))])
|
||||||
assert.deepEqual(content(markdownToAdf('- a\n-\n\n- c\n')), [bulletList(item(paragraph('a')), item(), item(paragraph('c')))])
|
assert.deepEqual(content(markdownToAdf('- a\n-\n\n- c\n')), [bulletList(item(paragraph('a')), item(), item(paragraph('c')))])
|
||||||
assert.deepEqual(content(markdownToAdf('- a\n1. b\n')), [bulletList(item(paragraph('a'))), orderedList(1, item(paragraph('b')))])
|
assert.deepEqual(content(markdownToAdf('- a\n1. b\n')), [bulletList(item(paragraph('a'))), orderedList(1, item(paragraph('b')))])
|
||||||
|
assert.deepEqual(content(markdownToAdf('- a\n\n[r]: /u\n\n- b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))])
|
||||||
assert.deepEqual(content(markdownToAdf('-\n\n Part.\n')), [bulletList(item()), paragraph('Part.')])
|
assert.deepEqual(content(markdownToAdf('-\n\n Part.\n')), [bulletList(item()), paragraph('Part.')])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -79,10 +79,9 @@ function directiveBody(read: BlockDirectiveNode, blocks: Block[] | undefined, de
|
|||||||
return inlineBodyNode(node, blocks, definitions, path)
|
return inlineBodyNode(node, blocks, definitions, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
// spec/flavour.md, The CommonMark blocks: the language rides the one slot fenceInfo picks for it.
|
|
||||||
function codeDirectiveNode(node: AdfNode, blocks: readonly Block[], path: ConvertErrorPath): Result<AdfNode> {
|
function codeDirectiveNode(node: AdfNode, blocks: readonly Block[], path: ConvertErrorPath): Result<AdfNode> {
|
||||||
const only = blocks.length === 1 ? blocks[0] : undefined
|
const only = blocks.length === 1 ? blocks[0] : undefined
|
||||||
if (only?.kind !== 'code') return failure('unsupported-node-shape', `${node.type} takes one fenced code block as its body`, path)
|
if (only?.kind !== 'code') return failure('unsupported-node-shape', `${node.type} takes one code block as its body`, path)
|
||||||
const attribute = node.attrs?.['language']
|
const attribute = node.attrs?.['language']
|
||||||
const fromFence = only.language !== ''
|
const fromFence = only.language !== ''
|
||||||
const slot = languageSlot(fromFence ? only.language : attribute)
|
const slot = languageSlot(fromFence ? only.language : attribute)
|
||||||
|
|||||||
@@ -19,22 +19,21 @@ export function malformedPipeTable(message: string): ConvertFault {
|
|||||||
// spec/flavour.md, Tables: the cells of a claimed row, the closing `|` the spelling writes optional here.
|
// spec/flavour.md, Tables: the cells of a claimed row, the closing `|` the spelling writes optional here.
|
||||||
export function pipeCells(line: string): string[] | undefined {
|
export function pipeCells(line: string): string[] | undefined {
|
||||||
if (!claimsPipeLine(line)) return undefined
|
if (!claimsPipeLine(line)) return undefined
|
||||||
const row = line.replace(/[ \t]+$/, '')
|
|
||||||
const cells: string[] = []
|
const cells: string[] = []
|
||||||
let start = 1
|
let start = 1
|
||||||
let index = 1
|
let index = 1
|
||||||
while (index < row.length) {
|
while (index < line.length) {
|
||||||
if (backslashEscape(row, index) !== undefined) {
|
if (backslashEscape(line, index) !== undefined) {
|
||||||
index += 2
|
index += 2
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (row.charAt(index) === '|') {
|
if (line.charAt(index) === '|') {
|
||||||
cells.push(trimSpace(row.slice(start, index)))
|
cells.push(trimSpace(line.slice(start, index)))
|
||||||
start = index + 1
|
start = index + 1
|
||||||
}
|
}
|
||||||
index += 1
|
index += 1
|
||||||
}
|
}
|
||||||
cells.push(trimSpace(row.slice(start)))
|
cells.push(trimSpace(line.slice(start)))
|
||||||
if (cells.length > 1 && cells.at(-1) === '') cells.pop()
|
if (cells.length > 1 && cells.at(-1) === '') cells.pop()
|
||||||
return cells
|
return cells
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user