4.2: a list whose item holds a whitespace-only line takes the directive form

This commit is contained in:
2026-09-14 21:10:10 +02:00
parent c219aeb07b
commit fb1cef719a
6 changed files with 94 additions and 10 deletions
@@ -0,0 +1,57 @@
{
"content": [
{
"content": [
{
"content": [
{
"content": [
{
"text": " \na",
"type": "text"
}
],
"type": "codeBlock"
}
],
"type": "listItem"
}
],
"type": "bulletList"
},
{
"attrs": {
"order": 1
},
"content": [
{
"content": [
{
"content": [
{
"content": [
{
"content": [
{
"text": "\t",
"type": "text"
}
],
"type": "codeBlock"
}
],
"type": "listItem"
}
],
"type": "bulletList"
}
],
"type": "listItem"
}
],
"type": "orderedList"
}
],
"type": "doc",
"version": 1
}
@@ -0,0 +1,20 @@
::::bulletList
:::listItem
```
a
```
:::
::::
::::::orderedList {order=1}
:::::listItem
::::bulletList
:::listItem
```
```
:::
::::
:::::
::::::
+2 -1
View File
@@ -28,7 +28,8 @@ normalizes to it through the round-trip.
adjacent lists of a kind back as one. The leaf `::listBreak` parts them, taking the separation adjacent lists of a kind back as one. The leaf `::listBreak` parts them, taking the separation
any directive block takes where it sits. It builds no node, and it reads only between two any directive block takes where it sits. It builds no node, and it reads only between two
adjacent lists of one type: elsewhere, or carrying an argument, `{attrs}` or a body, it is a adjacent lists of one type: elsewhere, or carrying an argument, `{attrs}` or a body, it is a
named error. named error. A list whose item holds a line of spaces or tabs alone, which a list item reads
back empty, takes the directive form.
- 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
+4
View File
@@ -167,6 +167,10 @@ export function isAutolink(text: string): boolean {
return autolink.test(text) return autolink.test(text)
} }
export function isBlankLine(line: string): boolean {
return /^[ \t]*$/.test(line)
}
export function isThematicBreak(line: string): boolean { export function isThematicBreak(line: string): boolean {
return thematicBreak.test(line) return thematicBreak.test(line)
} }
+4 -2
View File
@@ -6,7 +6,7 @@ import { carriedBlock } from '../opaque-carry.ts'
import { emitInlineLine } from './inline-line.ts' import { emitInlineLine } from './inline-line.ts'
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts' import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { fencedCodeBlock } from '../backtick-runs.ts' import { fencedCodeBlock } from '../backtick-runs.ts'
import { holdsNullCharacter, isThematicBreak, markerInterruptsParagraph } from '../commonmark-grammar.ts' import { holdsNullCharacter, isBlankLine, isThematicBreak, markerInterruptsParagraph } from '../commonmark-grammar.ts'
import { languageSlot } from '../code-language.ts' import { languageSlot } from '../code-language.ts'
import { largestNesting } from '../../nesting.ts' import { largestNesting } from '../../nesting.ts'
import { listBreakSpelling } from '../list-break.ts' import { listBreakSpelling } from '../list-break.ts'
@@ -225,8 +225,10 @@ function emitListItem(item: AdfNode, marker: string, path: ConvertErrorPath, dep
const inner = emitBlocks(nodeContent(item), 'list-item', path, depth + 1) const inner = emitBlocks(nodeContent(item), 'list-item', path, depth + 1)
if (!inner.ok) return inner if (!inner.ok) return inner
if (inner.value.text === '') return success({ fenceColons: 0, text: marker.trimEnd() }) if (inner.value.text === '') return success({ fenceColons: 0, text: marker.trimEnd() })
const body = inner.value.text.split('\n')
if (body.some((line) => line !== '' && isBlankLine(line))) return undefined
const indent = ' '.repeat(marker.length) const indent = ' '.repeat(marker.length)
const lines = inner.value.text.split('\n').map((line, index) => (index === 0 ? `${marker}${line}` : line === '' ? '' : `${indent}${line}`)) const lines = body.map((line, index) => (index === 0 ? `${marker}${line}` : line === '' ? '' : `${indent}${line}`))
if (isThematicBreak(lines[0] ?? '')) return undefined if (isThematicBreak(lines[0] ?? '')) return undefined
return success({ fenceColons: inner.value.fenceColons, text: lines.join('\n') }) return success({ fenceColons: inner.value.fenceColons, text: lines.join('\n') })
} }
+7 -7
View File
@@ -7,6 +7,7 @@ import {
claimsPipeLine, claimsPipeLine,
closingCodeFence, closingCodeFence,
decodeTextEscapes, decodeTextEscapes,
isBlankLine,
isThematicBreak, isThematicBreak,
listMarker, listMarker,
markerInterruptsParagraph, markerInterruptsParagraph,
@@ -61,7 +62,6 @@ type Line = { column: number; text: string }
type Walk = ParsedBlocks & { leaf: OpenLeaf | undefined; position: SourcePosition; stack: OpenContainer[] } type Walk = ParsedBlocks & { leaf: OpenLeaf | undefined; position: SourcePosition; stack: OpenContainer[] }
const blankLine = /^[ \t]*$/
const indentedCodeColumns = 4 const indentedCodeColumns = 4
const largestOpenerIndentation = 3 const largestOpenerIndentation = 3
const leafColons = 2 const leafColons = 2
@@ -117,7 +117,7 @@ function continuesContainer(walk: Walk, container: OpenContainer, line: Line): L
// A directive container has no continuation marker: only its own fence closes it. // A directive container has no continuation marker: only its own fence closes it.
if (container.kind === 'directive') return line if (container.kind === 'directive') return line
// A list item begins with at most one blank line: an empty one gives the second up. // A list item begins with at most one blank line: an empty one gives the second up.
if (blankLine.test(line.text)) { if (isBlankLine(line.text)) {
return container.blocks.length === 0 && walk.leaf === undefined ? undefined : { column: line.column, text: '' } return container.blocks.length === 0 && walk.leaf === undefined ? undefined : { column: line.column, text: '' }
} }
return leadingColumns(line) < container.indentation ? undefined : removeColumns(line, container.indentation) return leadingColumns(line) < container.indentation ? undefined : removeColumns(line, container.indentation)
@@ -155,7 +155,7 @@ function itemStart(line: Line, opener: Line, paragraphOpen: boolean, enclosing:
const marker = listMarker(opener.text) const marker = listMarker(opener.text)
if (marker === undefined) return undefined if (marker === undefined) return undefined
const after: Line = { column: opener.column + marker.width, text: opener.text.slice(marker.width) } const after: Line = { column: opener.column + marker.width, text: opener.text.slice(marker.width) }
const blank = blankLine.test(after.text) const blank = isBlankLine(after.text)
if (paragraphOpen && !markerInterruptsParagraph(marker.start, blank)) return undefined if (paragraphOpen && !markerInterruptsParagraph(marker.start, blank)) return undefined
const spaces = leadingColumns(after) const spaces = leadingColumns(after)
const padding = blank || spaces > indentedCodeColumns ? 1 : spaces const padding = blank || spaces > indentedCodeColumns ? 1 : spaces
@@ -268,7 +268,7 @@ function pushFault(walk: Walk, fault: ConvertFault): void {
// A claimed line ends the lazy continuation CommonMark would fold it into (spec/flavour.md). // A claimed line ends the lazy continuation CommonMark would fold it into (spec/flavour.md).
function continuesLazily(walk: Walk, line: Line): boolean { function continuesLazily(walk: Walk, line: Line): boolean {
if (walk.leaf?.kind !== 'paragraph' || blankLine.test(line.text)) return false if (walk.leaf?.kind !== 'paragraph' || isBlankLine(line.text)) return false
if (leadingColumns(line) >= indentedCodeColumns) return true if (leadingColumns(line) >= indentedCodeColumns) return true
const opener = removeColumns(line, largestOpenerIndentation).text const opener = removeColumns(line, largestOpenerIndentation).text
if (claimsDirectiveLine(opener) || claimsPipeLine(opener) || isThematicBreak(opener)) return false if (claimsDirectiveLine(opener) || claimsPipeLine(opener) || isThematicBreak(opener)) return false
@@ -283,7 +283,7 @@ function readBlockLine(walk: Walk, line: Line): void {
return return
} }
if (leaf?.kind === 'html') { if (leaf?.kind === 'html') {
if (leaf.closer === undefined ? blankLine.test(line.text) : leaf.closer.test(line.text)) closeLeaf(walk) if (leaf.closer === undefined ? isBlankLine(line.text) : leaf.closer.test(line.text)) closeLeaf(walk)
return return
} }
if (leaf?.kind === 'pipe-table') { if (leaf?.kind === 'pipe-table') {
@@ -298,7 +298,7 @@ function readBlockLine(walk: Walk, line: Line): void {
if (readIndentedCodeLine(leaf, line)) return if (readIndentedCodeLine(leaf, line)) return
closeLeaf(walk) closeLeaf(walk)
} }
if (blankLine.test(line.text)) { if (isBlankLine(line.text)) {
closeLeaf(walk) closeLeaf(walk)
return return
} }
@@ -310,7 +310,7 @@ function readBlockLine(walk: Walk, line: Line): void {
} }
function readIndentedCodeLine(leaf: Extract<OpenLeaf, { kind: 'indented-code' }>, line: Line): boolean { function readIndentedCodeLine(leaf: Extract<OpenLeaf, { kind: 'indented-code' }>, line: Line): boolean {
if (blankLine.test(line.text)) { if (isBlankLine(line.text)) {
leaf.held.push(removeColumns(line, indentedCodeColumns).text) leaf.held.push(removeColumns(line, indentedCodeColumns).text)
return true return true
} }