Answer the architecture pass: the list a blank line split, and the slot the seam names
CI / gate (push) Successful in 8s
CI / gate (push) Successful in 8s
This commit is contained in:
@@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "Bolt M8",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "listItem"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "Nut M8",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "listItem"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "listItem"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "Washer M8",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "listItem"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "bulletList"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attrs": {
|
||||||
|
"order": 1
|
||||||
|
},
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "Loosen the clamp",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "listItem"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"text": "Lift the cover",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "paragraph"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "listItem"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "orderedList"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"type": "doc",
|
||||||
|
"version": 1
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
- Bolt M8
|
||||||
|
* Nut M8
|
||||||
|
-
|
||||||
|
|
||||||
|
+ Washer M8
|
||||||
|
|
||||||
|
1. Loosen the clamp
|
||||||
|
1) Lift the cover
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
* Bolt M8
|
* Bolt M8
|
||||||
|
|
||||||
+ Nut M8
|
* Nut M8
|
||||||
|
|
||||||
Then:
|
Then:
|
||||||
|
|
||||||
|
|||||||
+6
-1
@@ -187,12 +187,17 @@ for (const directory of emittingDirectories) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (const name of pairedNames(normalizationRoot, '.md', '.json')) {
|
for (const name of pairedNames(normalizationRoot, '.md', '.json')) {
|
||||||
test(`normalization/${name} parses to the document beside it`, () => {
|
test(`normalization/${name} parses to the document beside it, which emits and reads back to itself`, () => {
|
||||||
const expected: unknown = JSON.parse(readFileSync(join(normalizationRoot, `${name}.json`), 'utf8'))
|
const expected: unknown = JSON.parse(readFileSync(join(normalizationRoot, `${name}.json`), 'utf8'))
|
||||||
assert.ok(isAdfDocument(expected), `${name}.json is not an ADF document`)
|
assert.ok(isAdfDocument(expected), `${name}.json is not an ADF document`)
|
||||||
const result = markdownToAdf(readFileSync(join(normalizationRoot, `${name}.md`), 'utf8'))
|
const result = markdownToAdf(readFileSync(join(normalizationRoot, `${name}.md`), 'utf8'))
|
||||||
assert.ok(result.ok, result.ok ? '' : `${result.error.code}: ${result.error.message}`)
|
assert.ok(result.ok, result.ok ? '' : `${result.error.code}: ${result.error.message}`)
|
||||||
assert.deepEqual(result.value, expected)
|
assert.deepEqual(result.value, expected)
|
||||||
|
const emitted = adfToMarkdown(result.value)
|
||||||
|
assert.ok(emitted.ok, emitted.ok ? '' : `${emitted.error.code}: ${emitted.error.message}`)
|
||||||
|
const again = markdownToAdf(emitted.value)
|
||||||
|
assert.ok(again.ok, again.ok ? '' : `${again.error.code}: ${again.error.message}`)
|
||||||
|
assert.deepEqual(again.value, expected)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import { carryName } from './opaque-carry.ts'
|
|||||||
import { holdsControlCharacter } from './commonmark-grammar.ts'
|
import { holdsControlCharacter } from './commonmark-grammar.ts'
|
||||||
import { holdsEntityReference } from './entity-references.ts'
|
import { holdsEntityReference } from './entity-references.ts'
|
||||||
|
|
||||||
// spec/flavour.md, The CommonMark blocks: the info string the language rides, `undefined` where the attribute carries it.
|
export type LanguageSlot = { info: string; kind: 'fence' } | { kind: 'attribute' } | { kind: 'none' }
|
||||||
export function fenceInfo(language: JsonValue | undefined): string | undefined {
|
|
||||||
if (language === undefined) return ''
|
// spec/flavour.md, The CommonMark blocks: the one slot a codeBlock's language rides, both directions.
|
||||||
if (typeof language !== 'string' || language === '' || language === carryName) return undefined
|
export function languageSlot(language: JsonValue | undefined): LanguageSlot {
|
||||||
if (/[`\\]/.test(language) || holdsControlCharacter(language) || language !== language.trim() || holdsEntityReference(language)) return undefined
|
if (language === undefined) return { kind: 'none' }
|
||||||
return language
|
if (typeof language !== 'string' || language === '' || language === carryName) return { kind: 'attribute' }
|
||||||
|
if (/[`\\]/.test(language) || holdsControlCharacter(language) || language !== language.trim() || holdsEntityReference(language)) return { kind: 'attribute' }
|
||||||
|
return { info: language, kind: 'fence' }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -171,13 +171,13 @@ export function isUnicodeWhitespace(character: string): boolean {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// `start` is the list's first number, `undefined` for a bullet.
|
// `start` is the list's first number, `undefined` for a bullet.
|
||||||
export function listMarker(line: string): { delimiter: string; start: number | undefined; width: number } | undefined {
|
export function listMarker(line: string): { start: number | undefined; width: number } | undefined {
|
||||||
const ordered = orderedListOpener.exec(line)
|
const ordered = orderedListOpener.exec(line)
|
||||||
if (ordered !== null) {
|
if (ordered !== null) {
|
||||||
const digits = ordered[1] ?? ''
|
const digits = ordered[1] ?? ''
|
||||||
return { delimiter: ordered[2] ?? '', start: Number(digits), width: digits.length + 1 }
|
return { start: Number(digits), width: digits.length + 1 }
|
||||||
}
|
}
|
||||||
return bulletListOpener.test(line) ? { delimiter: line.charAt(0), start: undefined, width: 1 } : undefined
|
return bulletListOpener.test(line) ? { start: undefined, width: 1 } : undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export function markerInterruptsParagraph(start: number | undefined, empty: boolean): boolean {
|
export function markerInterruptsParagraph(start: number | undefined, empty: boolean): boolean {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ 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 { fenceInfo } from '../code-language.ts'
|
import { languageSlot } from '../code-language.ts'
|
||||||
import { holdsNullCharacter, isThematicBreak, markerInterruptsParagraph } from '../commonmark-grammar.ts'
|
import { holdsNullCharacter, isThematicBreak, markerInterruptsParagraph } from '../commonmark-grammar.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'
|
||||||
@@ -147,20 +147,21 @@ function emitBlockquote(node: AdfNode, path: ConvertErrorPath, depth: number): R
|
|||||||
|
|
||||||
function emitCodeBlock(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
function emitCodeBlock(node: AdfNode, path: ConvertErrorPath): Result<EmittedBlock> | undefined {
|
||||||
if (!carriesOnly(node, ['language'])) return undefined
|
if (!carriesOnly(node, ['language'])) return undefined
|
||||||
const info = fenceInfo(node.attrs?.['language'])
|
const slot = languageSlot(node.attrs?.['language'])
|
||||||
if (info === undefined) return undefined
|
if (slot.kind === 'attribute') return undefined
|
||||||
const text = codeBlockText(node, path)
|
const text = codeBlockText(node, path)
|
||||||
if (!text.ok) return text
|
if (!text.ok) return text
|
||||||
return success(commonMarkText(fencedCodeBlock(info, text.value)))
|
return success(commonMarkText(fencedCodeBlock(slot.kind === 'fence' ? slot.info : '', text.value)))
|
||||||
}
|
}
|
||||||
|
|
||||||
function emitCodeDirective(node: AdfNode, directive: BlockDirective, path: ConvertErrorPath): Result<EmittedBlock> {
|
function emitCodeDirective(node: AdfNode, directive: BlockDirective, path: ConvertErrorPath): Result<EmittedBlock> {
|
||||||
const info = fenceInfo(node.attrs?.['language'])
|
const slot = languageSlot(node.attrs?.['language'])
|
||||||
const header = spellDirectiveHeader(node, directive, info === undefined ? [] : ['language'])
|
const header = spellDirectiveHeader(node, directive, slot.kind === 'attribute' ? [] : ['language'])
|
||||||
if (header === undefined) return commonMarkLine(carriedBlock(node, path))
|
if (header === undefined) return commonMarkLine(carriedBlock(node, path))
|
||||||
const text = codeBlockText(node, path)
|
const text = codeBlockText(node, path)
|
||||||
if (!text.ok) return text
|
if (!text.ok) return text
|
||||||
return success({ fenceColons: 3, spelling: 'directive', text: `:::${header}\n${fencedCodeBlock(info ?? '', text.value)}\n:::` })
|
const info = slot.kind === 'fence' ? slot.info : ''
|
||||||
|
return success({ fenceColons: 3, spelling: 'directive', text: `:::${header}\n${fencedCodeBlock(info, text.value)}\n:::` })
|
||||||
}
|
}
|
||||||
|
|
||||||
function codeBlockText(node: AdfNode, path: ConvertErrorPath): Result<string> {
|
function codeBlockText(node: AdfNode, path: ConvertErrorPath): Result<string> {
|
||||||
|
|||||||
@@ -176,9 +176,19 @@ function openContainer(walk: Walk, start: ContainerStart): void {
|
|||||||
walk.stack.push(blockquote)
|
walk.stack.push(blockquote)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (start.fresh) currentBlocks(walk).push(start.list)
|
const list = openedList(walk, start)
|
||||||
start.list.items.push(blocks)
|
list.items.push(blocks)
|
||||||
walk.stack.push({ blocks, indentation: start.indentation, kind: 'item', list: start.list })
|
walk.stack.push({ blocks, indentation: start.indentation, kind: 'item', list })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Two lists of a kind never sit adjacent: one `- ` spelling reads them back as one (spec/flavour.md).
|
||||||
|
function openedList(walk: Walk, start: Extract<ContainerStart, { kind: 'item' }>): ListBlock {
|
||||||
|
if (!start.fresh) return start.list
|
||||||
|
const blocks = currentBlocks(walk)
|
||||||
|
const previous = blocks.at(-1)
|
||||||
|
if ((previous?.kind === 'bulletList' || previous?.kind === 'orderedList') && previous.kind === start.list.kind) return previous
|
||||||
|
blocks.push(start.list)
|
||||||
|
return start.list
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeContainers(walk: Walk, depth: number): void {
|
function closeContainers(walk: Walk, depth: number): void {
|
||||||
|
|||||||
@@ -390,6 +390,8 @@ test('reads a bullet list, the marker width setting the continuation', () => {
|
|||||||
assert.deepEqual(content(markdownToAdf('- Code.\n')), [bulletList(item({ content: [text('Code.')], type: 'codeBlock' }))])
|
assert.deepEqual(content(markdownToAdf('- Code.\n')), [bulletList(item({ content: [text('Code.')], type: 'codeBlock' }))])
|
||||||
assert.deepEqual(content(markdownToAdf('- a\n* b\n')), [bulletList(item(paragraph('a')), item(paragraph('b')))])
|
assert.deepEqual(content(markdownToAdf('- a\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+ 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('-\n\n Part.\n')), [bulletList(item()), paragraph('Part.')])
|
assert.deepEqual(content(markdownToAdf('-\n\n Part.\n')), [bulletList(item()), paragraph('Part.')])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { LinkDefinitions } from './inline-content.ts'
|
|||||||
import { carryName } from '../opaque-carry.ts'
|
import { carryName } from '../opaque-carry.ts'
|
||||||
import { commonMarkSpelling } from '../emit/adf-to-markdown.ts'
|
import { commonMarkSpelling } from '../emit/adf-to-markdown.ts'
|
||||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
import { fenceInfo } from '../code-language.ts'
|
import { languageSlot } from '../code-language.ts'
|
||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
import { parseBlocks } from './blocks.ts'
|
import { parseBlocks } from './blocks.ts'
|
||||||
import { parseInlineContent } from './inline-content.ts'
|
import { parseInlineContent } from './inline-content.ts'
|
||||||
@@ -85,8 +85,8 @@ function codeDirectiveNode(node: AdfNode, blocks: readonly Block[], path: Conver
|
|||||||
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 fenced 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 info = fenceInfo(fromFence ? only.language : attribute)
|
const slot = languageSlot(fromFence ? only.language : attribute)
|
||||||
if ((info !== undefined && info !== '') !== fromFence || (fromFence && attribute !== undefined)) {
|
if ((slot.kind === 'fence') !== fromFence || (fromFence && attribute !== undefined)) {
|
||||||
return failure('unsupported-node-shape', `${node.type} spells its language in the fence info string, or in the attribute where no info string carries it back`, path)
|
return failure('unsupported-node-shape', `${node.type} spells its language in the fence info string, or in the attribute where no info string carries it back`, path)
|
||||||
}
|
}
|
||||||
const spelled = fromFence ? { ...node, attrs: { ...node.attrs, language: only.language } } : node
|
const spelled = fromFence ? { ...node, attrs: { ...node.attrs, language: only.language } } : node
|
||||||
|
|||||||
+11
-7
@@ -295,13 +295,17 @@ Under **3 — `markdownToAdf` (`0.1.0`)**:
|
|||||||
`corpus.test.ts` too, and `unspelled-block-separation`, which loses its only cause here.
|
`corpus.test.ts` too, and `unspelled-block-separation`, which loses its only cause here.
|
||||||
The emitter's other refusals survive on causes no fixture in that directory covers, so
|
The emitter's other refusals survive on causes no fixture in that directory covers, so
|
||||||
3k's one-list pass is where they get fixtures or the directory goes.
|
3k's one-list pass is where they get fixtures or the directory goes.
|
||||||
**Settled** (the maintainer, 2026-09-01): losing that cause closed one of the two shapes
|
**Settled** (the maintainer, 2026-09-01): losing that cause closed one of the shapes input
|
||||||
input accepted and emit refused, not the last. The other surfaced here — CommonMark opens a
|
accepted and emit refused, not the last. Two adjacent lists of a kind are what
|
||||||
second list on a marker change, so `- a` over `* b` built two adjacent `bulletList` nodes
|
`adfToMarkdown` refuses and one `- ` spelling cannot hold apart, and the walk reached them
|
||||||
`adfToMarkdown` refuses — and the parser continues the list instead, the way it already
|
two ways — a marker change, which CommonMark opens a second list on, and an empty last item,
|
||||||
drops the blank lines between items. With both closed the parse-then-emit fixpoint the
|
whose blank line pops the container the list's identity hung from. The parser opens no list
|
||||||
README promises holds for every construct the parser reads, rather than only for what the
|
beside one of its own kind instead, the way it already drops the blank lines between items;
|
||||||
emitter wrote.
|
3k owes the CommonMark suite an exception where the reference HTML holds two `<ul>`. The
|
||||||
|
`normalization/` arm emits each document and reads it back from here, so the population that
|
||||||
|
class lives in is checked rather than read. The README's canonical-fixpoint sentence still
|
||||||
|
claims more than the parser keeps — 3e's three shapes — which stays milestone 5's to
|
||||||
|
narrow.
|
||||||
- [x] **3g — The node tables read backwards.** `commonmark-subset/` reads back, the first
|
- [x] **3g — The node tables read backwards.** `commonmark-subset/` reads back, the first
|
||||||
directory to. A parsed directive becomes its node: the name to the type and an unknown one
|
directory to. A parsed directive becomes its node: the name to the type and an unknown one
|
||||||
to a named error, the arg to the attribute it names, each value to the type its section
|
to a named error, the arg to the attribute it names, each value to the type its section
|
||||||
|
|||||||
@@ -61,9 +61,10 @@ numbering is the order the work was planned in, not the order it ships.
|
|||||||
parsing the slot inside it is a cycle; the four directive marks get `parse/directive-marks.ts`
|
parsing the slot inside it is a cycle; the four directive marks get `parse/directive-marks.ts`
|
||||||
that `inline-content.ts` tries ahead of the node reader, as `mark-spellings.ts` sits apart
|
that `inline-content.ts` tries ahead of the node reader, as `mark-spellings.ts` sits apart
|
||||||
from `emit/inline-directive-spelling.ts`; and the five markdown-spelled mark names in inline
|
from `emit/inline-directive-spelling.ts`; and the five markdown-spelled mark names in inline
|
||||||
directive position want a claim code — `:em[x]` is an error forever, so
|
directive position take `unsupported-node-shape` rather than a code of their own — §8
|
||||||
`unknown-directive-name`'s "a later MINOR may give the name meaning" is the wrong signal,
|
already answers a well-formed directive the node tables refuse, and the message names the
|
||||||
as it was for `adf`. `corpus/errors/directive-content-slot` goes when the slot opens.
|
spelling to use (`*x*`), while `unknown-directive-name`'s "a later MINOR may give the name
|
||||||
|
meaning" stays the wrong signal, as it was for `adf`. `corpus/errors/directive-content-slot` goes when the slot opens.
|
||||||
The marks a spelling wraps answer the same question 3g settled for a block's form: only the
|
The marks a spelling wraps answer the same question 3g settled for a block's form: only the
|
||||||
nesting the emitter writes parses back.
|
nesting the emitter writes parses back.
|
||||||
- [ ] **3j — The carry and the combinations.** `opaque-carry/` and `combinations/` read back:
|
- [ ] **3j — The carry and the combinations.** `opaque-carry/` and `combinations/` read back:
|
||||||
@@ -123,7 +124,10 @@ numbering is the order the work was planned in, not the order it ships.
|
|||||||
landed the last decision `corpus/unspellable/` held and the directory went with it, so what
|
landed the last decision `corpus/unspellable/` held and the directory went with it, so what
|
||||||
the code list holds from here is permanent. The parser's own code
|
the code list holds from here is permanent. The parser's own code
|
||||||
additions are read here as one list before that freeze — nine sessions mint them
|
additions are read here as one list before that freeze — nine sessions mint them
|
||||||
independently, and one cause wearing two codes is breaking to undo after `0.1.0`. `0.1.0`
|
independently, and one cause wearing two codes is breaking to undo after `0.1.0`. That read
|
||||||
|
gets a test rather than an eye — every `ConvertErrorCode` member named at a production call
|
||||||
|
site, the way `spec.test.ts` guards the node tables — since `unspelled-block-separation`
|
||||||
|
outlived its cause until 3h went looking. `0.1.0`
|
||||||
is the markdown round-trip: both markdown directions, the types, `isAdfDocument`. The build
|
is the markdown round-trip: both markdown directions, the types, `isAdfDocument`. The build
|
||||||
lands here: `tsconfig.build.json` gains emit of JS and `.d.ts` to `dist/` (its own
|
lands here: `tsconfig.build.json` gains emit of JS and `.d.ts` to `dist/` (its own
|
||||||
`allowImportingTsExtensions` forces `noEmit`, so `rewriteRelativeImportExtensions` lands
|
`allowImportingTsExtensions` forces `noEmit`, so `rewriteRelativeImportExtensions` lands
|
||||||
|
|||||||
Reference in New Issue
Block a user