diff --git a/AGENTS.md b/AGENTS.md index 4c38bee..3ac5be1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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 diff --git a/docs/decisions.md b/docs/decisions.md index da392ef..aad9d5f 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -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. diff --git a/src/conformance/adf-schema.test.ts b/src/conformance/adf-schema.test.ts index f6bd0bb..01d1e1b 100644 --- a/src/conformance/adf-schema.test.ts +++ b/src/conformance/adf-schema.test.ts @@ -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' diff --git a/src/conformance/flavour.test.ts b/src/conformance/flavour.test.ts index 07d0b5d..5d8fcae 100644 --- a/src/conformance/flavour.test.ts +++ b/src/conformance/flavour.test.ts @@ -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 } diff --git a/src/conformance/markdown-property.test.ts b/src/conformance/markdown-property.test.ts index 126d4f6..a7ee666 100644 --- a/src/conformance/markdown-property.test.ts +++ b/src/conformance/markdown-property.test.ts @@ -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; hostile?: true; weight: number } diff --git a/src/conformance/module-boundaries.test.ts b/src/conformance/module-boundaries.test.ts new file mode 100644 index 0000000..464b8d3 --- /dev/null +++ b/src/conformance/module-boundaries.test.ts @@ -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'), + [], + ) +}) diff --git a/src/conformance/property-harness.ts b/src/conformance/property-harness.ts index 2203663..d213b16 100644 --- a/src/conformance/property-harness.ts +++ b/src/conformance/property-harness.ts @@ -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' diff --git a/src/markdown/adjacent-text.ts b/src/markdown/adjacent-text.ts index 7bfe097..e763194 100644 --- a/src/markdown/adjacent-text.ts +++ b/src/markdown/adjacent-text.ts @@ -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' diff --git a/src/markdown/code-language.ts b/src/markdown/code-language.ts index 1bd48b8..bef4f93 100644 --- a/src/markdown/code-language.ts +++ b/src/markdown/code-language.ts @@ -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' diff --git a/src/markdown/directive/attributes.ts b/src/markdown/directive/attributes.ts index 8f2e101..7cec646 100644 --- a/src/markdown/directive/attributes.ts +++ b/src/markdown/directive/attributes.ts @@ -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' } diff --git a/src/markdown/directive/block-spelling.ts b/src/markdown/directive/block-spelling.ts index d0dd6e8..85e1cf3 100644 --- a/src/markdown/directive/block-spelling.ts +++ b/src/markdown/directive/block-spelling.ts @@ -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' diff --git a/src/markdown/directive/block.ts b/src/markdown/directive/block.ts index ad5378f..6e37fd6 100644 --- a/src/markdown/directive/block.ts +++ b/src/markdown/directive/block.ts @@ -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({ diff --git a/src/markdown/directive/inline-spelling.ts b/src/markdown/directive/inline-spelling.ts index 69f8e76..6f33cd6 100644 --- a/src/markdown/directive/inline-spelling.ts +++ b/src/markdown/directive/inline-spelling.ts @@ -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' diff --git a/src/markdown/directive/marks.ts b/src/markdown/directive/marks.ts index 59c0f06..2800c71 100644 --- a/src/markdown/directive/marks.ts +++ b/src/markdown/directive/marks.ts @@ -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 | undefined { const spelling = markSpelling(name) diff --git a/src/markdown/directive/nodes.ts b/src/markdown/directive/nodes.ts index 53e2415..b4bbbee 100644 --- a/src/markdown/directive/nodes.ts +++ b/src/markdown/directive/nodes.ts @@ -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 } diff --git a/src/markdown/directive/syntax.test.ts b/src/markdown/directive/syntax.test.ts index dc3f296..ab31c92 100644 --- a/src/markdown/directive/syntax.test.ts +++ b/src/markdown/directive/syntax.test.ts @@ -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?] diff --git a/src/markdown/directive/syntax.ts b/src/markdown/directive/syntax.ts index 61b5f6b..a9d9b6b 100644 --- a/src/markdown/directive/syntax.ts +++ b/src/markdown/directive/syntax.ts @@ -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 -export type Read = { 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 } diff --git a/src/markdown/directive/text.ts b/src/markdown/directive/text.ts index 92691d7..5764fab 100644 --- a/src/markdown/directive/text.ts +++ b/src/markdown/directive/text.ts @@ -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+)$/ diff --git a/src/markdown/emit/adf-to-markdown.ts b/src/markdown/emit/adf-to-markdown.ts index 1a2e09e..5045c53 100644 --- a/src/markdown/emit/adf-to-markdown.ts +++ b/src/markdown/emit/adf-to-markdown.ts @@ -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' diff --git a/src/markdown/emit/inline-line.ts b/src/markdown/emit/inline-line.ts index efb194d..d74d469 100644 --- a/src/markdown/emit/inline-line.ts +++ b/src/markdown/emit/inline-line.ts @@ -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[] } diff --git a/src/markdown/emit/line-escaping.ts b/src/markdown/emit/line-escaping.ts index 0910bbb..cd6adb2 100644 --- a/src/markdown/emit/line-escaping.ts +++ b/src/markdown/emit/line-escaping.ts @@ -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' diff --git a/src/markdown/emit/pipe-table.ts b/src/markdown/emit/pipe-table.ts index 15609f7..f3787e6 100644 --- a/src/markdown/emit/pipe-table.ts +++ b/src/markdown/emit/pipe-table.ts @@ -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' diff --git a/src/markdown/emit/spelling-questions.ts b/src/markdown/emit/spelling-questions.ts new file mode 100644 index 0000000..1c5ec37 --- /dev/null +++ b/src/markdown/emit/spelling-questions.ts @@ -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' diff --git a/src/markdown/empty-keys.ts b/src/markdown/empty-keys.ts index 5aba8e7..900ad1a 100644 --- a/src/markdown/empty-keys.ts +++ b/src/markdown/empty-keys.ts @@ -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; rest: Map } diff --git a/src/markdown/flavour.ts b/src/markdown/flavour.ts new file mode 100644 index 0000000..097e646 --- /dev/null +++ b/src/markdown/flavour.ts @@ -0,0 +1,26 @@ +export type Flavour = 'commonmark' | 'lossless' | 'portable' + +export type WrittenFlavour = Exclude + +// 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> = { + 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 }, +} diff --git a/src/markdown/inline-tokens.test.ts b/src/markdown/inline-tokens.test.ts index ff85656..6ffa348 100644 --- a/src/markdown/inline-tokens.test.ts +++ b/src/markdown/inline-tokens.test.ts @@ -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' diff --git a/src/markdown/inline-tokens.ts b/src/markdown/inline-tokens.ts index 3d37787..284098b 100644 --- a/src/markdown/inline-tokens.ts +++ b/src/markdown/inline-tokens.ts @@ -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' diff --git a/src/markdown/mark-spellings.ts b/src/markdown/mark-spellings.ts index b3c9bc2..d78ea00 100644 --- a/src/markdown/mark-spellings.ts +++ b/src/markdown/mark-spellings.ts @@ -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' diff --git a/src/markdown/opaque-carry.ts b/src/markdown/opaque-carry.ts index 27dc267..21487d6 100644 --- a/src/markdown/opaque-carry.ts +++ b/src/markdown/opaque-carry.ts @@ -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' diff --git a/src/markdown/parse/blocks.test.ts b/src/markdown/parse/blocks.test.ts index a71132c..c358ded 100644 --- a/src/markdown/parse/blocks.test.ts +++ b/src/markdown/parse/blocks.test.ts @@ -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)))') }) diff --git a/src/markdown/parse/blocks.ts b/src/markdown/parse/blocks.ts index e9bf4b9..2db0c36 100644 --- a/src/markdown/parse/blocks.ts +++ b/src/markdown/parse/blocks.ts @@ -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, diff --git a/src/markdown/parse/inline-content.ts b/src/markdown/parse/inline-content.ts index e5de746..788831b 100644 --- a/src/markdown/parse/inline-content.ts +++ b/src/markdown/parse/inline-content.ts @@ -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 { - 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 { + 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) diff --git a/src/markdown/parse/markdown-to-adf.ts b/src/markdown/parse/markdown-to-adf.ts index 5e9189f..f35a9b2 100644 --- a/src/markdown/parse/markdown-to-adf.ts +++ b/src/markdown/parse/markdown-to-adf.ts @@ -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 { - return readDocument(markdown, flavourClaims.commonmark) + return readDocument(markdown, 'commonmark') } export function losslessMarkdownToAdf(markdown: string): Result { - return readDocument(markdown, flavourClaims.lossless) + return readDocument(markdown, 'lossless') } export function portableMarkdownToAdf(markdown: string): Result { - return readDocument(markdown, flavourClaims.portable) + return readDocument(markdown, 'portable') } -function readDocument(markdown: string, claims: Claims): Result { +function readDocument(markdown: string, flavour: Flavour): Result { return positioned( - withinEngineLimits(() => readMarkdown(markdown, claims)), + withinEngineLimits(() => readMarkdown(markdown, flavour)), { line: 1, offset: 0 }, ) } -function readMarkdown(markdown: string, claims: Claims): Result { +function readMarkdown(markdown: string, flavour: Flavour): Result { 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 { 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 { - 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 { - 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 { - 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)) } diff --git a/src/markdown/portable/adf-to-portable-markdown.ts b/src/markdown/portable/adf-to-portable-markdown.ts index e5ed0fb..4d60a14 100644 --- a/src/markdown/portable/adf-to-portable-markdown.ts +++ b/src/markdown/portable/adf-to-portable-markdown.ts @@ -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. diff --git a/src/markdown/portable/conventions.ts b/src/markdown/portable/conventions.ts index 682f369..6cca461 100644 --- a/src/markdown/portable/conventions.ts +++ b/src/markdown/portable/conventions.ts @@ -1,32 +1,5 @@ import { isWordCharacter } from '../commonmark/emphasis-matching.ts' -type Flavour = 'commonmark' | 'lossless' | 'portable' - -export type WrittenFlavour = Exclude - -// 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> = { - 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]-' diff --git a/src/result.ts b/src/result.ts index cedcfd1..457671e 100644 --- a/src/result.ts +++ b/src/result.ts @@ -25,6 +25,9 @@ export type ParseError = ConvertError & { position: SourcePosition } export type Result = { 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 = { fault: ConvertFault; value?: undefined } | { fault?: undefined; value: T } + export function failure(code: ConvertErrorCode, message: string, path: ConvertErrorPath): Result { return { error: { code, message, path }, ok: false } } diff --git a/todo.md b/todo.md index 73bfc5e..9cca82d 100644 --- a/todo.md +++ b/todo.md @@ -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` 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`.** | 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` 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` 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.