3
0
mirror of https://github.com/larvit/adf-codec.git synced 2026-11-12 19:58:28 +01:00

Give the directive grammar, flavours and reader failures one place each

This commit is contained in:
2026-10-21 23:43:05 +02:00
committed by Lillem4n
parent db10287cae
commit b080410852
37 changed files with 212 additions and 189 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ In `docs/decisions.md`:
- Firefox reads the build
- The coverage floors
- The size ratchet
- The project ships under the comprehension floor until items 59, 60, 62 and 95 land
- The project ships under the comprehension floor until items 62 and 95 land
- Properties on a fixed seed
- The CommonMark suite checks three ways
- The flavour spec is read as a source
+14 -13
View File
@@ -687,11 +687,11 @@ config gone missing fails the leg instead of falling back to oxlint's own defaul
`--deny-warnings`, since a rule from a category this config never names arrives as a warning it
exits 0 on.
## The project ships under the comprehension floor until items 59, 60, 62 and 95 land
## The project ships under the comprehension floor until items 62 and 95 land
2026-10-03, the maintainer; item 95 in place of item 61, 2026-10-09, the maintainer. KISS, a
technical principle, and its comprehension floor of 7.
Valid while `todo.md` items 59, 60, 62 and 95 are open.
Valid while `todo.md` items 62 and 95 are open.
A four-seat comprehension panel scored the project under the floor of 7. Its scoring run of
2026-10-08 is the baseline.
@@ -924,14 +924,15 @@ them. A primitive knowing neither ADF nor a format stays at `src/` root. A const
what both read where there are two.
A flavour's own code, both directions included, sits in its own directory: `markdown/portable/`;
`todo.md` item 62 moves what still sits in `parse/` and `emit/`. Otherwise each format directory
parts into `emit/` (ADF→format) and `parse/` (format→ADF), the rest of it holding what both
directions read. A construct's reader lives there beside the regex the emitter escapes against, so
the two cannot drift; a reader with no emit counterpart goes in `parse/`, unless it is part of a
construct that side already holds — a grammar stays in one file rather than splitting across the
seam. A rule both directions must answer alike — whether a list marker interrupts a paragraph — is
one function there too, never a copy per direction, however conservative the copy would be. Where
the rule is the emitter's own choice, input consults it rather than restating it, and that is the
only value import `parse/` takes from `emit/` — `commonMarkSpelling` and `openingLinkTakesDirective`
— so no fixture the emitter writes can be refused, and a spelling the emitter refuses gives its own
error rather than a second name for it.
`todo.md` item 62 moves what still sits in `parse/` and `emit/`. The directive grammar likewise
sits in `markdown/directive/`. Otherwise each format directory parts into `emit/` (ADF→format) and
`parse/` (format→ADF), the rest of it holding what both directions read. A construct's reader lives
there beside the regex the emitter escapes against, so the two cannot drift; a reader with no emit
counterpart goes in `parse/`, unless it is part of a construct that side already holds — a grammar
stays in one file rather than splitting across the seam. A rule both directions must answer alike —
whether a list marker interrupts a paragraph — is one function there too, never a copy per
direction, however conservative the copy would be. Where the rule is the emitter's own choice, input
consults it rather than restating it, and that is the only import `parse/` takes from `emit/`,
through `emit/spelling-questions.ts`, which names `commonMarkSpelling` and
`openingLinkTakesDirective` and which a test holds — so no fixture the emitter writes can be
refused, and a spelling the emitter refuses gives its own error rather than a second name for it.
+1 -1
View File
@@ -6,7 +6,7 @@ import { readFileSync } from 'node:fs'
import test from 'node:test'
import type { AttributeKind, AttributeVocabulary } from '../adf/attribute-vocabulary.ts'
import { blockArgument } from '../markdown/block-directive.ts'
import { blockArgument } from '../markdown/directive/block.ts'
import { blockNodes } from '../adf/block-nodes.ts'
import { codeTextMarkTypes, inlineNodes } from '../adf/inline-nodes.ts'
import { markVocabularies } from '../adf/mark-attributes.ts'
+1 -1
View File
@@ -8,7 +8,7 @@ import type { AttributeKind, AttributeVocabulary } from '../adf/attribute-vocabu
import { blockNodes } from '../adf/block-nodes.ts'
import { inlineNodes } from '../adf/inline-nodes.ts'
import { blockMarkAttributes, markAttributes, markVocabularies } from '../adf/mark-attributes.ts'
import { textDirectiveName } from '../markdown/text-directive.ts'
import { textDirectiveName } from '../markdown/directive/text.ts'
type Declared = { attributes: AttributeVocabulary }
+3 -3
View File
@@ -10,7 +10,7 @@ import type { Result } from '../result.ts'
import { adfDocument, attributes, blockTaskItemChildren, jsonKey, jsonValue, markdownPieces, propertyRuns, propertyTimeout, textOf } from './property-harness.ts'
import { adfToLosslessMarkdown } from '../markdown/emit/adf-to-markdown.ts'
import { adfToPortableMarkdown } from '../markdown/portable/adf-to-portable-markdown.ts'
import { blockArgument, marksAttribute } from '../markdown/block-directive.ts'
import { blockArgument, marksAttribute } from '../markdown/directive/block.ts'
import { blockNodes } from '../adf/block-nodes.ts'
import { carryName } from '../markdown/opaque-carry.ts'
import { commonMarkToAdf, losslessMarkdownToAdf, portableMarkdownToAdf } from '../markdown/parse/markdown-to-adf.ts'
@@ -24,14 +24,14 @@ import {
spellJsonAttribute,
spellStringAttribute,
spellVocabulary,
} from '../markdown/directive-syntax.ts'
} from '../markdown/directive/syntax.ts'
import { fencedCodeBlock } from '../markdown/commonmark/backtick-runs.ts'
import { inlineNodes } from '../adf/inline-nodes.ts'
import { markAttributes } from '../adf/mark-attributes.ts'
import { markSpelling } from '../markdown/mark-spellings.ts'
import { nodeContent, nodeMarks } from '../adf/document.ts'
import { serializeCanonicalJson } from '../canonical-json.ts'
import { textDirectiveName } from '../markdown/text-directive.ts'
import { textDirectiveName } from '../markdown/directive/text.ts'
import { vocabularyPairs } from '../adf/attribute-vocabulary.ts'
type Choice = { arbitrary: Arbitrary<string>; hostile?: true; weight: number }
+28
View File
@@ -0,0 +1,28 @@
import assert from 'node:assert/strict'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { readdirSync, readFileSync } from 'node:fs'
import test from 'node:test'
const markdown = join(dirname(fileURLToPath(import.meta.url)), '..', 'markdown')
const importSpecifier = /\bfrom '([^']+)'/g
function importsOf(directory: string): { file: string; specifier: string }[] {
const imports: { file: string; specifier: string }[] = []
for (const file of readdirSync(join(markdown, directory)).filter((name) => name.endsWith('.ts') && !name.endsWith('.test.ts'))) {
for (const match of readFileSync(join(markdown, directory, file), 'utf8').matchAll(importSpecifier)) {
imports.push({ file: `${directory}/${file}`, specifier: match[1] ?? '' })
}
}
return imports
}
// docs/decisions.md §The source parts by ADF and format
test('parse/ asks emit/ only through the module that names its questions', () => {
const reached = importsOf('parse').filter(({ specifier }) => specifier.startsWith('../emit/'))
assert.ok(reached.length > 0)
assert.deepEqual(
reached.filter(({ specifier }) => specifier !== '../emit/spelling-questions.ts'),
[],
)
})
+2 -2
View File
@@ -7,11 +7,11 @@ import type { Arbitrary } from 'fast-check'
import type { AttributeKind, AttributeVocabulary } from '../adf/attribute-vocabulary.ts'
import type { JsonValue } from '../json-value.ts'
import type { LinkDefinition } from '../markdown/commonmark/link-syntax.ts'
import { blockArgument } from '../markdown/block-directive.ts'
import { blockArgument } from '../markdown/directive/block.ts'
import { blockMarkAttributes, markAttributes } from '../adf/mark-attributes.ts'
import { blockNodes } from '../adf/block-nodes.ts'
import { centeredImage, externalMedia } from '../markdown/external-image.ts'
import { directivePrefix } from '../markdown/directive-syntax.ts'
import { directivePrefix } from '../markdown/directive/syntax.ts'
import { emptyKeys, mergeAdjacentText, nodeContent } from '../adf/document.ts'
import { inlineNodes } from '../adf/inline-nodes.ts'
import { joinsWhenRead } from '../markdown/adjacent-text.ts'
+1 -1
View File
@@ -1,6 +1,6 @@
import type { AdfNode } from '../adf/document.ts'
import { identicalMarks, isSpellableText, nodeMarks } from '../adf/document.ts'
import { spellInlineLeafDirective } from './directive-syntax.ts'
import { spellInlineLeafDirective } from './directive/syntax.ts'
export const textBreakName = 'textBreak'
+1 -1
View File
@@ -1,4 +1,4 @@
import type { Claims } from './portable/conventions.ts'
import type { Claims } from './flavour.ts'
import type { JsonValue } from '../json-value.ts'
import { carryFenceType } from './opaque-carry.ts'
import { infoStringCarries } from './commonmark/grammar.ts'
+2 -2
View File
@@ -1,8 +1,8 @@
import type { AdfAttributes } from '../../adf/document.ts'
import type { AttributeVocabulary } from '../../adf/attribute-vocabulary.ts'
import type { DirectiveAttributes } from '../directive-syntax.ts'
import type { DirectiveAttributes } from './syntax.ts'
import { attributeNestingMessage } from '../../adf/document.ts'
import { attributeValue, spellAttributeValue } from '../directive-syntax.ts'
import { attributeValue, spellAttributeValue } from './syntax.ts'
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
export type Elsewhere = { key: string; slot: 'argument' | 'content' }
+2 -2
View File
@@ -1,9 +1,9 @@
import type { AdfNode } from '../../adf/document.ts'
import type { BlockNodeModel } from '../../adf/block-nodes.ts'
import { attributeNestingMessage, nodeAttrs, nodeMarks } from '../../adf/document.ts'
import { blockArgument, marksAttribute, spellBlockMarks } from '../block-directive.ts'
import { blockArgument, marksAttribute, spellBlockMarks } from './block.ts'
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { isBareToken, spellAttributes, spellDirectiveOpener, spellVocabulary } from '../directive-syntax.ts'
import { isBareToken, spellAttributes, spellDirectiveOpener, spellVocabulary } from './syntax.ts'
import { spellEmptyKeys } from '../empty-keys.ts'
import { vocabularyPairs } from '../../adf/attribute-vocabulary.ts'
+10 -10
View File
@@ -1,13 +1,13 @@
import type { AdfMark } from '../adf/document.ts'
import type { BlockType } from '../adf/block-nodes.ts'
import type { JsonValue } from '../json-value.ts'
import { blockNodeModel } from '../adf/block-nodes.ts'
import { isAdfMark } from '../adf/document.ts'
import { markVocabularies } from '../adf/mark-attributes.ts'
import { overNested } from '../json-value.ts'
import { serializeCanonicalJson } from '../canonical-json.ts'
import { spellAttributes, spellDirectiveOpener, spellJsonAttribute, spellStringAttribute } from './directive-syntax.ts'
import { spellMarkVocabulary } from './mark-spellings.ts'
import type { AdfMark } from '../../adf/document.ts'
import type { BlockType } from '../../adf/block-nodes.ts'
import type { JsonValue } from '../../json-value.ts'
import { blockNodeModel } from '../../adf/block-nodes.ts'
import { isAdfMark } from '../../adf/document.ts'
import { markVocabularies } from '../../adf/mark-attributes.ts'
import { overNested } from '../../json-value.ts'
import { serializeCanonicalJson } from '../../canonical-json.ts'
import { spellAttributes, spellDirectiveOpener, spellJsonAttribute, spellStringAttribute } from './syntax.ts'
import { spellMarkVocabulary } from '../mark-spellings.ts'
const argumentByType = new Map(
Object.entries({
+1 -1
View File
@@ -1,7 +1,7 @@
import type { AdfNode } from '../../adf/document.ts'
import type { InlineNodeModel } from '../../adf/inline-nodes.ts'
import { nodeAttrs } from '../../adf/document.ts'
import { spellAttributes, spellVocabulary } from '../directive-syntax.ts'
import { spellAttributes, spellVocabulary } from './syntax.ts'
import { spellEmptyKeys } from '../empty-keys.ts'
import { vocabularyPairs } from '../../adf/attribute-vocabulary.ts'
+3 -3
View File
@@ -1,13 +1,13 @@
import type { AdfMark } from '../../adf/document.ts'
import type { AttributeVocabulary } from '../../adf/attribute-vocabulary.ts'
import type { ConvertFault } from '../../result.ts'
import type { DirectiveAttributes } from '../directive-syntax.ts'
import type { DirectiveAttributes } from './syntax.ts'
import type { MarkSpelling } from '../mark-spellings.ts'
import { directivePrefix } from '../directive-syntax.ts'
import { directivePrefix } from './syntax.ts'
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { markSpelling } from '../mark-spellings.ts'
import { readEmptyKeys } from '../empty-keys.ts'
import { readVocabulary } from './directive-attributes.ts'
import { readVocabulary } from './attributes.ts'
export function readDirectiveMark(name: string, attributes: DirectiveAttributes, path: ConvertErrorPath): Result<AdfMark> | undefined {
const spelling = markSpelling(name)
+8 -8
View File
@@ -1,22 +1,22 @@
import type { AdfAttributes, AdfMark, AdfNode, EmptyKey } from '../../adf/document.ts'
import type { BlockNodeModel } from '../../adf/block-nodes.ts'
import type { ConvertFault } from '../../result.ts'
import type { DirectiveAttributes, DirectiveValue } from '../directive-syntax.ts'
import type { Elsewhere } from './directive-attributes.ts'
import type { DirectiveAttributes, DirectiveValue } from './syntax.ts'
import type { Elsewhere } from './attributes.ts'
import { attributeNestingMessage, isUnmarkedSpellableText } from '../../adf/document.ts'
import { attributePairCause, attributeValue, directivePrefix, readAttributes, readDirectiveName, unknownDirectiveFault } from '../directive-syntax.ts'
import { blockArgument, blockDirectiveForm, marksAttribute, readMarkValues, spellBlockMarks } from '../block-directive.ts'
import { attributePairCause, attributeValue, directivePrefix, readAttributes, readDirectiveName, unknownDirectiveFault } from './syntax.ts'
import { blockArgument, blockDirectiveForm, marksAttribute, readMarkValues, spellBlockMarks } from './block.ts'
import { blockNodeModel } from '../../adf/block-nodes.ts'
import { carryFencePrefix, carryName } from '../opaque-carry.ts'
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { inlineMarkSpellingFault, readMarkAttributes } from './directive-marks.ts'
import { inlineMarkSpellingFault, readMarkAttributes } from './marks.ts'
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
import { markVocabularies } from '../../adf/mark-attributes.ts'
import { readEmptyKeys, spellsEmpty } from '../empty-keys.ts'
import { readVocabulary } from './directive-attributes.ts'
import { slotFault } from '../directive-syntax.ts'
import { readVocabulary } from './attributes.ts'
import { slotFault } from './syntax.ts'
import { textBreakName } from '../adjacent-text.ts'
import { textDirectiveName } from '../text-directive.ts'
import { textDirectiveName } from './text.ts'
export type BlockDirectiveNode = { contentModel: BlockNodeModel['contentModel']; node: AdfNode }
+4 -4
View File
@@ -1,10 +1,10 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import type { DirectiveAttributes, DirectiveLine } from './directive-syntax.ts'
import { claimDirectivePrefix, readDirectiveLine, readInlineDirective } from './directive-syntax.ts'
import { inlineSource } from './inline-tokens.ts'
import { largestNesting } from '../nesting.ts'
import type { DirectiveAttributes, DirectiveLine } from './syntax.ts'
import { claimDirectivePrefix, readDirectiveLine, readInlineDirective } from './syntax.ts'
import { inlineSource } from '../inline-tokens.ts'
import { largestNesting } from '../../nesting.ts'
// A pair the input spells bare decodes to itself; a quoted one names its spelling beside the decoding.
type Pair = [string, string, string?]
+12 -14
View File
@@ -1,15 +1,15 @@
import type { AttributeKind, VocabularyPair, VocabularyValue } from '../adf/attribute-vocabulary.ts'
import type { CodeSpanCloser } from './commonmark/backtick-runs.ts'
import type { ConvertFault } from '../result.ts'
import type { InlineSource } from './inline-tokens.ts'
import type { JsonValue } from '../json-value.ts'
import { backslashEscape, holdsNullCharacter } from './commonmark/grammar.ts'
import { backtickRun } from './commonmark/backtick-runs.ts'
import { isNumberList } from '../adf/attribute-vocabulary.ts'
import { overNested } from '../json-value.ts'
import { largestNesting } from '../nesting.ts'
import { parseJsonText } from '../json-text.ts'
import { serializeCanonicalJson } from '../canonical-json.ts'
import type { AttributeKind, VocabularyPair, VocabularyValue } from '../../adf/attribute-vocabulary.ts'
import type { CodeSpanCloser } from '../commonmark/backtick-runs.ts'
import type { ConvertFault, Read } from '../../result.ts'
import type { InlineSource } from '../inline-tokens.ts'
import type { JsonValue } from '../../json-value.ts'
import { backslashEscape, holdsNullCharacter } from '../commonmark/grammar.ts'
import { backtickRun } from '../commonmark/backtick-runs.ts'
import { isNumberList } from '../../adf/attribute-vocabulary.ts'
import { overNested } from '../../json-value.ts'
import { largestNesting } from '../../nesting.ts'
import { parseJsonText } from '../../json-text.ts'
import { serializeCanonicalJson } from '../../canonical-json.ts'
export type AttributeReading = { refusal: 'kind' | 'nesting'; value?: undefined } | { refusal?: undefined; value: VocabularyValue }
@@ -31,8 +31,6 @@ export type DirectiveSpan = { attributes: DirectiveAttributes; content: string |
export type NestedSpans = ReadonlyMap<number, DirectiveSpan>
export type Read<T> = { fault: ConvertFault; value?: undefined } | { fault?: undefined; value: T }
type AttributePair = { end: number; key: string; value: DirectiveValue }
type Content = { content: string | undefined; end: number; spans: NestedSpans }
+3 -2
View File
@@ -1,5 +1,6 @@
import type { DirectiveSpan, Read } from './directive-syntax.ts'
import { readSoleStringAttribute, spellAttributes, spellInlineLeafDirective, spellStringAttribute, unsupportedNodeShape } from './directive-syntax.ts'
import type { DirectiveSpan } from './syntax.ts'
import type { Read } from '../../result.ts'
import { readSoleStringAttribute, spellAttributes, spellInlineLeafDirective, spellStringAttribute, unsupportedNodeShape } from './syntax.ts'
const name = 'text'
const spelledRun = /^(?:[ \t]+|\n+|\0+)$/
+6 -5
View File
@@ -1,20 +1,21 @@
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
import type { BlockNodeModel } from '../../adf/block-nodes.ts'
import type { WrittenFlavour } from '../portable/conventions.ts'
import type { WrittenFlavour } from '../flavour.ts'
import { adfDocumentFault, heldTextLength, holdsOnlyAttributes, isUnmarkedSpellableText, nodeAttrs, nodeContent } from '../../adf/document.ts'
import { alertMarker, flavourClaims, foldedAlertMarker, leadingMarker, readAlertMarker, readTaskMarker, taskMarker } from '../portable/conventions.ts'
import { blockDirectiveForm, documentSpelling } from '../block-directive.ts'
import { alertMarker, foldedAlertMarker, leadingMarker, readAlertMarker, readTaskMarker, taskMarker } from '../portable/conventions.ts'
import { blockDirectiveForm, documentSpelling } from '../directive/block.ts'
import { blockNodeModel, blockNodes, isTaskItem } from '../../adf/block-nodes.ts'
import { carriedBlock } from '../opaque-carry.ts'
import { emitInlineLine } from './inline-line.ts'
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { fencedCodeBlock } from '../commonmark/backtick-runs.ts'
import { flavourClaims } from '../flavour.ts'
import { holdsNullCharacter, isBlankLine, isThematicBreak, markerInterruptsParagraph } from '../commonmark/grammar.ts'
import { languageSlot, type LanguageSlot } from '../code-language.ts'
import { largestNesting } from '../../nesting.ts'
import { longestMarkdown, tooLong, withinEngineLimits, withinLongest } from '../length-bound.ts'
import { spellBlockDirectiveOpener } from './block-directive-spelling.ts'
import { spellDirectiveCloser, spellDirectiveOpener } from '../directive-syntax.ts'
import { spellBlockDirectiveOpener } from '../directive/block-spelling.ts'
import { spellDirectiveCloser, spellDirectiveOpener } from '../directive/syntax.ts'
import { tryImage } from './image.ts'
import { tryPipeTable } from './pipe-table.ts'
+6 -5
View File
@@ -2,22 +2,23 @@ import type { AdfMark, AdfNode } from '../../adf/document.ts'
import type { InlineNodeModel } from '../../adf/inline-nodes.ts'
import type { LineContainer } from '../line-container.ts'
import type { LinkDefinition } from '../commonmark/link-syntax.ts'
import type { WrittenFlavour } from '../portable/conventions.ts'
import type { WrittenFlavour } from '../flavour.ts'
import { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, type MarkRun, type NodeRange } from './line-escaping.ts'
import { carriedInline } from '../opaque-carry.ts'
import { commonMarkLink, linkHref, markSpelling, spellMarkAttributes } from '../mark-spellings.ts'
import { escapeUnbalanced, spellDestination, spellLinkTarget } from '../commonmark/link-syntax.ts'
import { failure, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { flavourClaims, highlightDelimiter } from '../portable/conventions.ts'
import { flavourClaims } from '../flavour.ts'
import { highlightDelimiter } from '../portable/conventions.ts'
import { holdsNullCharacter, trimTrailingSpace } from '../commonmark/grammar.ts'
import { identicalMark, isSpellableText, nodeAttrs, nodeContent, nodeMarks } from '../../adf/document.ts'
import { inlineNodeModel, takenMarkTypes } from '../../adf/inline-nodes.ts'
import { joinsWhenRead, textBreakSpelling } from '../adjacent-text.ts'
import { largestNesting } from '../../nesting.ts'
import { longestBacktickRun } from '../commonmark/backtick-runs.ts'
import { slotFault, spellInlineDirectiveOpener, spellInlineLeafDirective } from '../directive-syntax.ts'
import { spellInlineNodeAttributes } from './inline-directive-spelling.ts'
import { spellTextDirective } from '../text-directive.ts'
import { slotFault, spellInlineDirectiveOpener, spellInlineLeafDirective } from '../directive/syntax.ts'
import { spellInlineNodeAttributes } from '../directive/inline-spelling.ts'
import { spellTextDirective } from '../directive/text.ts'
type EmittedLine = { line: string; openingLinkAsDirective: boolean; segments: InlineSegment[] }
+3 -2
View File
@@ -1,10 +1,11 @@
import type { Claims, WrittenFlavour } from '../portable/conventions.ts'
import type { Claims, WrittenFlavour } from '../flavour.ts'
import type { InlineSource, InlineToken } from '../inline-tokens.ts'
import type { LineContainer } from '../line-container.ts'
import { delimiterFlags, isWordCharacter, matchEmphasis, runLength } from '../commonmark/emphasis-matching.ts'
import { delimiterRunToken, inlineSource, readInlineToken } from '../inline-tokens.ts'
import { escapesLineClaim, type LinePosition } from '../commonmark/grammar.ts'
import { flavourClaims, highlightFlanking } from '../portable/conventions.ts'
import { flavourClaims } from '../flavour.ts'
import { highlightFlanking } from '../portable/conventions.ts'
import { isBareDelimiterRow } from '../pipe-table-syntax.ts'
import { opensLinkDefinition } from '../commonmark/link-reference-definitions.ts'
+1 -1
View File
@@ -1,6 +1,6 @@
import type { AdfNode } from '../../adf/document.ts'
import type { ConvertErrorPath } from '../../result.ts'
import type { WrittenFlavour } from '../portable/conventions.ts'
import type { WrittenFlavour } from '../flavour.ts'
import { holdsOnlyAttributes, nodeContent } from '../../adf/document.ts'
import { spellPipeDelimiter, spellPipeRow } from '../pipe-table-syntax.ts'
import { tryPipeCell } from './inline-line.ts'
+3
View File
@@ -0,0 +1,3 @@
// The only file `parse/` imports from `emit/` (docs/decisions.md §The source parts by ADF and format).
export { commonMarkSpelling, type SpellingMemo } from './adf-to-markdown.ts'
export { openingLinkTakesDirective } from './inline-line.ts'
+3 -2
View File
@@ -1,7 +1,8 @@
import type { DirectiveAttributes, DirectiveValue, Read } from './directive-syntax.ts'
import type { DirectiveAttributes, DirectiveValue } from './directive/syntax.ts'
import type { EmptyKey } from '../adf/document.ts'
import type { Read } from '../result.ts'
import { emptyKeys } from '../adf/document.ts'
import { unsupportedNodeShape } from './directive-syntax.ts'
import { unsupportedNodeShape } from './directive/syntax.ts'
type EmptyKeysRead = { empty: ReadonlySet<EmptyKey>; rest: Map<string, DirectiveValue> }
+26
View File
@@ -0,0 +1,26 @@
export type Flavour = 'commonmark' | 'lossless' | 'portable'
export type WrittenFlavour = Exclude<Flavour, 'commonmark'>
// What a reader reads past CommonMark, and so what its writer escapes (docs/decisions.md §Each call names its flavour).
export type Claims = {
alerts: boolean
carryFence: boolean
directives: boolean
highlights: boolean
// `gfm`: a row may drop its leading `|`, and an alignment colon is read and dropped; `flavour`: every row opens with `|`, the delimiter row `-` runs only.
pipeTables: 'flavour' | 'gfm' | false
strikethrough: boolean
taskMarkers: boolean
}
type WrittenClaims = Claims & { pipeTables: 'flavour' | 'gfm'; strikethrough: true }
// Directives exclude alerts and highlights: an expand directive leaves `inExpand` unset and a directive's slot reads `==`.
type ReadClaims = Claims & ({ directives: false } | { alerts: false; directives: true; highlights: false })
export const flavourClaims: Readonly<{ commonmark: ReadClaims } & Record<WrittenFlavour, ReadClaims & WrittenClaims>> = {
commonmark: { alerts: false, carryFence: false, directives: false, highlights: false, pipeTables: false, strikethrough: false, taskMarkers: false },
lossless: { alerts: false, carryFence: true, directives: true, highlights: false, pipeTables: 'flavour', strikethrough: true, taskMarkers: false },
portable: { alerts: true, carryFence: false, directives: false, highlights: true, pipeTables: 'gfm', strikethrough: true, taskMarkers: true },
}
+1 -1
View File
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict'
import test from 'node:test'
import { adfToLosslessMarkdown } from './emit/adf-to-markdown.ts'
import { flavourClaims } from './portable/conventions.ts'
import { flavourClaims } from './flavour.ts'
import { inlineSource, readInlineToken } from './inline-tokens.ts'
import { losslessMarkdownToAdf } from './parse/markdown-to-adf.ts'
+3 -3
View File
@@ -1,9 +1,9 @@
import type { ClaimedPrefix } from './directive-syntax.ts'
import type { Claims } from './portable/conventions.ts'
import type { ClaimedPrefix } from './directive/syntax.ts'
import type { Claims } from './flavour.ts'
import type { CodeSpanCloser } from './commonmark/backtick-runs.ts'
import { backslashEscape, inlineHtmlConstruct, readBracketedAutolink, readEmailAutolink } from './commonmark/grammar.ts'
import { backtickCloser, backtickRun } from './commonmark/backtick-runs.ts'
import { claimDirectivePrefix } from './directive-syntax.ts'
import { claimDirectivePrefix } from './directive/syntax.ts'
import { delimiterFlags, runLength } from './commonmark/emphasis-matching.ts'
import { highlightDelimiter, highlightFlanking } from './portable/conventions.ts'
import { readEntityReference } from './commonmark/entity-references.ts'
+1 -1
View File
@@ -4,7 +4,7 @@ import type { LinkDefinition } from './commonmark/link-syntax.ts'
import type { MarkType } from '../adf/mark-attributes.ts'
import { emptyKeys, nodeAttrs, nodeMarks } from '../adf/document.ts'
import { escapeUnbalanced, spellLinkTarget } from './commonmark/link-syntax.ts'
import { holdsDirectivePrefix, spellAttributes, spellVocabulary } from './directive-syntax.ts'
import { holdsDirectivePrefix, spellAttributes, spellVocabulary } from './directive/syntax.ts'
import { holdsEntityReference } from './commonmark/entity-references.ts'
import { isAutolink } from './commonmark/grammar.ts'
import { isMarkType, markAttributes } from '../adf/mark-attributes.ts'
+3 -3
View File
@@ -1,10 +1,10 @@
import type { AdfNode } from '../adf/document.ts'
import type { DirectiveSpan, Read } from './directive-syntax.ts'
import type { DirectiveSpan } from './directive/syntax.ts'
import type { JsonSpelling } from '../canonical-json.ts'
import type { JsonValue } from '../json-value.ts'
import { canonicalJsonPieces, spellsCanonicalJson } from '../canonical-json.ts'
import { directiveEscape, malformedDirective, readSoleStringAttribute, spellAttributes, spellDirectiveOpener, spellInlineLeafDirective, spellStringAttribute, unsupportedNodeShape } from './directive-syntax.ts'
import { failure, success, type ConvertErrorPath, type Result } from '../result.ts'
import { directiveEscape, malformedDirective, readSoleStringAttribute, spellAttributes, spellDirectiveOpener, spellInlineLeafDirective, spellStringAttribute, unsupportedNodeShape } from './directive/syntax.ts'
import { failure, success, type ConvertErrorPath, type Read, type Result } from '../result.ts'
import { fencedCodeBlock } from './commonmark/backtick-runs.ts'
import { infoStringCarries } from './commonmark/grammar.ts'
import { isAdfNode } from '../adf/document.ts'
+9 -10
View File
@@ -3,15 +3,14 @@ import test from 'node:test'
import type { Block } from './blocks.ts'
import type { LinkDefinition } from '../commonmark/link-syntax.ts'
import { flavourClaims } from '../portable/conventions.ts'
import { parseBlocks } from './blocks.ts'
function definitions(markdown: string): [string, LinkDefinition][] {
return [...parseBlocks(markdown, flavourClaims.lossless).definitions]
return [...parseBlocks(markdown, 'lossless').definitions]
}
function kinds(markdown: string): string[] {
return parseBlocks(markdown, flavourClaims.lossless).blocks.map((block) => block.kind)
return parseBlocks(markdown, 'lossless').blocks.map((block) => block.kind)
}
function faults(markdown: string): string[] {
@@ -24,7 +23,7 @@ function faults(markdown: string): string[] {
if (block.kind === 'bulletList' || block.kind === 'orderedList') for (const item of block.items) walk(item)
}
}
walk(parseBlocks(markdown, flavourClaims.lossless).blocks)
walk(parseBlocks(markdown, 'lossless').blocks)
return messages
}
@@ -90,7 +89,7 @@ test('opens a container where the content model takes content, and holds it open
assert.deepEqual(faults('!adf:panel info\n> Part.\n!adf:/panel\n'), [])
assert.deepEqual(faults('!adf:panel info\n- !adf:panel warning\n Part.\n !adf:/panel\n!adf:/panel\n'), [])
assert.deepEqual(faults('!adf:panel info\n```\n!adf:/panel\n```\n!adf:/panel\n'), [])
assert.deepEqual(parseBlocks('!adf:panel info {panelColor="#ff0000"}\nPart.\n!adf:/panel\n', flavourClaims.lossless).blocks, [
assert.deepEqual(parseBlocks('!adf:panel info {panelColor="#ff0000"}\nPart.\n!adf:/panel\n', 'lossless').blocks, [
{
argument: 'info',
attributes: new Map([['panelColor', { decoded: '#ff0000', spelling: '"#ff0000"' }]]),
@@ -100,7 +99,7 @@ test('opens a container where the content model takes content, and holds it open
position: { line: 1, offset: 0 },
},
])
assert.deepEqual(parseBlocks('!adf:rule\n', flavourClaims.lossless).blocks, [
assert.deepEqual(parseBlocks('!adf:rule\n', 'lossless').blocks, [
{ argument: undefined, attributes: new Map(), blocks: undefined, kind: 'directive', name: 'rule', position: { line: 1, offset: 0 } },
])
})
@@ -125,7 +124,7 @@ test("releases an indented code block's held blank lines as a walk rather than a
// A level is one list item.
function deepestItem(markdown: string): { depth: number; kinds: string[] } {
let blocks = parseBlocks(markdown, flavourClaims.lossless).blocks
let blocks = parseBlocks(markdown, 'lossless').blocks
let depth = 0
for (let list = blocks[0]; list?.kind === 'bulletList'; list = blocks[0]) {
blocks = list.items.at(-1) ?? []
@@ -148,7 +147,7 @@ test('stops a blank line at the first blockquote', () => {
test('stops a blank line at an empty item, and gives a code block in an item the blank line without its spaces', () => {
assert.deepEqual(deepestItem('- - \n\n a\n'), { depth: 2, kinds: [] })
assert.deepEqual(parseBlocks('- ```\n \n x\n ```\n', flavourClaims.lossless).blocks.map((block) => block.kind === 'bulletList' && block.items[0]?.[0]), [
assert.deepEqual(parseBlocks('- ```\n \n x\n ```\n', 'lossless').blocks.map((block) => block.kind === 'bulletList' && block.items[0]?.[0]), [
{ kind: 'code', language: '', position: { line: 1, offset: 0 }, text: '\nx' },
])
})
@@ -166,7 +165,7 @@ function outline(blocks: readonly Block[]): string {
}
test('reads a marker a tab indents four or more columns into its container as text', () => {
const read = (markdown: string): string => outline(parseBlocks(markdown, flavourClaims.lossless).blocks)
const read = (markdown: string): string => outline(parseBlocks(markdown, 'lossless').blocks)
assert.equal(read('> a\n \t> b\n'), 'quote(paragraph(a|> b))')
assert.equal(read('> > a\n>\t\t> b\n'), 'quote(quote(paragraph(a|> b)))')
assert.equal(read('- > a\n\t\t> b\n'), 'list(quote(paragraph(a|> b)))')
@@ -174,7 +173,7 @@ test('reads a marker a tab indents four or more columns into its container as te
})
test("keeps as spaces the columns a tab holds past an item's indentation", () => {
const read = (markdown: string): string => outline(parseBlocks(markdown, flavourClaims.lossless).blocks)
const read = (markdown: string): string => outline(parseBlocks(markdown, 'lossless').blocks)
assert.equal(read('- ```\n\tx\n ```\n'), 'list(code(" x"))')
assert.equal(read('- a\n \t- b\n'), 'list(paragraph(a) list(paragraph(b)))')
})
+7 -6
View File
@@ -1,6 +1,6 @@
import type { Claims } from '../portable/conventions.ts'
import type { Claims, Flavour } from '../flavour.ts'
import type { ConvertFault, SourcePosition } from '../../result.ts'
import type { DirectiveAttributes, DirectiveLine } from '../directive-syntax.ts'
import type { DirectiveAttributes, DirectiveLine } from '../directive/syntax.ts'
import type { LinkDefinition } from '../commonmark/link-syntax.ts'
import {
atxHeading,
@@ -20,8 +20,9 @@ import {
type ThematicBreakTail,
} from '../commonmark/grammar.ts'
import { barePipeCells, isDelimiterRow, isPipeAlignment, malformedPipeTable, pipeCells } from '../pipe-table-syntax.ts'
import { blockDirectiveForm } from '../block-directive.ts'
import { directiveEscape, malformedDirective, readDirectiveLine, spellDirectiveCloser } from '../directive-syntax.ts'
import { blockDirectiveForm } from '../directive/block.ts'
import { directiveEscape, malformedDirective, readDirectiveLine, spellDirectiveCloser } from '../directive/syntax.ts'
import { flavourClaims } from '../flavour.ts'
import { readLinkDefinitions } from '../commonmark/link-reference-definitions.ts'
export type Block = { position: SourcePosition } & (
@@ -90,10 +91,10 @@ const indentedCodeColumns = 4
const largestOpenerIndentation = 3
const tabStop = 4
export function parseBlocks(markdown: string, claims: Claims): ParsedBlocks {
export function parseBlocks(markdown: string, flavour: Flavour): ParsedBlocks {
const walk: Walk = {
blocks: [],
claims,
claims: flavourClaims[flavour],
definitions: new Map(),
endedLists: new Set(),
leaf: undefined,
+10 -9
View File
@@ -1,6 +1,6 @@
import type { AdfMark, AdfNode } from '../../adf/document.ts'
import type { ClaimedPrefix, DirectiveSpan, NestedSpans } from '../directive-syntax.ts'
import type { Claims } from '../portable/conventions.ts'
import type { ClaimedPrefix, DirectiveSpan, NestedSpans } from '../directive/syntax.ts'
import type { Claims, Flavour } from '../flavour.ts'
import type { EmphasisPairing } from '../commonmark/emphasis-matching.ts'
import type { InlineSource, InlineToken } from '../inline-tokens.ts'
import type { LineContainer } from '../line-container.ts'
@@ -9,19 +9,20 @@ import { centeredImage, externalMedia } from '../external-image.ts'
import { commonMarkLink, linkHref, linkMark } from '../mark-spellings.ts'
import { decodeTextEscapes, trimTrailingSpace } from '../commonmark/grammar.ts'
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { flavourClaims } from '../flavour.ts'
import { highlightDelimiter } from '../portable/conventions.ts'
import { identicalMarks, mergeAdjacentText, nodeAttrs, nodeMarks } from '../../adf/document.ts'
import { inlineNodeModel, takesMark } from '../../adf/inline-nodes.ts'
import { joinsWhenRead, textBreakName, textBreakSpelling } from '../adjacent-text.ts'
import { matchEmphasis } from '../commonmark/emphasis-matching.ts'
import { normalizeLabel, readInlineTarget, readLabel } from '../commonmark/link-syntax.ts'
import { noSpans, readInlineDirective } from '../directive-syntax.ts'
import { openingLinkTakesDirective } from '../emit/inline-line.ts'
import { noSpans, readInlineDirective } from '../directive/syntax.ts'
import { openingLinkTakesDirective } from '../emit/spelling-questions.ts'
import { readCarriedInline } from '../opaque-carry.ts'
import { readDirectiveMark } from './directive-marks.ts'
import { readInlineDirectiveNode } from './directive-nodes.ts'
import { readDirectiveMark } from '../directive/marks.ts'
import { readInlineDirectiveNode } from '../directive/nodes.ts'
import { inlineSource, readInlineToken } from '../inline-tokens.ts'
import { readTextDirective } from '../text-directive.ts'
import { readTextDirective } from '../directive/text.ts'
// Only a `paragraph` block takes `image`, the `mediaSingle` a lone image builds; every other container takes `nodes`, where the image is its linked alt text.
export type InlineContent = { image?: AdfNode; nodes: AdfNode[] }
@@ -89,8 +90,8 @@ const spellableLink = 'link takes the directive form only where CommonMark canno
const textBreak: TextBreak = { kind: 'textBreak' }
// The outermost content: only here does a text break see both its neighbours.
export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer, claims: Claims): Result<InlineContent> {
const scan = freshScan(source, { definitions, path }, { claims, container, inLink: false, spans: noSpans })
export function parseInlineContent(source: string, definitions: LinkDefinitions, path: ConvertErrorPath, container: LineContainer, flavour: Flavour): Result<InlineContent> {
const scan = freshScan(source, { definitions, path }, { claims: flavourClaims[flavour], container, inLink: false, spans: noSpans })
const scanned = scanInline(scan)
if (!scanned.ok) return scanned
const nodes = partText(scanned.value.nodes, scan)
+24 -23
View File
@@ -1,23 +1,24 @@
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
import type { Block, DirectiveBlock } from './blocks.ts'
import type { BlockDirectiveNode } from './directive-nodes.ts'
import type { Claims } from '../portable/conventions.ts'
import type { BlockDirectiveNode } from '../directive/nodes.ts'
import type { ConvertFault } from '../../result.ts'
import type { Flavour } from '../flavour.ts'
import type { LineContainer } from '../line-container.ts'
import type { LinkDefinitions } from './inline-content.ts'
import { carryFenceType, readCarriedBlock } from '../opaque-carry.ts'
import { commonMarkSpelling, type SpellingMemo } from '../emit/adf-to-markdown.ts'
import { documentAttribute, documentName, documentSpelling } from '../block-directive.ts'
import { commonMarkSpelling, type SpellingMemo } from '../emit/spelling-questions.ts'
import { documentAttribute, documentName, documentSpelling } from '../directive/block.ts'
import { emptyKeys, nodeAttrs, nodeContent } from '../../adf/document.ts'
import { failure, faulted, positioned, success, type ConvertErrorPath, type ParseError, type Result, type SourcePosition } from '../../result.ts'
import { flavourClaims, leadingMarker, readAlertMarker, readTaskMarker } from '../portable/conventions.ts'
import { flavourClaims } from '../flavour.ts'
import { languageSlot } from '../code-language.ts'
import { largestNesting } from '../../nesting.ts'
import { leadingMarker, readAlertMarker, readTaskMarker } from '../portable/conventions.ts'
import { mintTaskIds } from '../portable/task-ids.ts'
import { parseBlocks } from './blocks.ts'
import { parseInlineContent } from './inline-content.ts'
import { readBlockDirectiveNode } from './directive-nodes.ts'
import { spellStringAttribute, unsupportedNodeShape } from '../directive-syntax.ts'
import { readBlockDirectiveNode } from '../directive/nodes.ts'
import { spellStringAttribute, unsupportedNodeShape } from '../directive/syntax.ts'
import { spellsEmpty } from '../empty-keys.ts'
import { splitPlace, splitTaskList } from '../portable/task-list.ts'
import { withinEngineLimits } from '../length-bound.ts'
@@ -30,40 +31,40 @@ type TaskParagraph = { kind: 'taskParagraph'; position: SourcePosition; text: st
type ReadBlock = Block | TaskParagraph
// `inExpand` is whether an expand holds the blocks, which makes a folded callout a nestedExpand.
type Reading = { claims: Claims; definitions: LinkDefinitions; inExpand: boolean; memo: SpellingMemo }
type Reading = { definitions: LinkDefinitions; flavour: Flavour; inExpand: boolean; memo: SpellingMemo }
export function commonMarkToAdf(markdown: string): Result<AdfDocument, ParseError> {
return readDocument(markdown, flavourClaims.commonmark)
return readDocument(markdown, 'commonmark')
}
export function losslessMarkdownToAdf(markdown: string): Result<AdfDocument, ParseError> {
return readDocument(markdown, flavourClaims.lossless)
return readDocument(markdown, 'lossless')
}
export function portableMarkdownToAdf(markdown: string): Result<AdfDocument, ParseError> {
return readDocument(markdown, flavourClaims.portable)
return readDocument(markdown, 'portable')
}
function readDocument(markdown: string, claims: Claims): Result<AdfDocument, ParseError> {
function readDocument(markdown: string, flavour: Flavour): Result<AdfDocument, ParseError> {
return positioned(
withinEngineLimits(() => readMarkdown(markdown, claims)),
withinEngineLimits(() => readMarkdown(markdown, flavour)),
{ line: 1, offset: 0 },
)
}
function readMarkdown(markdown: string, claims: Claims): Result<AdfDocument> {
function readMarkdown(markdown: string, flavour: Flavour): Result<AdfDocument> {
if (typeof markdown !== 'string') return failure('not-a-string', `markdown is a string: found ${typeName(markdown)}`, [])
const parsed = parseBlocks(markdown, claims)
const parsed = parseBlocks(markdown, flavour)
const [only] = parsed.blocks
if (parsed.blocks.length === 1 && only?.kind === 'directive' && only.name === documentName) {
const fault = documentFault(only)
return fault === undefined ? success({ type: 'doc', version: 1 }) : positioned(faulted(fault, []), only.position)
}
const reading: Reading = { claims, definitions: parsed.definitions, inExpand: false, memo: new Map() }
const reading: Reading = { definitions: parsed.definitions, flavour, inExpand: false, memo: new Map() }
const content = readBlocks(parsed.blocks, reading, [], 0)
if (!content.ok) return content
const document: AdfDocument = { content: content.value, type: 'doc', version: 1 }
if (claims.taskMarkers) mintTaskIds(document, markdown)
if (flavourClaims[flavour].taskMarkers) mintTaskIds(document, markdown)
return success(document)
}
@@ -84,7 +85,7 @@ function readBlocks(blocks: readonly ReadBlock[], reading: Reading, path: Conver
const fault = documentFault(block) ?? unsupportedNodeShape(`delete the ${documentSpelling} line to give the document content: it stands only as the whole document`)
return positioned(faulted(fault, nodePath), block.position)
}
if (block.kind === 'bulletList' && reading.claims.taskMarkers) {
if (block.kind === 'bulletList' && flavourClaims[reading.flavour].taskMarkers) {
const listed = positioned(bulletBlocks(block.items, reading, nodePath, depth), block.position)
if (!listed.ok) return listed
for (const node of listed.value) content.push(node)
@@ -113,7 +114,7 @@ function documentFault(block: DirectiveBlock): ConvertFault | undefined {
function readBlock(block: ReadBlock, reading: Reading, path: ConvertErrorPath, depth: number): Result<AdfNode> {
switch (block.kind) {
case 'blockquote':
return reading.claims.alerts ? quoteNode(block.blocks, reading, path, depth) : containerNode({ type: 'blockquote' }, block.blocks, reading, path, depth)
return flavourClaims[reading.flavour].alerts ? quoteNode(block.blocks, reading, path, depth) : containerNode({ type: 'blockquote' }, block.blocks, reading, path, depth)
case 'bulletList':
// readBlocks reads a portable task list before it reaches here.
return listNode({ type: 'bulletList' }, block.items, reading, path, depth)
@@ -165,7 +166,7 @@ function quoteNode(blocks: readonly Block[], reading: Reading, path: ConvertErro
const { folded, panelType } = led.marker
const { line, rest } = markerLine(led.text)
if (!folded) return filledNode({ attrs: { panelType }, type: 'panel' }, readBlocks([...paragraphsOf(led.position, line, rest), ...body], reading, path, depth + 1))
const title = parseInlineContent(line, reading.definitions, path, 'paragraph', reading.claims)
const title = parseInlineContent(line, reading.definitions, path, 'paragraph', reading.flavour)
if (!title.ok) return title
const text = titleText(title.value.nodes)
const type = reading.inExpand ? 'nestedExpand' : 'expand'
@@ -369,7 +370,7 @@ function listNode(node: AdfNode, items: readonly Block[][], reading: Reading, pa
}
function codeBlockNode(language: string, text: string, reading: Reading, path: ConvertErrorPath, depth: number): Result<AdfNode> {
const carriedType = reading.claims.carryFence ? carryFenceType(language) : undefined
const carriedType = flavourClaims[reading.flavour].carryFence ? carryFenceType(language) : undefined
if (carriedType !== undefined) {
const carried = readCarriedBlock(carriedType, text, depth)
return carried.fault === undefined ? success(carried.value) : faulted(carried.fault, path)
@@ -380,14 +381,14 @@ function codeBlockNode(language: string, text: string, reading: Reading, path: C
// spec/flavour.md, The CommonMark image: only a `paragraph` block gives a lone image the block it needs.
function paragraphNode(text: string, reading: Reading, path: ConvertErrorPath): Result<AdfNode> {
const content = parseInlineContent(text, reading.definitions, path, 'paragraph', reading.claims)
const content = parseInlineContent(text, reading.definitions, path, 'paragraph', reading.flavour)
if (!content.ok) return content
const image = content.value.image
return success(image === undefined ? withContent({ type: 'paragraph' }, content.value.nodes) : image)
}
function contentNode(node: AdfNode, text: string, reading: Reading, path: ConvertErrorPath, container: LineContainer): Result<AdfNode> {
const content = parseInlineContent(text, reading.definitions, path, container, reading.claims)
const content = parseInlineContent(text, reading.definitions, path, container, reading.flavour)
if (!content.ok) return content
return success(withContent(node, content.value.nodes))
}
@@ -4,7 +4,7 @@ import { blockNodeModel, isTaskItem } from '../../adf/block-nodes.ts'
import { centeredImage, externalMedia } from '../external-image.ts'
import { commonMarkSpelling, largestListMarker, writeMarkdown, type SpellingMemo } from '../emit/adf-to-markdown.ts'
import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts'
import { flavourClaims, taskMarker } from './conventions.ts'
import { flavourClaims } from '../flavour.ts'
import { inlineLeaves, isBlockNodeType, oneLine, portableLink, reduceInline, writableHref } from './inline-reduction.ts'
import { inlineNodeModel } from '../../adf/inline-nodes.ts'
import { languageSlot } from '../code-language.ts'
@@ -12,6 +12,7 @@ import { largestNesting } from '../../nesting.ts'
import { linkDefinition } from '../mark-spellings.ts'
import { longestMarkdown, tooLong, withinEngineLimits } from '../length-bound.ts'
import { splitTaskList } from './task-list.ts'
import { taskMarker } from './conventions.ts'
import { toEditorNormal } from './editor-normal.ts'
// depth: the level the node reduced stands at, counted as the emitter counts it.
-27
View File
@@ -1,32 +1,5 @@
import { isWordCharacter } from '../commonmark/emphasis-matching.ts'
type Flavour = 'commonmark' | 'lossless' | 'portable'
export type WrittenFlavour = Exclude<Flavour, 'commonmark'>
// What a reader reads past CommonMark, and so what its writer escapes (docs/decisions.md §Each call names its flavour).
export type Claims = {
alerts: boolean
carryFence: boolean
directives: boolean
highlights: boolean
// `gfm`: a row may drop its leading `|`, and an alignment colon is read and dropped; `flavour`: every row opens with `|`, the delimiter row `-` runs only.
pipeTables: 'flavour' | 'gfm' | false
strikethrough: boolean
taskMarkers: boolean
}
type WrittenClaims = Claims & { pipeTables: 'flavour' | 'gfm'; strikethrough: true }
// Directives exclude alerts and highlights: an expand directive leaves `inExpand` unset and a directive's slot reads `==`.
type ReadClaims = Claims & ({ directives: false } | { alerts: false; directives: true; highlights: false })
export const flavourClaims: Readonly<{ commonmark: ReadClaims } & Record<WrittenFlavour, ReadClaims & WrittenClaims>> = {
commonmark: { alerts: false, carryFence: false, directives: false, highlights: false, pipeTables: false, strikethrough: false, taskMarkers: false },
lossless: { alerts: false, carryFence: true, directives: true, highlights: false, pipeTables: 'flavour', strikethrough: true, taskMarkers: false },
portable: { alerts: true, carryFence: false, directives: false, highlights: true, pipeTables: 'gfm', strikethrough: true, taskMarkers: true },
}
type AlertMarker = { folded: boolean; length: number; panelType: string }
export const foldedAlertMarker = '[!NOTE]-'
+3
View File
@@ -25,6 +25,9 @@ export type ParseError = ConvertError & { position: SourcePosition }
export type Result<T, E extends ConvertError = ConvertError> = { error: E; ok: false } | { ok: true; value: T }
// A reader with no path to name; the walk attaches the path where it knows it.
export type Read<T> = { fault: ConvertFault; value?: undefined } | { fault?: undefined; value: T }
export function failure<T>(code: ConvertErrorCode, message: string, path: ConvertErrorPath): Result<T> {
return { error: { code, message, path }, ok: false }
}
+4 -21
View File
@@ -12,10 +12,8 @@
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 60 | 0.3.0 | | decision | **Collect the questions `parse/` asks `emit/` into one named module.** | 2 | 1 | 2 | 3 | 4 | code health by 4 | 17.4 |
| 92 | 0.3.0 | | | **Lower the reading cost of the files a chunk reads most before its first commit.** | 2 | 1 | 1 | 2 | 4 | code health by 4 | 17.4 |
| 102 | 0.3.0 | | decision | **Place a block a portable task list refuses at its path in the returned document without replaying `splitTaskList` on stand-in nodes.** | 2 | 1 | 2 | 3 | 4 | code health by 4 | 17.4 |
| 59 | 0.3.0 | | decision | **Group the directive grammar into `src/markdown/directive/`, move `Read<T>` to `result.ts`, and move `Flavour` and `flavourClaims` to `markdown/flavour.ts`.** | 3 | 1 | 2 | 4 | 5 | code health by 5 | 13.1 |
| 62 | 0.3.0 | | decision | **Move the portable flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/portable/`, so `emit/` no longer imports `portable/`.** | 3 | 1 | 2 | 4 | 5 | code health by 5 | 13.1 |
| 123 | 0.3.0 | | defect | **Spell a link title holding a backslash, an entity reference or, in a table cell, a pipe, instead of dropping it.** | 3 | 1 | 2 | 4 | 3 | 7 by 2 | 13.1 |
| 122 | 0.3.0 | | defect | **Keep an image's URL and title when a link's text holds the image, instead of dropping them.** | 3 | 1 | 2 | 4 | 5 | 7 by 4 | 10.5 |
@@ -25,7 +23,7 @@
| 109 | 0.3.0 | | defect | **Spell nested lists and quotes in time linear in the markdown.** | 3 | 1 | 2 | 4 | 5 | 9 by 3 | 7.8 |
| 115 | 0.3.0 | | defect | **Build text holding no `border` mark in `losslessMarkdownToAdf`, and have `adfToLosslessMarkdown` carry text holding one.** | 2 | 1 | 2 | 3 | 3 | 4 by 3 | 4.6 |
| 91 | 0.3.0 | 95 | | **Spell an emphasis once across the code spans, an emphasis or link once across the hard breaks, dates, emoji, inline cards, mentions and statuses between two nodes it marks, and an emphasis across inline media, in both writers.** | 3 | 2 | 2 | 5 | 5 | 6 by 6 | 3.6 |
| 73 | 0.3.0 | 33, 59, 60, 62, 91, 92, 95, 102, 109, 115, 122, 123, 125 | | **Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.** | 1 | 2 | 2 | 3 | 2 | 3 by 8 | 16.9 |
| 73 | 0.3.0 | 33, 62, 91, 92, 95, 102, 109, 115, 122, 123, 125 | | **Announce the package where someone needing an ADF converter already reads: JRACLOUD-77436, the Atlassian developer community and Stack Overflow's ADF-to-markdown questions.** | 1 | 2 | 2 | 3 | 2 | 3 by 8 | 16.9 |
| 75 | 0.4.0 | | | **Make the generated-markdown property write block carries as `adf:` fences, canonical and hostile.** | 1 | 1 | 1 | 1 | 2 | code health by 2 | 17.4 |
| 45 | 0.4.0 | | defect | **Replace `isAdfDocument` with a reader returning `Result<AdfDocument>`.** | 2 | 1 | 3 | 4 | 3 | 1 by 2 | 13.1 |
| 68 | 0.4.0 | | defect | **Carry a non-text inline node holding `content` or `text`, a block holding `text`, and a leaf block holding `content`.** | 2 | 1 | 2 | 3 | 3 | 1 by 2 | 13.1 |
@@ -33,7 +31,7 @@
| 80 | 0.4.0 | | | **Name the node's path and the attribute key when `not-an-adf-document` refuses a node inside the document.** | 2 | 1 | 2 | 3 | 3 | 6 by 4 | 6.0 |
| 81 | 0.4.0 | | | **Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.** | 1 | 1 | 1 | 1 | 2 | 3 by 2 | 4.2 |
| 6 | 0.4.0 | | decision | **Specify the HTML dialect.** | 3 | 2 | 1 | 4 | 6 | | 0.0 |
| 7 | 0.4.0 | 6, 59, 60, 62 | decision | **Ship HTML: `adfToHtml`, `htmlToAdf`, and markdown to and from HTML composed through ADF, each call named by its flavour.** | 4 | 2 | 2 | 6 | 9 | 3 by 6 | 0.3 |
| 7 | 0.4.0 | 6, 62 | decision | **Ship HTML: `adfToHtml`, `htmlToAdf`, and markdown to and from HTML composed through ADF, each call named by its flavour.** | 4 | 2 | 2 | 6 | 9 | 3 by 6 | 0.3 |
| 46 | 0.4.0 | 7 | | **Publish the bundle size in the README, failing the release pipeline when it drifts.** | 2 | 2 | 1 | 3 | 4 | 3 by 3 | 3.2 |
| 51 | 0.4.0 | | | **Match a reference label to its definition under Unicode case folding.** | 2 | 1 | 2 | 3 | 4 | 4 by 2 | 2.1 |
| 77 | 0.4.0 | | decision | **Read the input once into a plain copy, so an accessor property or a throwing Proxy yields `not-an-adf-document` and every call returns a result.** | 3 | 1 | 3 | 5 | 4 | | 0.0 |
@@ -59,12 +57,6 @@
## Details
### 60. Collect the questions `parse/` asks `emit/` into one named module.
`parse/` asks `emit/` through `commonMarkSpelling` and `openingLinkTakesDirective`, each imported
from where it happens to live. One module naming the questions keeps `docs/decisions.md` §The source
parts by ADF and format true as they grow.
### 92. Lower the reading cost of the files a chunk reads most before its first commit.
The last five chunks' median is 106412 new input tokens before their first commit, the five before
@@ -81,22 +73,13 @@ them 104037. Files read most over the last five chunks, counting opens and searc
`taskOfParagraphs` keeps the original child objects. Both paired-ruling seats on 2026-10-11 named it
the reason the project read worse.
### 59. Group the directive grammar into `src/markdown/directive/`, move `Read<T>` to `result.ts`, and move `Flavour` and `flavourClaims` to `markdown/flavour.ts`.
Eight directive files sit across three directories, and the `markdown/` root holds 14 entries. HTML
needs `Read<T>` and `Flavour` out of the portable flavour and `markdown/`; it needs the carry and
the mark spellings too, which item 7 moves where it learns what HTML shares.
Once moved, a reader takes a `Flavour` and looks up its claims, as a writer does, so no reader is
handed a set of claims no flavour holds.
### 62. Move the portable flavour's reading out of `parse/` and its writing out of `emit/` into `markdown/portable/`, so `emit/` no longer imports `portable/`.
Reading: the alert and task-marker reads in `parse/markdown-to-adf.ts` and the `mintTaskIds` call.
Writing: `spellPortableBlock`, `quotedUnder`, `tryTaskList` and `taskBlocks` in
`emit/adf-to-markdown.ts`. And `highlightDelimiter` and `highlightFlanking` move out of
`portable/conventions.ts`, which `emit/` imports them from; item 59 moves `Flavour` and
`flavourClaims`. Every comprehension reader on 2026-10-03 named the portable flavour's spread across
three directories.
`portable/conventions.ts`, which `emit/` imports them from. Every comprehension reader on
2026-10-03 named the portable flavour's spread across three directories.
### 123. Spell a link title holding a backslash, an entity reference or, in a table cell, a pipe, instead of dropping it.