Compare commits
2 Commits
c81bda9bfb
...
bd8d240712
| Author | SHA1 | Date | |
|---|---|---|---|
| bd8d240712 | |||
| 3edc12aaa9 |
+2
-1
@@ -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 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, 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
|
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
|
## A callout title keeps its link targets
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { unsupportedNodeShape } from '../directive-syntax.ts'
|
|||||||
type Paragraph = Extract<Block, { kind: 'paragraph' }>
|
type Paragraph = Extract<Block, { kind: 'paragraph' }>
|
||||||
|
|
||||||
// `inExpand` is whether an expand holds the blocks, which makes a folded callout a nestedExpand.
|
// `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<AdfNode>; definitions: LinkDefinitions; flavour: Flavour; inExpand: boolean; memo: SpellingMemo }
|
||||||
|
|
||||||
const documentStart: SourcePosition = { line: 1, offset: 0 }
|
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'
|
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<AdfDocument, ParseE
|
|||||||
|
|
||||||
function readDocument(markdown: string, flavour: Flavour): Result<AdfDocument, ParseError> {
|
function readDocument(markdown: string, flavour: Flavour): Result<AdfDocument, ParseError> {
|
||||||
const parsed = parseBlocks(markdown)
|
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)
|
const content = positioned(readBlocks(parsed.blocks, reading, [], 0), documentStart)
|
||||||
if (!content.ok) return content
|
if (!content.ok) return content
|
||||||
const document: AdfDocument = content.value.length === 0 ? { type: 'doc', version: 1 } : { content: content.value, type: 'doc', version: 1 }
|
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)
|
return success(document)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ function readBlock(block: Block, reading: Reading, path: ConvertErrorPath, depth
|
|||||||
case 'bulletList':
|
case 'bulletList':
|
||||||
return reading.flavour === 'plain' ? bulletNode(block.items, reading, path, depth) : listNode({ type: 'bulletList' }, block.items, reading, path, depth)
|
return reading.flavour === 'plain' ? bulletNode(block.items, reading, path, depth) : listNode({ type: 'bulletList' }, block.items, reading, path, depth)
|
||||||
case 'code':
|
case 'code':
|
||||||
return codeBlockNode(block.language, block.text, path, depth)
|
return codeBlockNode(block.language, block.text, reading, path, depth)
|
||||||
case 'directive':
|
case 'directive':
|
||||||
return directiveNode(block, reading, path, depth)
|
return directiveNode(block, reading, path, depth)
|
||||||
case 'fault':
|
case 'fault':
|
||||||
@@ -250,10 +250,11 @@ function listNode(node: AdfNode, items: readonly Block[][], reading: Reading, pa
|
|||||||
return success({ ...node, content })
|
return success({ ...node, content })
|
||||||
}
|
}
|
||||||
|
|
||||||
function codeBlockNode(language: string, text: string, path: ConvertErrorPath, depth: number): Result<AdfNode> {
|
function codeBlockNode(language: string, text: string, reading: Reading, path: ConvertErrorPath, depth: number): Result<AdfNode> {
|
||||||
if (language === carryName) {
|
if (language === carryName) {
|
||||||
const carried = readCarriedBlock(text, depth)
|
const carried = readCarriedBlock(text, depth)
|
||||||
if (carried.fault !== undefined) return faulted(carried.fault, path)
|
if (carried.fault !== undefined) return faulted(carried.fault, path)
|
||||||
|
reading.carried.add(carried.value)
|
||||||
return success(carried.value)
|
return success(carried.value)
|
||||||
}
|
}
|
||||||
const node: AdfNode = language === '' ? { type: 'codeBlock' } : { attrs: { language }, type: 'codeBlock' }
|
const node: AdfNode = language === '' ? { type: 'codeBlock' } : { attrs: { language }, type: 'codeBlock' }
|
||||||
|
|||||||
@@ -236,6 +236,10 @@ test('keeps what markdownToAdf reads that no row reads, and refuses only what it
|
|||||||
const future = bare('futureBlock', text('==x=='))
|
const future = bare('futureBlock', text('==x=='))
|
||||||
const carried = adfToMarkdown(document(future))
|
const carried = adfToMarkdown(document(future))
|
||||||
assert.deepEqual(carried.ok ? read(carried.value) : carried.error.code, [future])
|
assert.deepEqual(carried.ok ? read(carried.value) : carried.error.code, [future])
|
||||||
|
const uncarriable = bare('taskList', node('taskItem', { extra: { a: 1 }, state: 'TODO' }, text('a')))
|
||||||
|
const carriedPanel = adfToMarkdown(document(node('panel', { extra: true, panelType: 'info' }, uncarriable)))
|
||||||
|
const restored = carriedPanel.ok ? plainMarkdownToAdf(carriedPanel.value) : carriedPanel
|
||||||
|
assert.deepEqual(restored.ok ? restored.value.content : restored.error.code, [node('panel', { extra: true, panelType: 'info' }, uncarriable)])
|
||||||
const red: AdfMark = { attrs: { color: '#ff0000' }, type: 'backgroundColor' }
|
const red: AdfMark = { attrs: { color: '#ff0000' }, type: 'backgroundColor' }
|
||||||
const held = paragraph(text('a ==b== c', red), text(' ==d '), { attrs: { note: 'x' }, text: 'e==f', type: 'text' }, text(' g=='))
|
const held = paragraph(text('a ==b== c', red), text(' ==d '), { attrs: { note: 'x' }, text: 'e==f', type: 'text' }, text(' g=='))
|
||||||
const spelled = adfToMarkdown(document(held))
|
const spelled = adfToMarkdown(document(held))
|
||||||
|
|||||||
@@ -23,34 +23,38 @@ function tasks(...content: AdfNode[]): AdfDocument {
|
|||||||
test('mints each task node lacking a localId a UUID v4 in document order, unique and the same every run', () => {
|
test('mints each task node lacking a localId a UUID v4 in document order, unique and the same every run', () => {
|
||||||
const unminted = (): AdfDocument => tasks({ content: [{ attrs: { state: 'TODO' }, type: 'taskItem' }, { content: [{ attrs: { state: 'DONE' }, type: 'blockTaskItem' }], type: 'taskList' }], type: 'taskList' })
|
const unminted = (): AdfDocument => tasks({ content: [{ attrs: { state: 'TODO' }, type: 'taskItem' }, { content: [{ attrs: { state: 'DONE' }, type: 'blockTaskItem' }], type: 'taskList' }], type: 'taskList' })
|
||||||
const minted = unminted()
|
const minted = unminted()
|
||||||
mintTaskIds(minted, '- [ ] a\n')
|
mintTaskIds(minted, '- [ ] a\n', new Set())
|
||||||
const ids = taskIds(minted)
|
const ids = taskIds(minted)
|
||||||
assert.equal(ids.length, 4)
|
assert.equal(ids.length, 4)
|
||||||
for (const id of ids) assert.match(String(id), uuidV4)
|
for (const id of ids) assert.match(String(id), uuidV4)
|
||||||
assert.equal(new Set(ids).size, 4)
|
assert.equal(new Set(ids).size, 4)
|
||||||
const again = unminted()
|
const again = unminted()
|
||||||
mintTaskIds(again, '- [ ] a\n')
|
mintTaskIds(again, '- [ ] a\n', new Set())
|
||||||
assert.deepEqual(taskIds(again), ids)
|
assert.deepEqual(taskIds(again), ids)
|
||||||
const other = unminted()
|
const other = unminted()
|
||||||
mintTaskIds(other, '- [ ] b\n')
|
mintTaskIds(other, '- [ ] b\n', new Set())
|
||||||
assert.equal(taskIds(other).some((id) => ids.includes(id)), false)
|
assert.equal(taskIds(other).some((id) => ids.includes(id)), false)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('keeps a localId the document spells and skips it when minting, a carried one too', () => {
|
test('keeps a localId the document spells and skips it when minting, a carried one too', () => {
|
||||||
const first = tasks({ type: 'taskList' })
|
const first = tasks({ type: 'taskList' })
|
||||||
mintTaskIds(first, 'x')
|
mintTaskIds(first, 'x', new Set())
|
||||||
const [taken] = taskIds(first)
|
const [taken] = taskIds(first)
|
||||||
const spelled = tasks({ type: 'taskList' }, { content: [{ attrs: { localId: String(taken) }, type: 'taskList' }], type: 'futureBlock' })
|
const carried: AdfNode = { content: [{ attrs: { localId: String(taken) }, type: 'taskList' }], type: 'futureBlock' }
|
||||||
mintTaskIds(spelled, 'x')
|
const spelled = tasks({ type: 'taskList' }, carried)
|
||||||
|
mintTaskIds(spelled, 'x', new Set([carried]))
|
||||||
const [minted, kept] = taskIds(spelled)
|
const [minted, kept] = taskIds(spelled)
|
||||||
assert.equal(kept, taken)
|
assert.equal(kept, taken)
|
||||||
assert.notEqual(minted, taken)
|
assert.notEqual(minted, taken)
|
||||||
assert.match(String(minted), uuidV4)
|
assert.match(String(minted), uuidV4)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('leaves a task node inside a node type this version does not know as carried', () => {
|
test('leaves a node the carry restores as carried, minting neither it nor what it holds', () => {
|
||||||
const carried: AdfNode = { content: [{ type: 'taskList' }], type: 'futureBlock' }
|
const list = (): AdfNode => ({ content: [{ attrs: { state: 'TODO' }, type: 'taskItem' }], type: 'taskList' })
|
||||||
const document = tasks(carried)
|
const carriedList = list()
|
||||||
mintTaskIds(document, 'x')
|
const carriedPanel: AdfNode = { attrs: { panelType: 'info' }, content: [list()], type: 'panel' }
|
||||||
assert.deepEqual(document, tasks({ content: [{ type: 'taskList' }], type: 'futureBlock' }))
|
const future: AdfNode = { content: [list()], type: 'futureBlock' }
|
||||||
|
const document = tasks(carriedList, carriedPanel, future)
|
||||||
|
mintTaskIds(document, 'x', new Set([carriedList, carriedPanel]))
|
||||||
|
assert.deepEqual(document, tasks(list(), { attrs: { panelType: 'info' }, content: [list()], type: 'panel' }, { content: [list()], type: 'futureBlock' }))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import { nodeAttrs, nodeContent } from '../../adf/document.ts'
|
|||||||
|
|
||||||
const taskTypes = new Set(['blockTaskItem', 'taskItem', 'taskList'])
|
const taskTypes = new Set(['blockTaskItem', 'taskItem', 'taskList'])
|
||||||
|
|
||||||
// docs/decisions.md, Plain task ids come from position.
|
export function mintTaskIds(document: AdfDocument, markdown: string, carried: ReadonlySet<AdfNode>): void {
|
||||||
export function mintTaskIds(document: AdfDocument, markdown: string): void {
|
|
||||||
const taken = new Set<string>()
|
const taken = new Set<string>()
|
||||||
for (const node of preorder(document, () => true)) {
|
for (const node of preorder(document, () => true)) {
|
||||||
const localId = nodeAttrs(node)['localId']
|
const localId = nodeAttrs(node)['localId']
|
||||||
@@ -13,8 +12,8 @@ export function mintTaskIds(document: AdfDocument, markdown: string): void {
|
|||||||
}
|
}
|
||||||
const seed = hash128(markdown).join(' ')
|
const seed = hash128(markdown).join(' ')
|
||||||
let count = 0
|
let count = 0
|
||||||
for (const node of preorder(document, (held) => blockNodeModel(held.type) !== undefined)) {
|
for (const node of preorder(document, (held) => !carried.has(held) && blockNodeModel(held.type)?.contentModel === 'block')) {
|
||||||
if (!taskTypes.has(node.type) || typeof nodeAttrs(node)['localId'] === 'string') continue
|
if (!taskTypes.has(node.type) || carried.has(node) || typeof nodeAttrs(node)['localId'] === 'string') continue
|
||||||
let localId = ''
|
let localId = ''
|
||||||
do {
|
do {
|
||||||
count += 1
|
count += 1
|
||||||
|
|||||||
Reference in New Issue
Block a user