Compare commits
2 Commits
ba50364ae7
...
33a5e01511
| Author | SHA1 | Date | |
|---|---|---|---|
| 33a5e01511 | |||
| 016d5eba26 |
@@ -50,25 +50,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "paragraph"
|
"type": "paragraph"
|
||||||
},
|
|
||||||
{
|
|
||||||
"content": [
|
|
||||||
{
|
|
||||||
"attrs": {
|
|
||||||
"localId": "01a0a067-68cf-78af-abd6-c660ec0d189b"
|
|
||||||
},
|
|
||||||
"text": "Two ",
|
|
||||||
"type": "text"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"attrs": {
|
|
||||||
"localId": "01a0a067-68d9-73fa-9b9a-caa42061993c"
|
|
||||||
},
|
|
||||||
"text": "anchors",
|
|
||||||
"type": "text"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"type": "paragraph"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"type": "doc",
|
"type": "doc",
|
||||||
|
|||||||
@@ -3,5 +3,3 @@
|
|||||||
Signed by :adf{json="{\"attrs\":{\"localId\":\"01a0a067-68d2-787e-afc5-2459776aa029\"},\"text\":\"the owner\",\"type\":\"text\"}"}
|
Signed by :adf{json="{\"attrs\":{\"localId\":\"01a0a067-68d2-787e-afc5-2459776aa029\"},\"text\":\"the owner\",\"type\":\"text\"}"}
|
||||||
|
|
||||||
:adf{json="{\"attrs\":{\"localId\":\"01a0a067-68d5-7372-9962-29a039654056\"},\"text\":\"One \",\"type\":\"text\"}"}:adf{json="{\"attrs\":{\"localId\":\"01a0a067-68d5-7372-9962-29a039654056\"},\"text\":\"anchor\",\"type\":\"text\"}"}
|
:adf{json="{\"attrs\":{\"localId\":\"01a0a067-68d5-7372-9962-29a039654056\"},\"text\":\"One \",\"type\":\"text\"}"}:adf{json="{\"attrs\":{\"localId\":\"01a0a067-68d5-7372-9962-29a039654056\"},\"text\":\"anchor\",\"type\":\"text\"}"}
|
||||||
|
|
||||||
:adf{json="{\"attrs\":{\"localId\":\"01a0a067-68cf-78af-abd6-c660ec0d189b\"},\"text\":\"Two \",\"type\":\"text\"}"}:adf{json="{\"attrs\":{\"localId\":\"01a0a067-68d9-73fa-9b9a-caa42061993c\"},\"text\":\"anchors\",\"type\":\"text\"}"}
|
|
||||||
|
|||||||
+1
-1
@@ -72,7 +72,7 @@ export function isAdfMark(value: unknown): value is AdfMark {
|
|||||||
return !('attrs' in value) || isAttributes(value['attrs'])
|
return !('attrs' in value) || isAttributes(value['attrs'])
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nodeAttrs(node: { attrs?: AdfAttributes }): AdfAttributes {
|
export function nodeAttrs(node: { attrs?: AdfAttributes }): Readonly<AdfAttributes> {
|
||||||
return node.attrs ?? {}
|
return node.attrs ?? {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import assert from 'node:assert/strict'
|
|||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
|
|
||||||
import type { AdfNode } from './document.ts'
|
import type { AdfNode } from './document.ts'
|
||||||
|
import type { JsonValue } from '../json-value.ts'
|
||||||
import { toEditorNormal } from './editor-normal.ts'
|
import { toEditorNormal } from './editor-normal.ts'
|
||||||
|
|
||||||
test('merges adjacent text nodes carrying identical marks, at every level', () => {
|
test('merges adjacent text nodes carrying identical marks, at every level', () => {
|
||||||
@@ -14,6 +15,7 @@ test('merges adjacent text nodes carrying identical marks, at every level', () =
|
|||||||
{ text: 'e', type: 'text' },
|
{ text: 'e', type: 'text' },
|
||||||
{ attrs: { localId: '01a0a06b-5281-7f27-9022-8d3a74b0ab0d' }, text: 'f', type: 'text' },
|
{ attrs: { localId: '01a0a06b-5281-7f27-9022-8d3a74b0ab0d' }, text: 'f', type: 'text' },
|
||||||
{ text: 'g', type: 'text' },
|
{ text: 'g', type: 'text' },
|
||||||
|
{ attrs: {}, text: 'h', type: 'text' },
|
||||||
]
|
]
|
||||||
assert.deepEqual(toEditorNormal({ content: [{ attrs: { panelType: 'info' }, content: [{ content, type: 'paragraph' }], type: 'panel' }], type: 'doc', version: 1 }), {
|
assert.deepEqual(toEditorNormal({ content: [{ attrs: { panelType: 'info' }, content: [{ content, type: 'paragraph' }], type: 'panel' }], type: 'doc', version: 1 }), {
|
||||||
content: [
|
content: [
|
||||||
@@ -27,7 +29,7 @@ test('merges adjacent text nodes carrying identical marks, at every level', () =
|
|||||||
{ type: 'hardBreak' },
|
{ type: 'hardBreak' },
|
||||||
{ text: 'e', type: 'text' },
|
{ text: 'e', type: 'text' },
|
||||||
{ attrs: { localId: '01a0a06b-5281-7f27-9022-8d3a74b0ab0d' }, text: 'f', type: 'text' },
|
{ attrs: { localId: '01a0a06b-5281-7f27-9022-8d3a74b0ab0d' }, text: 'f', type: 'text' },
|
||||||
{ text: 'g', type: 'text' },
|
{ text: 'gh', type: 'text' },
|
||||||
],
|
],
|
||||||
type: 'paragraph',
|
type: 'paragraph',
|
||||||
},
|
},
|
||||||
@@ -58,7 +60,7 @@ test('reads an empty attrs object, marks array or content array as the absent ke
|
|||||||
assert.deepEqual(toEditorNormal({ content: [], type: 'doc', version: 1 }), { type: 'doc', version: 1 })
|
assert.deepEqual(toEditorNormal({ content: [], type: 'doc', version: 1 }), { type: 'doc', version: 1 })
|
||||||
})
|
})
|
||||||
|
|
||||||
test('normalizes a document nesting far past the levels a recursive walk survives', () => {
|
test('normalizes blocks and mark attributes nesting far past the levels a recursive walk survives', () => {
|
||||||
const levels = 100000
|
const levels = 100000
|
||||||
let node: AdfNode = { content: [], type: 'paragraph' }
|
let node: AdfNode = { content: [], type: 'paragraph' }
|
||||||
for (let level = 0; level < levels; level += 1) node = { content: [node], type: 'blockquote' }
|
for (let level = 0; level < levels; level += 1) node = { content: [node], type: 'blockquote' }
|
||||||
@@ -67,4 +69,9 @@ test('normalizes a document nesting far past the levels a recursive walk survive
|
|||||||
for (; normal?.content !== undefined; depth += 1) normal = normal.content[0]
|
for (; normal?.content !== undefined; depth += 1) normal = normal.content[0]
|
||||||
assert.equal(depth, levels)
|
assert.equal(depth, levels)
|
||||||
assert.deepEqual(normal, { type: 'paragraph' })
|
assert.deepEqual(normal, { type: 'paragraph' })
|
||||||
|
let deep: JsonValue = 1
|
||||||
|
for (let level = 0; level < 2 * levels; level += 1) deep = [deep]
|
||||||
|
const marks = [{ attrs: { deep }, type: 'textColor' }]
|
||||||
|
const merged = toEditorNormal({ content: [{ content: [{ marks, text: 'a', type: 'text' }, { marks, text: 'b', type: 'text' }], type: 'paragraph' }], type: 'doc', version: 1 })
|
||||||
|
assert.deepEqual(merged.content?.[0]?.content?.map((text) => text.text), ['ab'])
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export function sameMark(candidate: AdfMark, mark: AdfMark): boolean {
|
|||||||
return markKey(candidate) === markKey(mark)
|
return markKey(candidate) === markKey(mark)
|
||||||
}
|
}
|
||||||
|
|
||||||
// AGENTS.md §2: adjacent text nodes carrying identical marks and no attributes are one node.
|
|
||||||
export function mergeAdjacentText(nodes: readonly AdfNode[]): AdfNode[] {
|
export function mergeAdjacentText(nodes: readonly AdfNode[]): AdfNode[] {
|
||||||
const merged: AdfNode[] = []
|
const merged: AdfNode[] = []
|
||||||
for (const node of nodes) {
|
for (const node of nodes) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import assert from 'node:assert/strict'
|
import assert from 'node:assert/strict'
|
||||||
import test from 'node:test'
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import type { JsonValue } from './json-value.ts'
|
||||||
import { serializeCanonicalJson } from './canonical-json.ts'
|
import { serializeCanonicalJson } from './canonical-json.ts'
|
||||||
|
|
||||||
test('sorts object keys recursively', () => {
|
test('sorts object keys recursively', () => {
|
||||||
@@ -39,3 +40,15 @@ test('leaves non-ASCII raw', () => {
|
|||||||
test('spells scalars in canonical JSON', () => {
|
test('spells scalars in canonical JSON', () => {
|
||||||
assert.equal(serializeCanonicalJson([null, true, false, 0, -1.5, 'a"b'], 'compact'), '[null,true,false,0,-1.5,"a\\"b"]')
|
assert.equal(serializeCanonicalJson([null, true, false, 0, -1.5, 'a"b'], 'compact'), '[null,true,false,0,-1.5,"a\\"b"]')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('spells a value nesting far past the levels a recursive walk survives', () => {
|
||||||
|
const levels = 200000
|
||||||
|
let array: JsonValue = 1
|
||||||
|
let object: JsonValue = 1
|
||||||
|
for (let level = 0; level < levels; level += 1) {
|
||||||
|
array = [array]
|
||||||
|
object = { a: object }
|
||||||
|
}
|
||||||
|
assert.equal(serializeCanonicalJson(array, 'compact'), `${'['.repeat(levels)}1${']'.repeat(levels)}`)
|
||||||
|
assert.equal(serializeCanonicalJson(object, 'compact'), `${'{"a":'.repeat(levels)}1${'}'.repeat(levels)}`)
|
||||||
|
})
|
||||||
|
|||||||
+32
-18
@@ -2,28 +2,42 @@ import type { JsonValue } from './json-value.ts'
|
|||||||
|
|
||||||
export type JsonSpelling = 'compact' | 'two-space'
|
export type JsonSpelling = 'compact' | 'two-space'
|
||||||
|
|
||||||
|
type Member = { label: string; value: JsonValue }
|
||||||
|
|
||||||
|
type Pending = string | { depth: number; value: JsonValue }
|
||||||
|
|
||||||
export function serializeCanonicalJson(value: JsonValue, spelling: JsonSpelling): string {
|
export function serializeCanonicalJson(value: JsonValue, spelling: JsonSpelling): string {
|
||||||
return serialize(value, spelling === 'compact' ? '' : ' ', 0)
|
const indent = spelling === 'compact' ? '' : ' '
|
||||||
|
const text: string[] = []
|
||||||
|
const pending: Pending[] = [{ depth: 0, value }]
|
||||||
|
for (let next = pending.pop(); next !== undefined; next = pending.pop()) {
|
||||||
|
if (typeof next === 'string') {
|
||||||
|
text.push(next)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
const { depth, value: held } = next
|
||||||
|
if (Array.isArray(held)) schedule(pending, '[', held.map((item) => ({ label: '', value: item })), ']', indent, depth)
|
||||||
|
else if (held !== null && typeof held === 'object') schedule(pending, '{', objectMembers(held, indent), '}', indent, depth)
|
||||||
|
else text.push(JSON.stringify(held))
|
||||||
|
}
|
||||||
|
return text.join('')
|
||||||
}
|
}
|
||||||
|
|
||||||
function serialize(value: JsonValue, indent: string, depth: number): string {
|
function objectMembers(value: { [key: string]: JsonValue }, indent: string): Member[] {
|
||||||
if (Array.isArray(value)) {
|
|
||||||
if (value.length === 0) return '[]'
|
|
||||||
const items = value.map((item) => serialize(item, indent, depth + 1))
|
|
||||||
return `[${join(items, indent, depth)}]`
|
|
||||||
}
|
|
||||||
if (value !== null && typeof value === 'object') {
|
|
||||||
const keys = Object.keys(value).sort()
|
|
||||||
if (keys.length === 0) return '{}'
|
|
||||||
const separator = indent === '' ? ':' : ': '
|
const separator = indent === '' ? ':' : ': '
|
||||||
const entries = keys.map((key) => `${JSON.stringify(key)}${separator}${serialize(value[key] ?? null, indent, depth + 1)}`)
|
return Object.keys(value)
|
||||||
return `{${join(entries, indent, depth)}}`
|
.sort()
|
||||||
}
|
.map((key) => ({ label: `${JSON.stringify(key)}${separator}`, value: value[key] ?? null }))
|
||||||
return JSON.stringify(value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function join(parts: readonly string[], indent: string, depth: number): string {
|
function schedule(pending: Pending[], open: string, members: readonly Member[], close: string, indent: string, depth: number): void {
|
||||||
if (indent === '') return parts.join(',')
|
if (members.length === 0) {
|
||||||
const inner = `\n${indent.repeat(depth + 1)}`
|
pending.push(`${open}${close}`)
|
||||||
return `${inner}${parts.join(`,${inner}`)}\n${indent.repeat(depth)}`
|
return
|
||||||
|
}
|
||||||
|
const inner = indent === '' ? '' : `\n${indent.repeat(depth + 1)}`
|
||||||
|
const scheduled: Pending[] = []
|
||||||
|
for (const [index, member] of members.entries()) scheduled.push(`${index === 0 ? open : ','}${inner}${member.label}`, { depth: depth + 1, value: member.value })
|
||||||
|
scheduled.push(indent === '' ? close : `\n${indent.repeat(depth)}${close}`)
|
||||||
|
for (const item of scheduled.reverse()) pending.push(item)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -96,7 +96,7 @@ for (const directory of roundTripDirectories) {
|
|||||||
assert.ok(isAdfDocument(expected), `${name}.json is not an ADF document`)
|
assert.ok(isAdfDocument(expected), `${name}.json is not an ADF document`)
|
||||||
const result = markdownToAdf(readFileSync(join(roundTripRoot, directory, `${name}.md`), 'utf8'))
|
const result = markdownToAdf(readFileSync(join(roundTripRoot, directory, `${name}.md`), 'utf8'))
|
||||||
assert.ok(result.ok, result.ok ? '' : `${result.error.code}: ${result.error.message}`)
|
assert.ok(result.ok, result.ok ? '' : `${result.error.code}: ${result.error.message}`)
|
||||||
assert.deepEqual(toEditorNormal(result.value), toEditorNormal(expected))
|
assert.deepEqual(toEditorNormal(result.value), expected)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -185,12 +185,12 @@ for (const name of pairedNames(normalizationRoot, '.md', '.json')) {
|
|||||||
assert.ok(isAdfDocument(expected), `${name}.json is not an ADF document`)
|
assert.ok(isAdfDocument(expected), `${name}.json is not an ADF document`)
|
||||||
const result = markdownToAdf(readFileSync(join(normalizationRoot, `${name}.md`), 'utf8'))
|
const result = markdownToAdf(readFileSync(join(normalizationRoot, `${name}.md`), 'utf8'))
|
||||||
assert.ok(result.ok, result.ok ? '' : `${result.error.code}: ${result.error.message}`)
|
assert.ok(result.ok, result.ok ? '' : `${result.error.code}: ${result.error.message}`)
|
||||||
assert.deepEqual(toEditorNormal(result.value), toEditorNormal(expected))
|
assert.deepEqual(toEditorNormal(result.value), expected)
|
||||||
const emitted = adfToMarkdown(result.value)
|
const emitted = adfToMarkdown(result.value)
|
||||||
assert.ok(emitted.ok, emitted.ok ? '' : `${emitted.error.code}: ${emitted.error.message}`)
|
assert.ok(emitted.ok, emitted.ok ? '' : `${emitted.error.code}: ${emitted.error.message}`)
|
||||||
const again = markdownToAdf(emitted.value)
|
const again = markdownToAdf(emitted.value)
|
||||||
assert.ok(again.ok, again.ok ? '' : `${again.error.code}: ${again.error.message}`)
|
assert.ok(again.ok, again.ok ? '' : `${again.error.code}: ${again.error.message}`)
|
||||||
assert.deepEqual(toEditorNormal(again.value), toEditorNormal(expected))
|
assert.deepEqual(toEditorNormal(again.value), expected)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -356,7 +356,7 @@ test('refuses marks and attributes nested deeper than the emitter carries', () =
|
|||||||
assert.ok(spelled.ok, spelled.ok ? '' : spelled.error.message)
|
assert.ok(spelled.ok, spelled.ok ? '' : spelled.error.message)
|
||||||
const read = markdownToAdf(spelled.value)
|
const read = markdownToAdf(spelled.value)
|
||||||
assert.ok(read.ok, read.ok ? '' : read.error.message)
|
assert.ok(read.ok, read.ok ? '' : read.error.message)
|
||||||
assert.deepEqual(toEditorNormal(read.value), toEditorNormal(document(node)))
|
assert.deepEqual(toEditorNormal(read.value), document(node))
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.equal(markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs, type: 'em' }], text: 'x', type: 'text' })))), deeper('depth', 'em', largestNesting - 3))
|
assert.equal(markdown(adfToMarkdown(document(paragraph({ marks: [{ attrs, type: 'em' }], text: 'x', type: 'text' })))), deeper('depth', 'em', largestNesting - 3))
|
||||||
|
|||||||
Reference in New Issue
Block a user