diff --git a/docs/decisions.md b/docs/decisions.md index 41843e4..8a834d5 100644 --- a/docs/decisions.md +++ b/docs/decisions.md @@ -135,7 +135,8 @@ in the editor's UUID v4 shape, hashed (cyrb128) from the whole markdown and the document order, skipping any id the document holds. A site rejecting a missing `localId` takes the document, the same markdown reads to the same ids every run, and different markdown to ids no other document shares. The same markdown pasted twice into one document repeats its ids: determinism -wins over that case. +wins over that case. A node the carry restores stays deep-equal (§Unknown nodes ride the carry): its +ids are only skipped. ## A callout title keeps its link targets diff --git a/src/markdown/parse/markdown-to-adf.ts b/src/markdown/parse/markdown-to-adf.ts index 5ca6f6a..30fee02 100644 --- a/src/markdown/parse/markdown-to-adf.ts +++ b/src/markdown/parse/markdown-to-adf.ts @@ -22,7 +22,7 @@ import { unsupportedNodeShape } from '../directive-syntax.ts' type Paragraph = Extract // `inExpand` is whether an expand holds the blocks, which makes a folded callout a nestedExpand. -type Reading = { definitions: LinkDefinitions; flavour: Flavour; inExpand: boolean; memo: SpellingMemo } +type Reading = { carried: Set; definitions: LinkDefinitions; flavour: Flavour; inExpand: boolean; memo: SpellingMemo } const documentStart: SourcePosition = { line: 1, offset: 0 } const imageAfterMarker = 'an image fits only as a paragraph of its own: this one continues the paragraph a marker opens, which a blank line before it ends' @@ -38,11 +38,11 @@ export function plainMarkdownToAdf(markdown: string): Result { const parsed = parseBlocks(markdown) - const reading: Reading = { definitions: parsed.definitions, flavour, inExpand: false, memo: new Map() } + const reading: Reading = { carried: new Set(), definitions: parsed.definitions, flavour, inExpand: false, memo: new Map() } const content = positioned(readBlocks(parsed.blocks, reading, [], 0), documentStart) if (!content.ok) return content const document: AdfDocument = content.value.length === 0 ? { type: 'doc', version: 1 } : { content: content.value, type: 'doc', version: 1 } - if (flavour === 'plain') mintTaskIds(document, markdown) + if (flavour === 'plain') mintTaskIds(document, markdown, reading.carried) return success(document) } @@ -83,7 +83,7 @@ function readBlock(block: Block, reading: Reading, path: ConvertErrorPath, depth case 'bulletList': return reading.flavour === 'plain' ? bulletNode(block.items, reading, path, depth) : listNode({ type: 'bulletList' }, block.items, reading, path, depth) case 'code': - return codeBlockNode(block.language, block.text, path, depth) + return codeBlockNode(block.language, block.text, reading, path, depth) case 'directive': return directiveNode(block, reading, path, depth) case 'fault': @@ -250,10 +250,11 @@ function listNode(node: AdfNode, items: readonly Block[][], reading: Reading, pa return success({ ...node, content }) } -function codeBlockNode(language: string, text: string, path: ConvertErrorPath, depth: number): Result { +function codeBlockNode(language: string, text: string, reading: Reading, path: ConvertErrorPath, depth: number): Result { if (language === carryName) { const carried = readCarriedBlock(text, depth) if (carried.fault !== undefined) return faulted(carried.fault, path) + reading.carried.add(carried.value) return success(carried.value) } const node: AdfNode = language === '' ? { type: 'codeBlock' } : { attrs: { language }, type: 'codeBlock' } diff --git a/src/markdown/parse/task-ids.ts b/src/markdown/parse/task-ids.ts index 4043558..8033605 100644 --- a/src/markdown/parse/task-ids.ts +++ b/src/markdown/parse/task-ids.ts @@ -4,8 +4,7 @@ import { nodeAttrs, nodeContent } from '../../adf/document.ts' const taskTypes = new Set(['blockTaskItem', 'taskItem', 'taskList']) -// docs/decisions.md, Plain task ids come from position. -export function mintTaskIds(document: AdfDocument, markdown: string): void { +export function mintTaskIds(document: AdfDocument, markdown: string, carried: ReadonlySet): void { const taken = new Set() for (const node of preorder(document, () => true)) { const localId = nodeAttrs(node)['localId'] @@ -13,8 +12,8 @@ export function mintTaskIds(document: AdfDocument, markdown: string): void { } const seed = hash128(markdown).join(' ') let count = 0 - for (const node of preorder(document, (held) => blockNodeModel(held.type) !== undefined)) { - if (!taskTypes.has(node.type) || typeof nodeAttrs(node)['localId'] === 'string') continue + for (const node of preorder(document, (held) => !carried.has(held) && blockNodeModel(held.type)?.contentModel === 'block')) { + if (!taskTypes.has(node.type) || carried.has(node) || typeof nodeAttrs(node)['localId'] === 'string') continue let localId = '' do { count += 1