From ed099a093dd86ce6c8f26bb1f846c4d6083ceddf Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 18 Sep 2026 23:03:13 +0200 Subject: [PATCH 1/4] =?UTF-8?q?AGENTS.md=20=C2=A711:=20a=20sort=20keys=20o?= =?UTF-8?q?n=20the=20name=20a=20line=20introduces,=20so=20an=20import=20so?= =?UTF-8?q?rts=20on=20its=20first=20binding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 4 +++- browser-tests/run.js | 2 +- src/adf-property.test.ts | 2 +- src/adf-schema.test.ts | 6 +++--- src/commonmark-spec.test.ts | 4 ++-- src/corpus.test.ts | 4 ++-- src/flavour.test.ts | 4 ++-- src/markdown-property.test.ts | 2 +- src/markdown/commonmark/entity-references.test.ts | 4 ++-- src/markdown/emit/image.ts | 2 +- src/markdown/emit/line-escaping.ts | 4 ++-- src/markdown/emit/pipe-table.ts | 2 +- src/markdown/opaque-carry.ts | 2 +- src/markdown/parse/blocks.ts | 2 +- src/markdown/parse/inline-content.ts | 2 +- src/property-harness.ts | 2 +- src/result.test.ts | 4 ++-- 17 files changed, 27 insertions(+), 25 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e005118..93bb31f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -266,7 +266,9 @@ someone spells it or pins it. ## 11. Code rules - Two-space indent, strict TypeScript, English everywhere. Alphabetical order wherever order - carries no meaning. + carries no meaning, keyed on the name a line introduces rather than where it came from: an + import sorts on its first binding, type imports ahead of value imports, so moving or renaming a + module reorders nothing (the maintainer, 2026-09-18). - Failures are values: everything returns `Result` — `{ ok: true; value } | { ok: false; error: ConvertError }` — nothing throws. `try/catch` only wrapped tightly around a call that genuinely throws, converted to a result on diff --git a/browser-tests/run.js b/browser-tests/run.js index 3967467..0369402 100644 --- a/browser-tests/run.js +++ b/browser-tests/run.js @@ -1,7 +1,7 @@ import assert from 'node:assert/strict' -import { readFileSync, readdirSync } from 'node:fs' import { createServer } from 'node:http' import { extname, join } from 'node:path' +import { readFileSync, readdirSync } from 'node:fs' import { toEditorNormal } from '../dist/adf/editor-normal.js' const contentTypes = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript' } diff --git a/src/adf-property.test.ts b/src/adf-property.test.ts index 41e4410..f52789e 100644 --- a/src/adf-property.test.ts +++ b/src/adf-property.test.ts @@ -1,5 +1,5 @@ -import fc from 'fast-check' import assert from 'node:assert/strict' +import fc from 'fast-check' import test from 'node:test' import { adfDocument, propertyRuns, propertyTimeout } from './property-harness.ts' diff --git a/src/adf-schema.test.ts b/src/adf-schema.test.ts index 06ad808..377b007 100644 --- a/src/adf-schema.test.ts +++ b/src/adf-schema.test.ts @@ -1,15 +1,15 @@ import assert from 'node:assert/strict' import { createHash } from 'node:crypto' -import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' -import test from 'node:test' import { fileURLToPath } from 'node:url' +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-arguments.ts' import { blockDirectives } from './adf/block-directives.ts' import { inlineDirectives } from './adf/inline-directives.ts' import { markAttributes } from './adf/mark-attributes.ts' -import { blockArgument } from './markdown/block-directive-arguments.ts' type Held = Map> type Properties = Map diff --git a/src/commonmark-spec.test.ts b/src/commonmark-spec.test.ts index 3bfc714..6026c62 100644 --- a/src/commonmark-spec.test.ts +++ b/src/commonmark-spec.test.ts @@ -1,9 +1,9 @@ import assert from 'node:assert/strict' import { createHash } from 'node:crypto' -import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' -import test from 'node:test' import { fileURLToPath } from 'node:url' +import { readFileSync } from 'node:fs' +import test from 'node:test' import type { AdfDocument, AdfNode } from './adf/document.ts' import { adfToMarkdown } from './markdown/emit/adf-to-markdown.ts' diff --git a/src/corpus.test.ts b/src/corpus.test.ts index e7577f2..6f0d122 100644 --- a/src/corpus.test.ts +++ b/src/corpus.test.ts @@ -1,8 +1,8 @@ import assert from 'node:assert/strict' -import { readFileSync, readdirSync } from 'node:fs' import { basename, dirname, join, sep } from 'node:path' -import test from 'node:test' import { fileURLToPath } from 'node:url' +import { readFileSync, readdirSync } from 'node:fs' +import test from 'node:test' import { adfToMarkdown } from './markdown/emit/adf-to-markdown.ts' import { isAdfDocument } from './adf/document.ts' diff --git a/src/flavour.test.ts b/src/flavour.test.ts index 9017051..bad497a 100644 --- a/src/flavour.test.ts +++ b/src/flavour.test.ts @@ -1,8 +1,8 @@ import assert from 'node:assert/strict' -import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' -import test from 'node:test' import { fileURLToPath } from 'node:url' +import { readFileSync } from 'node:fs' +import test from 'node:test' import type { AttributeKind, AttributeVocabulary } from './adf/attribute-vocabulary.ts' import { blockDirectives } from './adf/block-directives.ts' diff --git a/src/markdown-property.test.ts b/src/markdown-property.test.ts index 44af8cf..1cc7933 100644 --- a/src/markdown-property.test.ts +++ b/src/markdown-property.test.ts @@ -1,5 +1,5 @@ -import fc from 'fast-check' import assert from 'node:assert/strict' +import fc from 'fast-check' import test from 'node:test' import type { AdfDocument } from './adf/document.ts' diff --git a/src/markdown/commonmark/entity-references.test.ts b/src/markdown/commonmark/entity-references.test.ts index 6767876..fc0c65f 100644 --- a/src/markdown/commonmark/entity-references.test.ts +++ b/src/markdown/commonmark/entity-references.test.ts @@ -1,8 +1,8 @@ import assert from 'node:assert/strict' -import { readFileSync } from 'node:fs' import { dirname, join } from 'node:path' -import test from 'node:test' import { fileURLToPath } from 'node:url' +import { readFileSync } from 'node:fs' +import test from 'node:test' import { readEntityReference } from './entity-references.ts' diff --git a/src/markdown/emit/image.ts b/src/markdown/emit/image.ts index 3b318d9..be6828e 100644 --- a/src/markdown/emit/image.ts +++ b/src/markdown/emit/image.ts @@ -1,6 +1,6 @@ import type { AdfNode } from '../../adf/document.ts' -import { carriesOnly, nodeAttrs, nodeContent } from '../../adf/document.ts' import type { ConvertErrorPath } from '../../result.ts' +import { carriesOnly, nodeAttrs, nodeContent } from '../../adf/document.ts' import { serializeCanonicalJson } from '../../canonical-json.ts' import { tryImageLine } from './inline-line.ts' diff --git a/src/markdown/emit/line-escaping.ts b/src/markdown/emit/line-escaping.ts index 6011ccf..379f232 100644 --- a/src/markdown/emit/line-escaping.ts +++ b/src/markdown/emit/line-escaping.ts @@ -1,7 +1,7 @@ -import { backtickRun, closingBacktickRun } from '../commonmark/backtick-runs.ts' -import { delimiterFlags, isWordCharacter, matchEmphasis, runLength } from '../commonmark/emphasis-matching.ts' import { backslashEscape, escapesLineClaim, inlineHtmlConstruct, opensBracketedAutolink, opensEmailAutolink, type LinePosition } from '../commonmark/grammar.ts' +import { backtickRun, closingBacktickRun } from '../commonmark/backtick-runs.ts' import { claimsDirectivePrefix } from '../directive-syntax.ts' +import { delimiterFlags, isWordCharacter, matchEmphasis, runLength } from '../commonmark/emphasis-matching.ts' import { isBareDelimiterRow } from '../pipe-table-syntax.ts' import { opensLinkDefinition } from '../commonmark/link-reference-definitions.ts' import { readEntityReference } from '../commonmark/entity-references.ts' diff --git a/src/markdown/emit/pipe-table.ts b/src/markdown/emit/pipe-table.ts index c57e5fc..b3234fd 100644 --- a/src/markdown/emit/pipe-table.ts +++ b/src/markdown/emit/pipe-table.ts @@ -1,8 +1,8 @@ import type { AdfNode } from '../../adf/document.ts' +import type { ConvertErrorPath } from '../../result.ts' import { carriesOnly, nodeContent } from '../../adf/document.ts' import { spellPipeDelimiter, spellPipeRow } from '../pipe-table-syntax.ts' import { tryPipeCell } from './inline-line.ts' -import type { ConvertErrorPath } from '../../result.ts' export function tryPipeTable(node: AdfNode, path: ConvertErrorPath): string | undefined { const rows = pipeRows(node) diff --git a/src/markdown/opaque-carry.ts b/src/markdown/opaque-carry.ts index c79b4a9..0e03467 100644 --- a/src/markdown/opaque-carry.ts +++ b/src/markdown/opaque-carry.ts @@ -2,9 +2,9 @@ import type { AdfNode } from '../adf/document.ts' import type { DirectiveSpan, Read } from './directive-syntax.ts' import type { JsonSpelling } from '../canonical-json.ts' import { failure, success, type ConvertErrorPath, type Result } from '../result.ts' +import { fencedCodeBlock } from './commonmark/backtick-runs.ts' import { isAdfNode } from '../adf/document.ts' import { isJsonValue, nestingDepth, overNested } from '../json-value.ts' -import { fencedCodeBlock } from './commonmark/backtick-runs.ts' import { largestNesting } from '../nesting.ts' import { malformedDirective, readSoleStringAttribute, spellAttributes, spellInlineLeafDirective, spellStringAttribute, unsupportedNodeShape } from './directive-syntax.ts' import { serializeCanonicalJson } from '../canonical-json.ts' diff --git a/src/markdown/parse/blocks.ts b/src/markdown/parse/blocks.ts index a6a672e..4ff6c24 100644 --- a/src/markdown/parse/blocks.ts +++ b/src/markdown/parse/blocks.ts @@ -18,9 +18,9 @@ import { thematicBreakTail, type ThematicBreakTail, } from '../commonmark/grammar.ts' +import { barePipeCells, isDelimiterRow, isPipeAlignment, isPipeDelimiter, malformedPipeTable, pipeCells } from '../pipe-table-syntax.ts' import { blockDirectiveForm } from '../block-directive-forms.ts' import { directiveEscape, malformedDirective, readDirectiveLine, spellDirectiveCloser } from '../directive-syntax.ts' -import { barePipeCells, isDelimiterRow, isPipeAlignment, isPipeDelimiter, malformedPipeTable, pipeCells } from '../pipe-table-syntax.ts' import { readLinkDefinitions } from '../commonmark/link-reference-definitions.ts' export type Block = { position: SourcePosition } & ( diff --git a/src/markdown/parse/inline-content.ts b/src/markdown/parse/inline-content.ts index 8c7b281..f140edd 100644 --- a/src/markdown/parse/inline-content.ts +++ b/src/markdown/parse/inline-content.ts @@ -10,12 +10,12 @@ import { delimiterFlags, matchEmphasis, runLength } from '../commonmark/emphasis import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts' import { inlineDirective } from '../../adf/inline-directives.ts' import { mergeAdjacentText } from '../../adf/editor-normal.ts' +import { noSpans, readInlineDirective } from '../directive-syntax.ts' import { nodeAttrs, nodeMarks } from '../../adf/document.ts' import { normalizeLabel, readInlineTarget, readLabel } from '../commonmark/link-syntax.ts' import { openingLinkTakesDirective } from '../emit/inline-line.ts' import { readCarriedInline } from '../opaque-carry.ts' import { readDirectiveMark } from './directive-marks.ts' -import { noSpans, readInlineDirective } from '../directive-syntax.ts' import { readInlineDirectiveNode } from './directive-nodes.ts' import { readTextDirective } from '../text-directive.ts' diff --git a/src/property-harness.ts b/src/property-harness.ts index c3c81c0..5054700 100644 --- a/src/property-harness.ts +++ b/src/property-harness.ts @@ -1,6 +1,6 @@ -import fc from 'fast-check' import assert from 'node:assert/strict' import { env } from 'node:process' +import fc from 'fast-check' import type { AdfAttributes, AdfDocument, AdfMark, AdfNode } from './adf/document.ts' import type { Arbitrary } from 'fast-check' diff --git a/src/result.test.ts b/src/result.test.ts index f30ae80..d672ad0 100644 --- a/src/result.test.ts +++ b/src/result.test.ts @@ -1,8 +1,8 @@ import assert from 'node:assert/strict' -import { readFileSync, readdirSync } from 'node:fs' import { dirname, join } from 'node:path' -import test from 'node:test' import { fileURLToPath } from 'node:url' +import { readFileSync, readdirSync } from 'node:fs' +import test from 'node:test' const sourceRoot = dirname(fileURLToPath(import.meta.url)) const union = /export type ConvertErrorCode =\n((?:\s+\| '[a-z-]+'\n)+)/ -- 2.52.0 From 451a617b13abc4d8c3c17fa0e3e1d4d0080483b0 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 18 Sep 2026 23:08:34 +0200 Subject: [PATCH 2/4] 15 - refuse the directive link spelling no href, so the mark has one spelling --- corpus/errors/directive-link-hrefless.error | 1 + corpus/errors/directive-link-hrefless.md | 1 + spec/flavour.md | 3 ++- src/markdown/emit/inline-line.ts | 6 +++--- src/markdown/mark-spellings.ts | 5 +++++ src/markdown/parse/inline-content.ts | 11 ++++++----- src/markdown/parse/markdown-to-adf.test.ts | 11 +++++++++-- 7 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 corpus/errors/directive-link-hrefless.error create mode 100644 corpus/errors/directive-link-hrefless.md diff --git a/corpus/errors/directive-link-hrefless.error b/corpus/errors/directive-link-hrefless.error new file mode 100644 index 0000000..a811539 --- /dev/null +++ b/corpus/errors/directive-link-hrefless.error @@ -0,0 +1 @@ +unsupported-node-shape diff --git a/corpus/errors/directive-link-hrefless.md b/corpus/errors/directive-link-hrefless.md new file mode 100644 index 0000000..df48aff --- /dev/null +++ b/corpus/errors/directive-link-hrefless.md @@ -0,0 +1 @@ +See !adf:link[the docs]{title="Setup guide"} here. diff --git a/spec/flavour.md b/spec/flavour.md index 8677892..e17ebb8 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -473,7 +473,8 @@ the inline directive `!adf:link[text]{attrs}` only where CommonMark does not: an `href` and `title`, an `href` or `title` no canonical escape spells (a control character, a backslash, an entity reference, an angle bracket beside a space or opening a bare destination, a newline in the title), or a link opening a paragraph whose markdown spelling would read as a link -reference definition. A directive link CommonMark could spell is a named error. +reference definition. Every such spelling carries an `href`: a directive link CommonMark could +spell is a named error, and so is one spelling no `href`, the mark without one riding the carry. - `border` — Attributes: `color` (string, `#rrggbb` or `#rrggbbaa`), `size` (number, 1–3). - `code`, `em`, `strike`, `strong` — Attributes: none. diff --git a/src/markdown/emit/inline-line.ts b/src/markdown/emit/inline-line.ts index 19c4484..320d4fd 100644 --- a/src/markdown/emit/inline-line.ts +++ b/src/markdown/emit/inline-line.ts @@ -3,7 +3,7 @@ import type { InlineDirective } from '../../adf/inline-directives.ts' import { assembleInlineLine, isSyntax, type InlineEscaping, type InlineSegment, type LineContainer, type NodeRange } from './line-escaping.ts' import { carriedInline } from '../opaque-carry.ts' import { claimsLine, holdsNullCharacter, trimTrailingSpace } from '../commonmark/grammar.ts' -import { commonMarkLink, markSpelling, spellMarkAttributes } from '../mark-spellings.ts' +import { commonMarkLink, linkHref, markSpelling, spellMarkAttributes } from '../mark-spellings.ts' import { escapeUnbalanced, spellDestination } from '../commonmark/link-syntax.ts' import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts' import { inlineDirective } from '../../adf/inline-directives.ts' @@ -300,8 +300,8 @@ function needsPadding(text: string): boolean { // `undefined` where the link takes the directive form the caller spells. function emitLink(nodes: readonly AdfNode[], mark: AdfMark, depth: number, range: NodeRange, context: InlineContext): Result | undefined { - const href = nodeAttrs(mark)['href'] - if (typeof href !== 'string') return success({ carry: range }) + const href = linkHref(nodeAttrs(mark)) + if (href === undefined) return success({ carry: range }) const opening = depth === 0 && range.first === 0 && context.openingLinkAsDirective const commonMark = opening ? undefined : commonMarkLink(nodeAttrs(mark), href, nodes, depth + 1, context.bracketed) if (commonMark === undefined) return undefined diff --git a/src/markdown/mark-spellings.ts b/src/markdown/mark-spellings.ts index a19a75f..a885262 100644 --- a/src/markdown/mark-spellings.ts +++ b/src/markdown/mark-spellings.ts @@ -35,6 +35,11 @@ export function markSpelling(type: string): MarkSpelling | undefined { return { attributes, kind: spelling.kind } } +export function linkHref(attrs: AdfAttributes): string | undefined { + const href = attrs['href'] + return typeof href === 'string' ? href : undefined +} + // spec/flavour.md, Marks. `marksInside` counts the marks the link's nodes carry within it: the emitter's depth + 1, the parser's 0. export function commonMarkLink(attrs: AdfAttributes, href: string, nodes: readonly AdfNode[], marksInside: number, bracketed: boolean): CommonMarkLink | undefined { if (Object.keys(attrs).some((key) => key !== 'href' && key !== 'title')) return undefined diff --git a/src/markdown/parse/inline-content.ts b/src/markdown/parse/inline-content.ts index f140edd..19beea7 100644 --- a/src/markdown/parse/inline-content.ts +++ b/src/markdown/parse/inline-content.ts @@ -5,7 +5,7 @@ import type { LineContainer } from '../emit/line-escaping.ts' import type { LinkDefinition } from '../commonmark/link-syntax.ts' import { backslashEscape, decodeTextEscapes, inlineHtmlConstruct, readBracketedAutolink, readEmailAutolink, trimTrailingSpace } from '../commonmark/grammar.ts' import { backtickRun, closingBacktickRun } from '../commonmark/backtick-runs.ts' -import { commonMarkLink } from '../mark-spellings.ts' +import { commonMarkLink, linkHref } from '../mark-spellings.ts' import { delimiterFlags, matchEmphasis, runLength } from '../commonmark/emphasis-matching.ts' import { failure, faulted, success, type ConvertErrorPath, type Result } from '../../result.ts' import { inlineDirective } from '../../adf/inline-directives.ts' @@ -51,6 +51,7 @@ type Scan = { type SlotContent = { carry: boolean; nodes: AdfNode[] } const carriedInMark = 'no mark spelling wraps an opaque carry: the carried node restores exactly, marks included' +const hreflessLink = 'the link mark spells its href: this one spells none' const imageAlone = 'an image fits only as a paragraph of its own: this one sits inside other content' const spellableLink = 'link takes the directive form only where CommonMark cannot spell it: this one it can, as [text](url "title") or ' @@ -208,15 +209,15 @@ function directiveMarkPiece(scan: Scan, name: string, mark: AdfMark, slot: SlotC return failure('unsupported-node-shape', `the ${name} mark wraps the [content] it marks: this one wraps none`, scan.path) } if (slot.carry) return failure('unsupported-node-shape', carriedInMark, scan.path) - const refused = mark.type === 'link' ? refuseSpellableLink(scan, mark, slot.nodes, index) : undefined + const refused = mark.type === 'link' ? refuseLinkDirective(scan, mark, slot.nodes, index) : undefined if (refused !== undefined) return refused return success({ kind: 'nodes', nodes: applyMark(slot.nodes, mark) }) } // spec/flavour.md, Marks. A link opening a paragraph may still need the directive form for the line it opens, which `assemble` asks the emitter. -function refuseSpellableLink(scan: Scan, mark: AdfMark, nodes: readonly AdfNode[], index: number): Result | undefined { - const href = nodeAttrs(mark)['href'] - if (typeof href !== 'string') return undefined +function refuseLinkDirective(scan: Scan, mark: AdfMark, nodes: readonly AdfNode[], index: number): Result | undefined { + const href = linkHref(nodeAttrs(mark)) + if (href === undefined) return failure('unsupported-node-shape', hreflessLink, scan.path) if (commonMarkLink(nodeAttrs(mark), href, nodes, 0, scan.container === undefined) === undefined) return undefined if (index !== 0 || scan.container !== 'paragraph') return failure('unsupported-node-shape', spellableLink, scan.path) scan.openingSpellableLink = true diff --git a/src/markdown/parse/markdown-to-adf.test.ts b/src/markdown/parse/markdown-to-adf.test.ts index d0f3f87..5410da5 100644 --- a/src/markdown/parse/markdown-to-adf.test.ts +++ b/src/markdown/parse/markdown-to-adf.test.ts @@ -984,8 +984,6 @@ test('refuses the directive link CommonMark could spell, and reads the one it co assert.equal(content(markdownToAdf('| !adf:link[`]: a`]{href="/u"} |\n| --- |\n')), refused) assert.equal(content(markdownToAdf('!adf:underline[!adf:link[a]{href="/u"}]\n')), refused) assert.deepEqual(path(markdownToAdf('Part.\n\nSee !adf:link[a]{href="/u"}.\n')), ['content', 1]) - const titled: AdfNode = { marks: [{ attrs: { title: 't' }, type: 'link' }], text: 'a', type: 'text' } - assert.deepEqual(content(markdownToAdf('!adf:link[a]{title=t}\n')), [{ content: [titled], type: 'paragraph' }]) const opening: AdfNode = { marks: [{ attrs: { href: '/u' }, type: 'link' }, { type: 'code' }], text: ']: a', type: 'text' } assert.deepEqual(content(markdownToAdf('!adf:link[`]: a`]{href="/u"}\n')), [{ content: [opening], type: 'paragraph' }]) assert.deepEqual(content(markdownToAdf('!adf:heading {level=1 localId=h}\n!adf:link[`]: a`]{href="/u"}\n!adf:/heading\n')), [ @@ -993,6 +991,15 @@ test('refuses the directive link CommonMark could spell, and reads the one it co ]) }) +test('names the href the directive link spells no value for', () => { + const named = 'unsupported-node-shape: the link mark spells its href: this one spells none' + assert.equal(content(markdownToAdf('!adf:link[a]\n')), named) + assert.equal(content(markdownToAdf('!adf:link[a]{title=t}\n')), named) + assert.equal(content(markdownToAdf('See !adf:link[a]{id=01a032c3-7a90-70c9-88f6-c60f710eda07}.\n')), named) + assert.equal(content(markdownToAdf('!adf:underline[!adf:link[a]{title=t}]\n')), named) + assert.deepEqual(path(markdownToAdf('Part.\n\nSee !adf:link[a]{title=t}.\n')), ['content', 1]) +}) + test('names the directive mark left without the content it wraps', () => { const named = 'unsupported-node-shape: the underline mark wraps the [content] it marks: this one wraps none' assert.equal(content(markdownToAdf('!adf:underline[]\n')), named) -- 2.52.0 From 88e5213326b09327bc85c9ad5be808635531c679 Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 18 Sep 2026 23:08:57 +0200 Subject: [PATCH 3/4] Tick 15, moving its text to todo-history.md and pointing the next session at a fetch first --- todo-history.md | 5 +++++ todo.md | 21 +++++++-------------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/todo-history.md b/todo-history.md index eeb6ca5..76c6c68 100644 --- a/todo-history.md +++ b/todo-history.md @@ -809,6 +809,11 @@ The done `todo.md` items in full, as they were written. `todo.md` keeps a one-li and `link-syntax.ts` — leaving the flavour's own constructs at the root, the split `spec/flavour.md` draws between the subset and the flavour (the systems-architect and the maintainer, 2026-09-16). +- [x] **15 — The href-less directive link (`0.2.0`).** Refuse `!adf:link[text]` spelling no `href` + with `unsupported-node-shape` naming the attribute, so the mark has one spelling: today it + parses to a mark the emitter writes back as a carry, while the schema requires `href` and + every other directive mark spells without attributes in both directions alike (the + stability-reviewer, 2026-09-16; the maintainer, 2026-09-17). ## 5 — Ship `0.1.0` diff --git a/todo.md b/todo.md index 7e2db4a..bd16e9b 100644 --- a/todo.md +++ b/todo.md @@ -7,16 +7,13 @@ milestone. A done item shrinks to its title here; its full text moves to `todo-h Start a session with: `Read AGENTS.md and todo.md, then do what todo.md's "Next session" says.` -1. The first unchecked item in shipping order, per AGENTS.md §15 — or, where that item has no +1. `git fetch origin` first and read this file at `origin/main`, then branch off it, not the + worktree left behind: a checkout behind the remote reads a merged item as unchecked, which sent + this session at 14 after it had shipped. +2. The first unchecked item in shipping order, per AGENTS.md §15 — or, where that item has no release, the planning chunk §15 describes. -2. In flight: nothing. -3. `git fetch origin` and branch off `origin/main`, not the worktree left behind. -4. Ask before touching code: §11 sorts alphabetically "wherever order carries no meaning" without - naming the key an import line sorts on. The code sorts by first binding, type imports ahead of - value imports, so a module rename reorders nothing — 14 leaned on that, and four files have - drifted from it. `src/html/` (6, 7) writes the next large body of imports. Candidate §11 rule: - an import line sorts on its first binding, type imports ahead of value imports. -5. Before stopping, rewrite this section: the in-flight line, and the prompt itself wherever the +3. In flight: nothing. +4. Before stopping, rewrite this section: the in-flight line, and the prompt itself wherever the session found it wrong or short. ## Milestones @@ -213,11 +210,7 @@ bundle size and the tagline. - [x] **13a — `rule` and `layoutSection`.** - [x] **13b — The directive link.** - [x] **14 — The CommonMark subset's directory (`0.2.0`).** -- [ ] **15 — The href-less directive link (`0.2.0`).** Refuse `!adf:link[text]` spelling no `href` - with `unsupported-node-shape` naming the attribute, so the mark has one spelling: today it - parses to a mark the emitter writes back as a carry, while the schema requires `href` and - every other directive mark spells without attributes in both directions alike (the - stability-reviewer, 2026-09-16; the maintainer, 2026-09-17). +- [x] **15 — The href-less directive link (`0.2.0`).** - [ ] **16 — The link wrapping a link (`0.2.0`).** Read `[](/v)` and `[!adf:link[a]{href="/u"}](/v)` as `[[a](/u)](/v)` reads — the inner link wins and the outer brackets stay literal text, CommonMark's rule that no link holds another — rather than -- 2.52.0 From b695c4c88c3a50cf38ccaf06b2117bc8928d864b Mon Sep 17 00:00:00 2001 From: Lilleman auf Larv Date: Fri, 18 Sep 2026 23:14:37 +0200 Subject: [PATCH 4/4] Review nits: drop the spec's restated carry rule, the test's duplicate paths and todo.md's incident note --- README.md | 2 +- spec/flavour.md | 2 +- src/markdown/parse/markdown-to-adf.test.ts | 2 -- todo.md | 3 +-- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index bcb8170..672bb20 100644 --- a/README.md +++ b/README.md @@ -102,7 +102,7 @@ emit refuses: | `unspellable-line-start` | a paragraph line begins with a code span whose backticks would read back as a code fence | put any text before the code span | | `unspellable-whitespace` | an `emoji`, `mention` or `status` holds a newline in the text its inline directive spells in the content slot | replace it with a space — an inline directive never spans lines | | `unsupported-nesting-depth` | blocks, marks, an attribute's JSON or a carried node's JSON nest past 500 levels | keep the ADF and pass the document over, or show it read-only; flatten the input where you are the one who wrote it | -| `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take — or markdown writes as a directive a node or mark the flavour spells as CommonMark | write the shape the message names; `spec/flavour.md` lists every type's attributes and body | +| `unsupported-node-shape` | a node carries an attribute, value, argument or body its type does not take, or lacks one it needs — or markdown writes as a directive a node or mark the flavour spells as CommonMark | write the shape the message names; `spec/flavour.md` lists every type's attributes and body | ## The guarantees diff --git a/spec/flavour.md b/spec/flavour.md index e17ebb8..8ae3b80 100644 --- a/spec/flavour.md +++ b/spec/flavour.md @@ -474,7 +474,7 @@ the inline directive `!adf:link[text]{attrs}` only where CommonMark does not: an backslash, an entity reference, an angle bracket beside a space or opening a bare destination, a newline in the title), or a link opening a paragraph whose markdown spelling would read as a link reference definition. Every such spelling carries an `href`: a directive link CommonMark could -spell is a named error, and so is one spelling no `href`, the mark without one riding the carry. +spell is a named error, and so is one spelling none. - `border` — Attributes: `color` (string, `#rrggbb` or `#rrggbbaa`), `size` (number, 1–3). - `code`, `em`, `strike`, `strong` — Attributes: none. diff --git a/src/markdown/parse/markdown-to-adf.test.ts b/src/markdown/parse/markdown-to-adf.test.ts index 5410da5..ec6bf9f 100644 --- a/src/markdown/parse/markdown-to-adf.test.ts +++ b/src/markdown/parse/markdown-to-adf.test.ts @@ -996,8 +996,6 @@ test('names the href the directive link spells no value for', () => { assert.equal(content(markdownToAdf('!adf:link[a]\n')), named) assert.equal(content(markdownToAdf('!adf:link[a]{title=t}\n')), named) assert.equal(content(markdownToAdf('See !adf:link[a]{id=01a032c3-7a90-70c9-88f6-c60f710eda07}.\n')), named) - assert.equal(content(markdownToAdf('!adf:underline[!adf:link[a]{title=t}]\n')), named) - assert.deepEqual(path(markdownToAdf('Part.\n\nSee !adf:link[a]{title=t}.\n')), ['content', 1]) }) test('names the directive mark left without the content it wraps', () => { diff --git a/todo.md b/todo.md index bd16e9b..cd480da 100644 --- a/todo.md +++ b/todo.md @@ -8,8 +8,7 @@ milestone. A done item shrinks to its title here; its full text moves to `todo-h Start a session with: `Read AGENTS.md and todo.md, then do what todo.md's "Next session" says.` 1. `git fetch origin` first and read this file at `origin/main`, then branch off it, not the - worktree left behind: a checkout behind the remote reads a merged item as unchecked, which sent - this session at 14 after it had shipped. + worktree left behind: a checkout behind the remote reads a merged item as unchecked. 2. The first unchecked item in shipping order, per AGENTS.md §15 — or, where that item has no release, the planning chunk §15 describes. 3. In flight: nothing. -- 2.52.0