Read the block nodes back, and stop a marker change splitting a list #40

Merged
lilleman merged 6 commits from block-nodes-read-back into main 2026-09-01 20:23:29 +02:00
8 changed files with 19 additions and 16 deletions
Showing only changes of commit e68dbc41a3 - Show all commits
+5 -3
View File
@@ -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
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
away, and so does the marker change CommonMark starts a second list on: ADF records no
tightness, and one `- ` spelling leaves two adjacent lists of a type no way back.
away, and no list opens beside one of its own kind — the marker change CommonMark starts a
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 `>`.
- ATX headings (`#``######`); setext input normalizes to ATX.
- 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
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
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
the `#` count, so a heading carrying none, or one that is no whole number from 1 to 6, has no
CommonMark spelling.
+1 -1
View File
@@ -5,7 +5,7 @@ import { holdsEntityReference } from './entity-references.ts'
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 {
if (language === undefined) return { kind: 'none' }
if (typeof language !== 'string' || language === '' || language === carryName) return { kind: 'attribute' }
+1 -1
View File
@@ -60,7 +60,7 @@ const firstCharacterOpeners = [atxHeadingOpener, /^>/, bulletListOpener, codeFen
const emailNameSource = "[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 orderedListOpener = /^(\d{1,9})([.)])(?:[ \t]|$)/
const orderedListOpener = /^(\d{1,9})(?:[.)])(?:[ \t]|$)/
const setextUnderline = /^(=+|-+)[ \t]*$/
const thematicBreak = /^(?:(?:\*[ \t]*){3,}|(?:-[ \t]*){3,}|(?:_[ \t]*){3,})$/
const unicodeWhitespace = /[\t\n\f\r \p{Zs}]/u
+1 -1
View File
@@ -6,8 +6,8 @@ import { carriesOnly, isAdfDocument } from '../../adf/document.ts'
import { emitInlineLine } from './inline-line.ts'
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { fencedCodeBlock } from '../backtick-runs.ts'
import { languageSlot } from '../code-language.ts'
import { holdsNullCharacter, isThematicBreak, markerInterruptsParagraph } from '../commonmark-grammar.ts'
import { languageSlot } from '../code-language.ts'
import { largestNesting } from '../../nesting.ts'
import { spellDirectiveHeader } from './block-directive-spelling.ts'
import { tryImage } from './image.ts'
-1
View File
@@ -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') })
}
// spec/flavour.md, Tables: the delimiter row underlines the header and leaves the body its cell count.
function pipeTableBlock(rows: readonly [string[], ...string[][]]): Block {
const [header, delimiter, ...body] = rows
if (delimiter !== undefined && delimiter.some(isPipeAlignment)) {
+5 -1
View File
@@ -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')), [
{ 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.
assert.deepEqual(content(markdownToAdf(':::codeBlock {wrap=true}\n```\\#c\nx\n```\n:::\n')), [
{ 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\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(':::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![a](/u)\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(
@@ -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\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\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.')])
})
+1 -2
View File
@@ -79,10 +79,9 @@ function directiveBody(read: BlockDirectiveNode, blocks: Block[] | undefined, de
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> {
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 fromFence = only.language !== ''
const slot = languageSlot(fromFence ? only.language : attribute)
+5 -6
View File
@@ -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.
export function pipeCells(line: string): string[] | undefined {
if (!claimsPipeLine(line)) return undefined
const row = line.replace(/[ \t]+$/, '')
const cells: string[] = []
let start = 1
let index = 1
while (index < row.length) {
if (backslashEscape(row, index) !== undefined) {
while (index < line.length) {
if (backslashEscape(line, index) !== undefined) {
index += 2
continue
}
if (row.charAt(index) === '|') {
cells.push(trimSpace(row.slice(start, index)))
if (line.charAt(index) === '|') {
cells.push(trimSpace(line.slice(start, index)))
start = 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()
return cells
}