Read the inline nodes and the marks back, and keep the whitespace CommonMark does not strip #41
@@ -98,7 +98,7 @@
|
||||
{
|
||||
"content": [
|
||||
{
|
||||
"text": "Sold as a pair",
|
||||
"text": "\fSold as a pair\u000b",
|
||||
"type": "text"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
| Part | Note |
|
||||
| --- | --- |
|
||||
| Bolt M8 | Grade `8.8` |
|
||||
| Nut \| washer | Sold as a pair |
|
||||
| Nut \| washer | Sold as a pair |
|
||||
| Washer M8 | 100 pcs:hardBreak{}zinc-plated |
|
||||
| Spare | |
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"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"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -87,6 +87,10 @@ export function spellJsonAttribute(value: JsonValue): string {
|
||||
return quote(serializeCanonicalJson(value, 'compact'))
|
||||
}
|
||||
|
||||
export function spellLeafDirective(name: string, attributes: string): string {
|
||||
return `:${name}${attributes === '' ? '{}' : attributes}`
|
||||
}
|
||||
|
||||
export function spellStringAttribute(text: string): string {
|
||||
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 { spellDestination, spellTitle } from '../link-syntax.ts'
|
||||
import { spellInlineNodeAttributes } from './inline-directive-spelling.ts'
|
||||
import { spellLeafDirective } from '../directive-syntax.ts'
|
||||
import { spellTextDirective } from '../text-directive.ts'
|
||||
|
||||
type EmittedLine = { line: string; segments: InlineSegment[] }
|
||||
@@ -121,10 +122,6 @@ function carriedText(text: string): InlineSegment {
|
||||
return syntax(spellTextDirective(text))
|
||||
}
|
||||
|
||||
function spellLeafDirective(name: string, attributes: string): string {
|
||||
return `:${name}${attributes === '' ? '{}' : attributes}`
|
||||
}
|
||||
|
||||
function syntax(text: string): InlineSegment {
|
||||
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 { failure, success, type ConvertErrorPath, type Result } from '../../result.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 {
|
||||
const spelling = markSpelling(name)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
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 { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts'
|
||||
import type { Elsewhere } from './directive-attributes.ts'
|
||||
import { attributeValue, spellAttributeValue, unknownDirectiveFault } from '../directive-syntax.ts'
|
||||
import { blockArgument } from '../block-directive-arguments.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 { inlineDirective } from '../../adf/inline-directives.ts'
|
||||
import { marksAttribute, readMarkValues } from '../block-directive-marks.ts'
|
||||
import { readVocabulary } from './directive-attributes.ts'
|
||||
|
||||
export type BlockDirectiveNode = { contentModel: BlockDirective['contentModel']; node: AdfNode }
|
||||
|
||||
type Elsewhere = { key: string; slot: 'argument' | 'content' }
|
||||
|
||||
export function readBlockDirectiveNode(
|
||||
name: string,
|
||||
argument: string | undefined,
|
||||
@@ -70,30 +69,6 @@ function slotText(content: readonly AdfNode[]): string | undefined {
|
||||
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[]> {
|
||||
const read = attributeValue(spelled.decoded, 'json')
|
||||
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(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')
|
||||
// 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')
|
||||
})
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { ConvertFault } from '../result.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 whitespaceRun = /^(?:[ \t]+|\n+)$/
|
||||
|
||||
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 {
|
||||
|
||||
+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 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
|
||||
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
|
||||
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
|
||||
|
||||
@@ -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
|
||||
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
|
||||
`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
|
||||
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.
|
||||
@@ -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
|
||||
`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 —
|
||||
an index walk, `trimTrailingSpace` where the ends match. §11's scanning rule is the whole
|
||||
argument; the pipeline persona feeds documents nobody typed.
|
||||
an index walk, `trimTrailingSpace` where the ends match. A fourth of another shape joins
|
||||
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,
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user