Read the inline nodes and the marks back, and keep the whitespace CommonMark does not strip #41
@@ -98,7 +98,7 @@
|
|||||||
{
|
{
|
||||||
"content": [
|
"content": [
|
||||||
{
|
{
|
||||||
"text": "Sold as a pair",
|
"text": "\fSold as a pair\u000b",
|
||||||
"type": "text"
|
"type": "text"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
| Part | Note |
|
| Part | Note |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| Bolt M8 | Grade `8.8` |
|
| Bolt M8 | Grade `8.8` |
|
||||||
| Nut \| washer | Sold as a pair |
|
| Nut \| washer | Sold as a pair |
|
||||||
| Washer M8 | 100 pcs:hardBreak{}zinc-plated |
|
| Washer M8 | 100 pcs:hardBreak{}zinc-plated |
|
||||||
| Spare | |
|
| Spare | |
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --test --experimental-test-coverage --test-coverage-exclude=\"src/**/*.test.ts\" --test-coverage-branches=97 --test-coverage-functions=100 --test-coverage-lines=100 \"src/**/*.test.ts\"",
|
"test": "node --test --experimental-test-coverage --test-coverage-exclude=\"src/**/*.test.ts\" --test-coverage-branches=97.8 --test-coverage-functions=100 --test-coverage-lines=100 \"src/**/*.test.ts\"",
|
||||||
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.build.json"
|
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.build.json"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ export function spellJsonAttribute(value: JsonValue): string {
|
|||||||
return quote(serializeCanonicalJson(value, 'compact'))
|
return quote(serializeCanonicalJson(value, 'compact'))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function spellLeafDirective(name: string, attributes: string): string {
|
||||||
|
return `:${name}${attributes === '' ? '{}' : attributes}`
|
||||||
|
}
|
||||||
|
|
||||||
export function spellStringAttribute(text: string): string {
|
export function spellStringAttribute(text: string): string {
|
||||||
return isBareToken(text) ? text : quote(text)
|
return isBareToken(text) ? text : quote(text)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
|
|||||||
import { sameMark } from '../../adf/editor-normal.ts'
|
import { sameMark } from '../../adf/editor-normal.ts'
|
||||||
import { spellDestination, spellTitle } from '../link-syntax.ts'
|
import { spellDestination, spellTitle } from '../link-syntax.ts'
|
||||||
import { spellInlineNodeAttributes } from './inline-directive-spelling.ts'
|
import { spellInlineNodeAttributes } from './inline-directive-spelling.ts'
|
||||||
|
import { spellLeafDirective } from '../directive-syntax.ts'
|
||||||
import { spellTextDirective } from '../text-directive.ts'
|
import { spellTextDirective } from '../text-directive.ts'
|
||||||
|
|
||||||
type EmittedLine = { line: string; segments: InlineSegment[] }
|
type EmittedLine = { line: string; segments: InlineSegment[] }
|
||||||
@@ -121,10 +122,6 @@ function carriedText(text: string): InlineSegment {
|
|||||||
return syntax(spellTextDirective(text))
|
return syntax(spellTextDirective(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
function spellLeafDirective(name: string, attributes: string): string {
|
|
||||||
return `:${name}${attributes === '' ? '{}' : attributes}`
|
|
||||||
}
|
|
||||||
|
|
||||||
function syntax(text: string): InlineSegment {
|
function syntax(text: string): InlineSegment {
|
||||||
return { escaping: 'none', text }
|
return { escaping: 'none', text }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { AdfAttributes } from '../../adf/document.ts'
|
||||||
|
import type { AttributeVocabulary } from '../../adf/attribute-vocabulary.ts'
|
||||||
|
import type { DirectiveAttributes } from '../directive-syntax.ts'
|
||||||
|
import { attributeValue, spellAttributeValue } from '../directive-syntax.ts'
|
||||||
|
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
|
|
||||||
|
export type Elsewhere = { key: string; slot: 'argument' | 'content' }
|
||||||
|
|
||||||
|
export function readVocabulary(
|
||||||
|
type: string,
|
||||||
|
attributes: DirectiveAttributes,
|
||||||
|
vocabulary: AttributeVocabulary,
|
||||||
|
elsewhere: Elsewhere | undefined,
|
||||||
|
path: ConvertErrorPath,
|
||||||
|
): Result<AdfAttributes> {
|
||||||
|
const attrs: AdfAttributes = {}
|
||||||
|
for (const [key, spelled] of attributes) {
|
||||||
|
if (key === elsewhere?.key) {
|
||||||
|
const place = elsewhere.slot === 'argument' ? 'as the directive argument' : 'in the content slot'
|
||||||
|
return failure('unsupported-node-shape', `${type} spells its ${key} attribute ${place}`, path)
|
||||||
|
}
|
||||||
|
const kind = Object.hasOwn(vocabulary, key) ? vocabulary[key] : undefined
|
||||||
|
if (kind === undefined) return failure('unsupported-node-shape', `${type} holds no ${key} attribute`, path)
|
||||||
|
const read = attributeValue(spelled.decoded, kind)
|
||||||
|
if (read === undefined) return failure('unsupported-node-shape', `the ${key} attribute of ${type} is no ${kind}`, path)
|
||||||
|
const spelling = spellAttributeValue(read)
|
||||||
|
if (spelling !== spelled.spelling) return failure('unsupported-node-shape', `${type} spells its ${key} attribute as ${key}=${spelling}`, path)
|
||||||
|
attrs[key] = read.value
|
||||||
|
}
|
||||||
|
return success(attrs)
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ import type { DirectiveAttributes } from '../directive-syntax.ts'
|
|||||||
import type { MarkSpelling } from '../mark-spellings.ts'
|
import type { MarkSpelling } from '../mark-spellings.ts'
|
||||||
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
import { markSpelling } from '../mark-spellings.ts'
|
import { markSpelling } from '../mark-spellings.ts'
|
||||||
import { readVocabulary } from './directive-nodes.ts'
|
import { readVocabulary } from './directive-attributes.ts'
|
||||||
|
|
||||||
export function readDirectiveMark(name: string, attributes: DirectiveAttributes, path: ConvertErrorPath): Result<AdfMark> | undefined {
|
export function readDirectiveMark(name: string, attributes: DirectiveAttributes, path: ConvertErrorPath): Result<AdfMark> | undefined {
|
||||||
const spelling = markSpelling(name)
|
const spelling = markSpelling(name)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { AdfAttributes, AdfMark, AdfNode } from '../../adf/document.ts'
|
import type { AdfAttributes, AdfMark, AdfNode } from '../../adf/document.ts'
|
||||||
import type { AttributeVocabulary } from '../../adf/attribute-vocabulary.ts'
|
|
||||||
import type { BlockDirective } from '../../adf/block-directives.ts'
|
import type { BlockDirective } from '../../adf/block-directives.ts'
|
||||||
import type { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts'
|
import type { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts'
|
||||||
|
import type { Elsewhere } from './directive-attributes.ts'
|
||||||
import { attributeValue, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
|
import { attributeValue, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
|
||||||
import { blockArgument } from '../block-directive-arguments.ts'
|
import { blockArgument } from '../block-directive-arguments.ts'
|
||||||
import { blockDirective } from '../../adf/block-directives.ts'
|
import { blockDirective } from '../../adf/block-directives.ts'
|
||||||
@@ -9,11 +9,10 @@ import { carryName } from '../opaque-carry.ts'
|
|||||||
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
|
||||||
import { inlineDirective } from '../../adf/inline-directives.ts'
|
import { inlineDirective } from '../../adf/inline-directives.ts'
|
||||||
import { marksAttribute, readMarkValues } from '../block-directive-marks.ts'
|
import { marksAttribute, readMarkValues } from '../block-directive-marks.ts'
|
||||||
|
import { readVocabulary } from './directive-attributes.ts'
|
||||||
|
|
||||||
export type BlockDirectiveNode = { contentModel: BlockDirective['contentModel']; node: AdfNode }
|
export type BlockDirectiveNode = { contentModel: BlockDirective['contentModel']; node: AdfNode }
|
||||||
|
|
||||||
type Elsewhere = { key: string; slot: 'argument' | 'content' }
|
|
||||||
|
|
||||||
export function readBlockDirectiveNode(
|
export function readBlockDirectiveNode(
|
||||||
name: string,
|
name: string,
|
||||||
argument: string | undefined,
|
argument: string | undefined,
|
||||||
@@ -70,30 +69,6 @@ function slotText(content: readonly AdfNode[]): string | undefined {
|
|||||||
return only.text
|
return only.text
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readVocabulary(
|
|
||||||
type: string,
|
|
||||||
attributes: DirectiveAttributes,
|
|
||||||
vocabulary: AttributeVocabulary,
|
|
||||||
elsewhere: Elsewhere | undefined,
|
|
||||||
path: ConvertErrorPath,
|
|
||||||
): Result<AdfAttributes> {
|
|
||||||
const attrs: AdfAttributes = {}
|
|
||||||
for (const [key, spelled] of attributes) {
|
|
||||||
if (key === elsewhere?.key) {
|
|
||||||
const place = elsewhere.slot === 'argument' ? 'as the directive argument' : 'in the content slot'
|
|
||||||
return failure('unsupported-node-shape', `${type} spells its ${key} attribute ${place}`, path)
|
|
||||||
}
|
|
||||||
const kind = Object.hasOwn(vocabulary, key) ? vocabulary[key] : undefined
|
|
||||||
if (kind === undefined) return failure('unsupported-node-shape', `${type} holds no ${key} attribute`, path)
|
|
||||||
const read = attributeValue(spelled.decoded, kind)
|
|
||||||
if (read === undefined) return failure('unsupported-node-shape', `the ${key} attribute of ${type} is no ${kind}`, path)
|
|
||||||
const spelling = spellAttributeValue(read)
|
|
||||||
if (spelling !== spelled.spelling) return failure('unsupported-node-shape', `${type} spells its ${key} attribute as ${key}=${spelling}`, path)
|
|
||||||
attrs[key] = read.value
|
|
||||||
}
|
|
||||||
return success(attrs)
|
|
||||||
}
|
|
||||||
|
|
||||||
function readMarks(type: string, spelled: DirectiveValue, path: ConvertErrorPath): Result<AdfMark[]> {
|
function readMarks(type: string, spelled: DirectiveValue, path: ConvertErrorPath): Result<AdfMark[]> {
|
||||||
const read = attributeValue(spelled.decoded, 'json')
|
const read = attributeValue(spelled.decoded, 'json')
|
||||||
const marks = read === undefined || spellAttributeValue(read) !== spelled.spelling ? undefined : readMarkValues(read.value)
|
const marks = read === undefined || spellAttributeValue(read) !== spelled.spelling ? undefined : readMarkValues(read.value)
|
||||||
|
|||||||
@@ -697,6 +697,9 @@ test('names the content slot no one unmarked text node reads back from', () => {
|
|||||||
assert.equal(code(markdownToAdf(':status[:date{timestamp=1}]{color=yellow}\n')), 'unsupported-node-shape')
|
assert.equal(code(markdownToAdf(':status[:date{timestamp=1}]{color=yellow}\n')), 'unsupported-node-shape')
|
||||||
assert.equal(content(markdownToAdf(':status[]{color=yellow}\n')), 'unmappable-image: an image fits only as a paragraph of its own')
|
assert.equal(content(markdownToAdf(':status[]{color=yellow}\n')), 'unmappable-image: an image fits only as a paragraph of its own')
|
||||||
assert.equal(code(markdownToAdf(':status[<div>]{color=yellow}\n')), 'unmappable-html')
|
assert.equal(code(markdownToAdf(':status[<div>]{color=yellow}\n')), 'unmappable-html')
|
||||||
|
// The slot parses before the name's table is consulted, so a doubly-broken span reports its inner error.
|
||||||
|
assert.equal(code(markdownToAdf(':date[<div>]{timestamp=1}\n')), 'unmappable-html')
|
||||||
|
assert.equal(code(markdownToAdf(':widget[<div>]\n')), 'unmappable-html')
|
||||||
assert.equal(content(markdownToAdf('Part :mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot')
|
assert.equal(content(markdownToAdf('Part :mention{id=b1c2 text=A}.\n')), 'unsupported-node-shape: mention spells its text attribute in the content slot')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import type { ConvertFault } from '../result.ts'
|
import type { ConvertFault } from '../result.ts'
|
||||||
import type { DirectiveSpan, Read } from './directive-syntax.ts'
|
import type { DirectiveSpan, Read } from './directive-syntax.ts'
|
||||||
import { spellAttributes, spellStringAttribute } from './directive-syntax.ts'
|
import { spellAttributes, spellLeafDirective, spellStringAttribute } from './directive-syntax.ts'
|
||||||
|
|
||||||
const name = 'text'
|
const name = 'text'
|
||||||
const whitespaceRun = /^(?:[ \t]+|\n+)$/
|
const whitespaceRun = /^(?:[ \t]+|\n+)$/
|
||||||
|
|
||||||
export function spellTextDirective(text: string): string {
|
export function spellTextDirective(text: string): string {
|
||||||
return `:${name}${spellAttributes([[name, spellStringAttribute(text)]])}`
|
return spellLeafDirective(name, spellAttributes([[name, spellStringAttribute(text)]]))
|
||||||
}
|
}
|
||||||
|
|
||||||
export function readTextDirective(span: DirectiveSpan): Read<string> | undefined {
|
export function readTextDirective(span: DirectiveSpan): Read<string> | undefined {
|
||||||
|
|||||||
+2
-1
@@ -357,7 +357,8 @@ Under **3 — `markdownToAdf` (`0.1.0`)**:
|
|||||||
CommonMark carries plainly, are named errors, as 3g refuses the directive form of a node
|
CommonMark carries plainly, are named errors, as 3g refuses the directive form of a node
|
||||||
CommonMark spells. The reader takes the slot's parsed nodes rather than its text, so the
|
CommonMark spells. The reader takes the slot's parsed nodes rather than its text, so the
|
||||||
rule refusing anything but one unmarked text node sits beside the node tables that own the
|
rule refusing anything but one unmarked text node sits beside the node tables that own the
|
||||||
slot, and a node taking no content still names that first.
|
slot, and a node taking no content still names that first. `directive-content-slot` stays
|
||||||
|
with the fixtures, its cause now a marked slot rather than a slot at all.
|
||||||
The same read found the hole the other way: `attemptLine` refused a line edged with a
|
The same read found the hole the other way: `attemptLine` refused a line edged with a
|
||||||
vertical tab or a form feed, where CommonMark strips spaces and tabs alone, so valid
|
vertical tab or a form feed, where CommonMark strips spaces and tabs alone, so valid
|
||||||
CommonMark parsed to a document `adfToMarkdown` then refused. The edges that check covered
|
CommonMark parsed to a document `adfToMarkdown` then refused. The edges that check covered
|
||||||
|
|||||||
@@ -55,7 +55,10 @@ The numbering is the order the work was planned in, not the order it ships.
|
|||||||
the `adf` fence and `:adf{json="…"}` restoring a deep-equal node, invalid JSON in either a
|
the `adf` fence and `:adf{json="…"}` restoring a deep-equal node, invalid JSON in either a
|
||||||
named error, a carry inside a mark spelling another, and the three carve-outs' escapes
|
named error, a carry inside a mark spelling another, and the three carve-outs' escapes
|
||||||
reading as the literal text they hold. 3g refuses the `adf` fence rather than reading a
|
reading as the literal text they hold. 3g refuses the `adf` fence rather than reading a
|
||||||
`codeBlock` from it; the refusal goes when the carry reads it.
|
`codeBlock` from it; the refusal goes when the carry reads it. 3i left the slot parse
|
||||||
|
contextless, so the refusal a carry inside a mark spelling earns needs a channel — a reader
|
||||||
|
context in place of `parseInline`'s `strip` flag, or a return arm from the slot — and
|
||||||
|
`directiveNodes` takes its fourth reader beside it.
|
||||||
`index.ts` gains `markdownToAdf` here, and the README's status line with it: this is the
|
`index.ts` gains `markdownToAdf` here, and the README's status line with it: this is the
|
||||||
last parser chunk, so `parsingDirectories` becomes `emittingDirectories` and the whole
|
last parser chunk, so `parsingDirectories` becomes `emittingDirectories` and the whole
|
||||||
corpus round-trips both ways — `0.1.0`'s proof, which 4 widens rather than replaces.
|
corpus round-trips both ways — `0.1.0`'s proof, which 4 widens rather than replaces.
|
||||||
@@ -109,8 +112,12 @@ The numbering is the order the work was planned in, not the order it ships.
|
|||||||
replaced the regex. Three sites the same sweep did not reach: `normalizeLabel` in
|
replaced the regex. Three sites the same sweep did not reach: `normalizeLabel` in
|
||||||
`link-syntax.ts`, whose shortcut-reference input is `scan.source.slice(...)` rather than the
|
`link-syntax.ts`, whose shortcut-reference input is `scan.source.slice(...)` rather than the
|
||||||
999-capped `readLabel` value, and two in `emit/inline-line.ts`. The fix is the one 3h used —
|
999-capped `readLabel` value, and two in `emit/inline-line.ts`. The fix is the one 3h used —
|
||||||
an index walk, `trimTrailingSpace` where the ends match. §11's scanning rule is the whole
|
an index walk, `trimTrailingSpace` where the ends match. A fourth of another shape joins
|
||||||
argument; the pipeline persona feeds documents nobody typed.
|
them: `readNestedDirective` restarts its depth counter per level, so each parse level
|
||||||
|
re-scans the region below it and nested inline directives cost O(depth × content) — 3f's
|
||||||
|
cost, which 3i's slot parse doubles rather than changes in class, bounded by the 500-level
|
||||||
|
guard. §11's scanning rule is the whole argument; the pipeline persona feeds documents
|
||||||
|
nobody typed.
|
||||||
- [ ] **5 — Release pipeline, ship `0.1.0`.** Publish-on-version-change (§9), `NPM_TOKEN` secret,
|
- [ ] **5 — Release pipeline, ship `0.1.0`.** Publish-on-version-change (§9), `NPM_TOKEN` secret,
|
||||||
the repo made public first (§6). The `ConvertErrorCode` freeze (§8) is checkable here: 3h
|
the repo made public first (§6). The `ConvertErrorCode` freeze (§8) is checkable here: 3h
|
||||||
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
|
||||||
|
|||||||
Reference in New Issue
Block a user