10f - plainMarkdownToAdf mints task localIds as UUID v4s hashed from the markdown and position #148
@@ -158,13 +158,15 @@ read replaces mentions, attachments and macros with text.
|
|||||||
- Text that would read as a marker takes a backslash: `==` wherever it could open or close a
|
- Text that would read as a marker takes a backslash: `==` wherever it could open or close a
|
||||||
highlight, `[!…]` opening a quote, and `[x]` or `[ ]` opening any list item, since GitHub reads
|
highlight, `[!…]` opening a quote, and `[x]` or `[ ]` opening any list item, since GitHub reads
|
||||||
that marker per item.
|
that marker per item.
|
||||||
- A node read back carries no `localId`, which Atlassian's schema requires on `taskList`,
|
- A node read back carries no `localId` except a `taskList`, `taskItem` or `blockTaskItem`, which
|
||||||
`taskItem` and `blockTaskItem`: mint one where the receiving site requires it.
|
Atlassian's schema requires one on: each gets a UUID v4 hashed from the whole markdown and its
|
||||||
|
position, the same on every read. Join markdown bound for one document and read it once: the same
|
||||||
|
markdown read twice into one document repeats its ids.
|
||||||
|
|
||||||
## The errors
|
## The errors
|
||||||
|
|
||||||
An ADF node type this version does not know is not an error: it is carried opaquely and restores
|
An ADF node type this version does not know is not an error: the lossless pair carries it opaquely
|
||||||
unchanged ([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)).
|
and restores it unchanged ([`docs/decisions.md`](https://gitea.larvit.se/larvit/adf-codec/src/branch/main/docs/decisions.md#unknown-nodes-ride-the-carry)).
|
||||||
|
|
||||||
`ConvertError` is `{ code, message, path, position? }`. `code` is the exported `ConvertErrorCode`,
|
`ConvertError` is `{ code, message, path, position? }`. `code` is the exported `ConvertErrorCode`,
|
||||||
stable across minors and safe to `switch` on exhaustively with no `default`; `message` is free text
|
stable across minors and safe to `switch` on exhaustively with no `default`; `message` is free text
|
||||||
|
|||||||
+10
-8
@@ -50,8 +50,8 @@ An unknown ADF node is carried opaquely — raw JSON rides a dedicated syntax in
|
|||||||
restores to a deep-equal node. The round-trip holds for documents newer than the library. So does
|
restores to a deep-equal node. The round-trip holds for documents newer than the library. So does
|
||||||
a known node no section spells where it stands: a markdown serializer spells a node by type without
|
a known node no section spells where it stands: a markdown serializer spells a node by type without
|
||||||
checking its position, and refusing loses a document ADF itself keeps in an `unsupportedBlock`.
|
checking its position, and refusing loses a document ADF itself keeps in an `unsupportedBlock`.
|
||||||
Where a container's own spelling cannot hold the child it has — a `bulletList` outside `listItem`,
|
Where a container's own spelling cannot hold the child it has — a `bulletList` holding other than
|
||||||
a `codeBlock` outside text — the error result names that instead.
|
`listItem`, a `codeBlock` other than text — the error result names that instead.
|
||||||
|
|
||||||
## Foreign HTML sorts three ways
|
## Foreign HTML sorts three ways
|
||||||
|
|
||||||
@@ -127,13 +127,15 @@ accepted.
|
|||||||
|
|
||||||
## Plain task ids come from position
|
## Plain task ids come from position
|
||||||
|
|
||||||
2026-09-26, the maintainer. Goals 5 and 7. Valid while a site rejects a task node with no
|
2026-09-26, spelling 2026-09-29, the maintainer. Goals 5 and 7. Valid while a site rejects a task
|
||||||
`localId`. Lands with `todo.md` 10f.
|
node with no `localId`.
|
||||||
|
|
||||||
`plainMarkdownToAdf` gives each `taskList`, `taskItem` and `blockTaskItem` a `localId` from its
|
`plainMarkdownToAdf` gives each `taskList`, `taskItem` and `blockTaskItem` lacking one a `localId`
|
||||||
position in document order, unique within the document and minted with no host API, so a site
|
in the editor's UUID v4 shape, hashed from the whole markdown and the node's order among those it
|
||||||
rejecting a missing `localId` takes the document and the same markdown reads to the same ids every
|
mints, skipping any id the document holds: the same markdown reads to the same ids every run,
|
||||||
run.
|
different markdown to different ids. The same markdown pasted twice into one document repeats its
|
||||||
|
ids: determinism 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
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { languageSlot } from '../code-language.ts'
|
|||||||
import { largestNesting } from '../../nesting.ts'
|
import { largestNesting } from '../../nesting.ts'
|
||||||
import { leadingMarker, readAlertMarker, readTaskMarker } from '../plain-conventions.ts'
|
import { leadingMarker, readAlertMarker, readTaskMarker } from '../plain-conventions.ts'
|
||||||
import { listBreakName, listBreakSpelling } from '../block-directive.ts'
|
import { listBreakName, listBreakSpelling } from '../block-directive.ts'
|
||||||
|
import { mintTaskIds } from './task-ids.ts'
|
||||||
import { nodeAttrs, nodeContent } from '../../adf/document.ts'
|
import { nodeAttrs, nodeContent } from '../../adf/document.ts'
|
||||||
import { parseBlocks } from './blocks.ts'
|
import { parseBlocks } from './blocks.ts'
|
||||||
import { parseInlineContent } from './inline-content.ts'
|
import { parseInlineContent } from './inline-content.ts'
|
||||||
@@ -21,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'
|
||||||
@@ -37,10 +38,12 @@ 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
|
||||||
return success(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, reading.carried)
|
||||||
|
return success(document)
|
||||||
}
|
}
|
||||||
|
|
||||||
function readBlocks(blocks: readonly Block[], reading: Reading, path: ConvertErrorPath, depth: number): Result<AdfNode[]> {
|
function readBlocks(blocks: readonly Block[], reading: Reading, path: ConvertErrorPath, depth: number): Result<AdfNode[]> {
|
||||||
@@ -80,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':
|
||||||
@@ -247,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' }
|
||||||
|
|||||||
@@ -13,9 +13,22 @@ const em: AdfMark = { type: 'em' }
|
|||||||
const highlight: AdfMark = { attrs: { color: '#f8e6a0' }, type: 'backgroundColor' }
|
const highlight: AdfMark = { attrs: { color: '#f8e6a0' }, type: 'backgroundColor' }
|
||||||
const strong: AdfMark = { type: 'strong' }
|
const strong: AdfMark = { type: 'strong' }
|
||||||
|
|
||||||
|
const taskTypes = ['blockTaskItem', 'taskItem', 'taskList']
|
||||||
|
|
||||||
function read(markdown: string): readonly AdfNode[] | string {
|
function read(markdown: string): readonly AdfNode[] | string {
|
||||||
const parsed = plainMarkdownToAdf(markdown)
|
const parsed = plainMarkdownToAdf(markdown)
|
||||||
return parsed.ok ? (toEditorNormal(parsed.value).content ?? []) : parsed.error.code
|
if (!parsed.ok) return parsed.error.code
|
||||||
|
const blocks = toEditorNormal(parsed.value).content ?? []
|
||||||
|
const pending = [...blocks]
|
||||||
|
for (let block = pending.pop(); block !== undefined; block = pending.pop()) {
|
||||||
|
for (const child of block.content ?? []) pending.push(child)
|
||||||
|
if (!taskTypes.includes(block.type)) continue
|
||||||
|
const { localId, ...attrs } = block.attrs ?? {}
|
||||||
|
assert.equal(typeof localId, 'string', `${block.type} in ${JSON.stringify(markdown)}`)
|
||||||
|
if (Object.keys(attrs).length === 0) delete block.attrs
|
||||||
|
else block.attrs = attrs
|
||||||
|
}
|
||||||
|
return blocks
|
||||||
}
|
}
|
||||||
|
|
||||||
function normal(...blocks: AdfNode[]): readonly AdfNode[] {
|
function normal(...blocks: AdfNode[]): readonly AdfNode[] {
|
||||||
@@ -123,6 +136,15 @@ test('reads a bullet list whose every item leads with a task marker to a task li
|
|||||||
bare('taskList', task('DONE', text('Write the spec')), task('TODO', text('Ship '), text('it', strong)), task('DONE', text('Tell'))),
|
bare('taskList', task('DONE', text('Write the spec')), task('TODO', text('Ship '), text('it', strong)), task('DONE', text('Tell'))),
|
||||||
])
|
])
|
||||||
assert.deepEqual(read('- [x]\n- [ ]\\\n after\n'), normal(bare('taskList', task('DONE'), task('TODO', text('after')))))
|
assert.deepEqual(read('- [x]\n- [ ]\\\n after\n'), normal(bare('taskList', task('DONE'), task('TODO', text('after')))))
|
||||||
|
const minted = plainMarkdownToAdf('- [x] Parent\n - [ ] Child\n')
|
||||||
|
assert.deepEqual(minted.ok ? minted.value.content : minted.error.code, [
|
||||||
|
node(
|
||||||
|
'taskList',
|
||||||
|
{ localId: '51470556-7c91-46cb-b140-16e225a9b1f2' },
|
||||||
|
node('taskItem', { localId: 'e04cbd87-eb04-4737-ae72-6d56d7799874', state: 'DONE' }, text('Parent')),
|
||||||
|
node('taskList', { localId: '9ab33f3f-8c58-428f-b4eb-343a32a177d6' }, node('taskItem', { localId: 'b16a2f09-9543-499b-8a97-b88fc342b918', state: 'TODO' }, text('Child'))),
|
||||||
|
),
|
||||||
|
])
|
||||||
})
|
})
|
||||||
|
|
||||||
test('moves a nested task list beside its item and makes an item holding more than one block a block task item', () => {
|
test('moves a nested task list beside its item and makes an item holding more than one block a block task item', () => {
|
||||||
@@ -208,9 +230,16 @@ test('reads text the writer kept from reading as a marker back as text', () => {
|
|||||||
|
|
||||||
test('keeps what markdownToAdf reads that no row reads, and refuses only what it refuses', () => {
|
test('keeps what markdownToAdf reads that no row reads, and refuses only what it refuses', () => {
|
||||||
assert.deepEqual(read('!adf:panel warning\n- [x] a\n!adf:/panel\n'), [panel('warning', bare('taskList', task('DONE', text('a'))))])
|
assert.deepEqual(read('!adf:panel warning\n- [x] a\n!adf:/panel\n'), [panel('warning', bare('taskList', task('DONE', text('a'))))])
|
||||||
|
assert.deepEqual(read('!adf:taskList\n!adf:taskItem TODO\nb\n!adf:/taskItem\n!adf:/taskList\n'), [bare('taskList', task('TODO', text('b')))])
|
||||||
|
const listed = plainMarkdownToAdf('!adf:taskList {localId=01a0eeb2-be48-7ea7-8587-db5e013c374a}\n- [ ] b\n!adf:/taskList\n')
|
||||||
|
assert.deepEqual(listed.ok ? listed.value.content?.[0]?.attrs : listed.error.code, { localId: '01a0eeb2-be48-7ea7-8587-db5e013c374a' })
|
||||||
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))
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
||||||
|
import { mintTaskIds } from './task-ids.ts'
|
||||||
|
|
||||||
|
const uuidV4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
|
||||||
|
|
||||||
|
function taskIds(document: AdfDocument): unknown[] {
|
||||||
|
const ids: unknown[] = []
|
||||||
|
const pending: AdfNode[] = [...(document.content ?? [])].reverse()
|
||||||
|
for (let node = pending.pop(); node !== undefined; node = pending.pop()) {
|
||||||
|
if (['blockTaskItem', 'taskItem', 'taskList'].includes(node.type)) ids.push(node.attrs?.['localId'])
|
||||||
|
for (const child of [...(node.content ?? [])].reverse()) pending.push(child)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
|
function tasks(...content: AdfNode[]): AdfDocument {
|
||||||
|
return { content, type: 'doc', version: 1 }
|
||||||
|
}
|
||||||
|
|
||||||
|
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 minted = unminted()
|
||||||
|
mintTaskIds(minted, '- [ ] a\n', new Set())
|
||||||
|
const ids = taskIds(minted)
|
||||||
|
assert.equal(ids.length, 4)
|
||||||
|
for (const id of ids) assert.match(String(id), uuidV4)
|
||||||
|
assert.equal(new Set(ids).size, 4)
|
||||||
|
const again = unminted()
|
||||||
|
mintTaskIds(again, '- [ ] a\n', new Set())
|
||||||
|
assert.deepEqual(taskIds(again), ids)
|
||||||
|
const other = unminted()
|
||||||
|
mintTaskIds(other, '- [ ] b\n', new Set())
|
||||||
|
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', () => {
|
||||||
|
const first = tasks({ type: 'taskList' })
|
||||||
|
mintTaskIds(first, 'x', new Set())
|
||||||
|
const [taken] = taskIds(first)
|
||||||
|
const carried: AdfNode = { content: [{ attrs: { localId: String(taken) }, type: 'taskList' }], type: 'futureBlock' }
|
||||||
|
const spelled = tasks({ type: 'taskList' }, carried)
|
||||||
|
mintTaskIds(spelled, 'x', new Set([carried]))
|
||||||
|
const [minted, kept] = taskIds(spelled)
|
||||||
|
assert.equal(kept, taken)
|
||||||
|
assert.notEqual(minted, taken)
|
||||||
|
assert.match(String(minted), uuidV4)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('leaves a node the carry restores as carried, minting neither it nor what it holds', () => {
|
||||||
|
const list = (): AdfNode => ({ content: [{ attrs: { state: 'TODO' }, type: 'taskItem' }], type: 'taskList' })
|
||||||
|
const carriedList = list()
|
||||||
|
const carriedPanel: AdfNode = { attrs: { panelType: 'info' }, content: [list()], type: 'panel' }
|
||||||
|
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' }))
|
||||||
|
})
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import type { AdfDocument, AdfNode } from '../../adf/document.ts'
|
||||||
|
import { blockNodeModel } from '../../adf/block-nodes.ts'
|
||||||
|
import { nodeAttrs, nodeContent } from '../../adf/document.ts'
|
||||||
|
|
||||||
|
const taskTypes = new Set(['blockTaskItem', 'taskItem', 'taskList'])
|
||||||
|
|
||||||
|
export function mintTaskIds(document: AdfDocument, markdown: string, carried: ReadonlySet<AdfNode>): void {
|
||||||
|
const taken = new Set<string>()
|
||||||
|
for (const node of preorder(document, () => true)) {
|
||||||
|
const localId = nodeAttrs(node)['localId']
|
||||||
|
if (typeof localId === 'string') taken.add(localId)
|
||||||
|
}
|
||||||
|
const seed = hash128(markdown).join(' ')
|
||||||
|
let count = 0
|
||||||
|
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
|
||||||
|
localId = uuidV4(hash128(`${seed} ${count}`))
|
||||||
|
} while (taken.has(localId))
|
||||||
|
taken.add(localId)
|
||||||
|
node.attrs = { ...node.attrs, localId }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function* preorder(document: AdfDocument, entered: (node: AdfNode) => boolean): Generator<AdfNode> {
|
||||||
|
const pending: AdfNode[] = []
|
||||||
|
const pushReversed = (nodes: readonly AdfNode[]): void => {
|
||||||
|
for (let index = nodes.length - 1; index >= 0; index -= 1) {
|
||||||
|
const node = nodes[index]
|
||||||
|
if (node !== undefined) pending.push(node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pushReversed(document.content ?? [])
|
||||||
|
for (let node = pending.pop(); node !== undefined; node = pending.pop()) {
|
||||||
|
yield node
|
||||||
|
if (entered(node)) pushReversed(nodeContent(node))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cyrb128, public domain.
|
||||||
|
function hash128(text: string): number[] {
|
||||||
|
let h1 = 1779033703
|
||||||
|
let h2 = 3144134277
|
||||||
|
let h3 = 1013904242
|
||||||
|
let h4 = 2773480762
|
||||||
|
for (let index = 0; index < text.length; index += 1) {
|
||||||
|
const unit = text.charCodeAt(index)
|
||||||
|
h1 = h2 ^ Math.imul(h1 ^ unit, 597399067)
|
||||||
|
h2 = h3 ^ Math.imul(h2 ^ unit, 2869860233)
|
||||||
|
h3 = h4 ^ Math.imul(h3 ^ unit, 951274213)
|
||||||
|
h4 = h1 ^ Math.imul(h4 ^ unit, 2716044179)
|
||||||
|
}
|
||||||
|
h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067)
|
||||||
|
h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233)
|
||||||
|
h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213)
|
||||||
|
h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179)
|
||||||
|
h1 ^= h2 ^ h3 ^ h4
|
||||||
|
h2 ^= h1
|
||||||
|
h3 ^= h1
|
||||||
|
h4 ^= h1
|
||||||
|
return [h1 >>> 0, h2 >>> 0, h3 >>> 0, h4 >>> 0]
|
||||||
|
}
|
||||||
|
|
||||||
|
function uuidV4(lanes: readonly number[]): string {
|
||||||
|
const hex = lanes.map((lane) => lane.toString(16).padStart(8, '0')).join('')
|
||||||
|
const variant = ((Number.parseInt(hex.charAt(16), 16) & 3) | 8).toString(16)
|
||||||
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-${variant}${hex.slice(17, 20)}-${hex.slice(20, 32)}`
|
||||||
|
}
|
||||||
@@ -2,9 +2,6 @@
|
|||||||
|
|
||||||
## 0.2.0
|
## 0.2.0
|
||||||
|
|
||||||
- **10f — Give task nodes read from plain markdown position ids.** Per `docs/decisions.md` §Plain
|
|
||||||
task ids come from position, README §Plain markdown's `localId` bullet saying so. The id spelling
|
|
||||||
is part of the chunk.
|
|
||||||
- **41 — Keep a link's target when `plainMarkdownToAdf` reads a callout title.** Per
|
- **41 — Keep a link's target when `plainMarkdownToAdf` reads a callout title.** Per
|
||||||
`docs/decisions.md` §A callout title keeps its link targets; today `> [!faq]- See [x](http://y)`
|
`docs/decisions.md` §A callout title keeps its link targets; today `> [!faq]- See [x](http://y)`
|
||||||
reads to an expand titled `See x`, the target gone.
|
reads to an expand titled `See x`, the target gone.
|
||||||
|
|||||||
Reference in New Issue
Block a user