@dinoxx/dinox-cli 1.0.19 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/README.md +756 -472
  2. package/dist/auth/userInfo.d.ts +10 -1
  3. package/dist/auth/userInfo.js +66 -17
  4. package/dist/cli.js +0 -2
  5. package/dist/commands/auth/index.js +29 -14
  6. package/dist/commands/boxes/index.js +272 -52
  7. package/dist/commands/boxes/model.d.ts +34 -0
  8. package/dist/commands/boxes/model.js +78 -0
  9. package/dist/commands/boxes/operations.d.ts +86 -0
  10. package/dist/commands/boxes/operations.js +415 -0
  11. package/dist/commands/boxes/repo.d.ts +13 -6
  12. package/dist/commands/boxes/repo.js +272 -149
  13. package/dist/commands/boxes/store.d.ts +22 -0
  14. package/dist/commands/boxes/store.js +143 -0
  15. package/dist/commands/graph/index.js +2 -36
  16. package/dist/commands/mcp/noteContentTools.d.ts +222 -0
  17. package/dist/commands/mcp/noteContentTools.js +390 -0
  18. package/dist/commands/mcp/server.d.ts +3 -0
  19. package/dist/commands/mcp/server.js +45 -16
  20. package/dist/commands/mcp/tools.js +3 -1
  21. package/dist/commands/mcp/types.d.ts +6 -2
  22. package/dist/commands/meta/index.js +6 -22
  23. package/dist/commands/meta/repo.d.ts +0 -3
  24. package/dist/commands/meta/repo.js +1 -4
  25. package/dist/commands/notes/actions.d.ts +2 -0
  26. package/dist/commands/notes/actions.js +149 -55
  27. package/dist/commands/notes/bulk.d.ts +34 -0
  28. package/dist/commands/notes/bulk.js +204 -0
  29. package/dist/commands/notes/contentEngine.d.ts +95 -0
  30. package/dist/commands/notes/contentEngine.js +652 -0
  31. package/dist/commands/notes/contentPatch.d.ts +84 -0
  32. package/dist/commands/notes/contentPatch.js +337 -0
  33. package/dist/commands/notes/contentPatchCommand.d.ts +3 -0
  34. package/dist/commands/notes/contentPatchCommand.js +126 -0
  35. package/dist/commands/notes/descriptors.js +192 -40
  36. package/dist/commands/notes/export.d.ts +21 -0
  37. package/dist/commands/notes/export.js +224 -0
  38. package/dist/commands/notes/metadataMutation.d.ts +28 -0
  39. package/dist/commands/notes/metadataMutation.js +127 -0
  40. package/dist/commands/notes/repo.d.ts +20 -0
  41. package/dist/commands/notes/repo.js +135 -18
  42. package/dist/commands/notes/runtime.d.ts +3 -12
  43. package/dist/commands/notes/runtime.js +4 -30
  44. package/dist/commands/notes/searchSql.js +1 -1
  45. package/dist/commands/prompt/index.js +15 -45
  46. package/dist/commands/prompt/repo.js +2 -2
  47. package/dist/commands/runtime.d.ts +26 -0
  48. package/dist/commands/runtime.js +76 -0
  49. package/dist/commands/schema/renderSkills.js +20 -7
  50. package/dist/commands/schema/spec.d.ts +1 -2
  51. package/dist/commands/schema/spec.js +2 -3
  52. package/dist/commands/schema/types.d.ts +0 -13
  53. package/dist/commands/storage/index.js +30 -60
  54. package/dist/commands/storage/repo.d.ts +1 -1
  55. package/dist/commands/storage/repo.js +20 -1
  56. package/dist/commands/sync.js +4 -2
  57. package/dist/commands/tags/content.d.ts +32 -0
  58. package/dist/commands/tags/content.js +243 -0
  59. package/dist/commands/tags/index.js +285 -49
  60. package/dist/commands/tags/model.d.ts +38 -0
  61. package/dist/commands/tags/model.js +100 -0
  62. package/dist/commands/tags/operations.d.ts +105 -0
  63. package/dist/commands/tags/operations.js +438 -0
  64. package/dist/commands/tags/repo.d.ts +5 -1
  65. package/dist/commands/tags/repo.js +142 -218
  66. package/dist/commands/tags/store.d.ts +28 -0
  67. package/dist/commands/tags/store.js +166 -0
  68. package/dist/commands/todo/index.js +30 -61
  69. package/dist/commands/writeDryRun.d.ts +25 -0
  70. package/dist/commands/writeDryRun.js +31 -1
  71. package/dist/config/paths.d.ts +2 -1
  72. package/dist/config/paths.js +11 -1
  73. package/dist/config/resolve.js +9 -7
  74. package/dist/daemon/index.d.ts +2 -0
  75. package/dist/daemon/index.js +176 -58
  76. package/dist/powersync/runtime.d.ts +2 -1
  77. package/dist/powersync/runtime.js +15 -3
  78. package/dist/powersync/schema/content.d.ts +2 -0
  79. package/dist/powersync/schema/content.js +2 -0
  80. package/dist/powersync/schema/index.d.ts +2 -0
  81. package/dist/skills/check.d.ts +1 -1
  82. package/dist/skills/check.js +27 -2
  83. package/dist/utils/authToken.d.ts +2 -0
  84. package/dist/utils/authToken.js +7 -2
  85. package/dist/utils/noteContentEngine.d.ts +333 -0
  86. package/dist/utils/noteContentEngine.js +740 -0
  87. package/dist/utils/tiptapMarkdownSchema.js +31 -0
  88. package/package.json +4 -8
@@ -0,0 +1,740 @@
1
+ import { generateId } from './id.js';
2
+ export class NoteContentEditError extends Error {
3
+ code;
4
+ details;
5
+ constructor(code, message, details) {
6
+ super(message);
7
+ this.code = code;
8
+ this.details = details;
9
+ this.name = 'NoteContentEditError';
10
+ }
11
+ }
12
+ export const NOTE_CONTENT_EDIT_OPERATION_TYPES = ['insertBlocksAfter', 'insertBlocksBefore', 'replaceBlocks', 'deleteBlocks', 'replaceText', 'updateBlockAttrs', 'replaceContainerContent'];
13
+ const INLINE = new Set(['text', 'hardBreak', 'mention', 'emoji', 'bidlink', 'hashtag', 'location', 'time', 'inlineMath']);
14
+ const ATOMIC = new Set(['image', 'video', 'audio', 'file', 'bookmark', 'iframe', 'mermaid', 'mindgraph', 'blockMath', 'math']);
15
+ const CONTAINER = new Set(['table', 'pdfAnnotation', 'audiosegment']);
16
+ const ANCHOR_KEYS = ['anchorKey', 'data-toc-id', 'id', 'uniqueId'];
17
+ const HASH_IGNORED_ATTRS = new Set(['anchorKey', 'data-toc-id', '_hash']);
18
+ const EMPTY_DOC = { type: 'doc', content: [{ type: 'paragraph', content: [] }] };
19
+ const SAFE_ATTRS = {
20
+ paragraph: ['anchorKey', 'id', 'uniqueId'], heading: ['anchorKey', 'id', 'uniqueId', 'level'], bulletList: ['anchorKey', 'id', 'uniqueId'], orderedList: ['anchorKey', 'id', 'uniqueId'], taskList: ['anchorKey', 'id', 'uniqueId'], blockquote: ['anchorKey', 'id', 'uniqueId'], codeBlock: ['anchorKey', 'id', 'uniqueId', 'language', 'lang'], horizontalRule: ['anchorKey', 'id', 'uniqueId'], table: ['anchorKey', 'id', 'uniqueId'], image: ['anchorKey', 'id', 'uniqueId', 'src', 'resourceId', 'storageKey', 'alt', 'title', 'caption', 'showCaption'], video: ['anchorKey', 'id', 'uniqueId', 'src', 'resourceId', 'storageKey', 'poster', 'title', 'autoplay', 'loop', 'muted', 'playsInline'], audio: ['anchorKey', 'id', 'uniqueId', 'src', 'resourceId', 'storageKey', 'title', 'preload'], file: ['anchorKey', 'id', 'uniqueId', 'src', 'resourceId', 'storageKey', 'name', 'type', 'mimeType', 'size'], mermaid: ['anchorKey', 'id', 'uniqueId', 'code'], mindgraph: ['anchorKey', 'id', 'uniqueId', 'markdown'],
21
+ };
22
+ const PATCHABLE_ATTRS = { image: ['alt', 'title', 'caption', 'showCaption'], video: ['title', 'poster', 'autoplay', 'loop', 'muted', 'playsInline'], audio: ['title', 'preload'], mermaid: ['code'], mindgraph: ['markdown'] };
23
+ export function normalizeTiptapDocument(value) {
24
+ const sanitized = sanitizeNode(coerceDocument(parseMaybeJson(value)));
25
+ if (!node(sanitized) || sanitized.type !== 'doc')
26
+ return clone(EMPTY_DOC);
27
+ return Array.isArray(sanitized.content) && sanitized.content.length ? sanitized : clone(EMPTY_DOC);
28
+ }
29
+ export function buildNoteEditContext(input) {
30
+ const document = normalizeTiptapDocument(input.document ?? input.contentJson);
31
+ const blocks = topNodes(document).map((block, index) => {
32
+ const anchorId = anchor(block) ?? `pos-${index}`;
33
+ const text = plainText(block).trim();
34
+ return { anchorId, hasStableAnchor: Boolean(anchor(block)), nodeIndex: index, nodeRange: `${index}:${index + 1}`, type: block.type, editMode: mode(block.type), level: num(block.attrs?.level), text, textPreview: text.slice(0, 1_000), textHash: hashString(text), nodeHash: hashNode(block), charCount: text.length, attrs: safeAttrs(block) };
35
+ });
36
+ const totalCharCount = blocks.reduce((sum, block) => sum + block.charCount, 0);
37
+ return { noteId: input.noteId, title: input.title ?? '', contentHash: hashStable(document), updatedAt: input.updatedAt ?? null, version: input.version ?? null, document, blocks, chunks: chunks(blocks), totalCharCount, estimatedTokens: Math.ceil(totalCharCount / 2.5) };
38
+ }
39
+ export function applyNoteEditPatch(input) {
40
+ const before = buildNoteEditContext(input);
41
+ validatePatch(input.patch, before.contentHash, input.updatedAt, input.version);
42
+ const blocks = clone(topNodes(before.document));
43
+ const resolved = input.patch.operations.map((operation, order) => ({ ...resolveOperation(operation, before.blocks), operation, order }));
44
+ assertNoRangeConflicts(resolved);
45
+ for (const item of [...resolved].sort((a, b) => b.startIndex - a.startIndex || b.order - a.order))
46
+ applyResolved(blocks, item);
47
+ const document = withStableAnchors(normalizeTiptapDocument({ ...before.document, content: blocks.length ? blocks : [{ type: 'paragraph', content: [] }] }), input.createAnchorId);
48
+ const projection = projectNoteContent(document);
49
+ const after = buildNoteEditContext({ ...input, document: projection.document });
50
+ return { noteId: input.noteId, changed: before.contentHash !== projection.contentHash, before, after, projection, appliedOperations: resolved.length, operationResults: resolved.map((item) => ({ operationIndex: item.order, type: item.operation.op, success: true, target: describeTarget(item.operation.target), nodeRange: `${item.startIndex}:${item.endIndex}`, error: null })).sort((a, b) => a.operationIndex - b.operationIndex) };
51
+ }
52
+ export function projectNoteContent(document) {
53
+ const sanitized = normalizeTiptapDocument(document);
54
+ const metadata = metadataFrom(sanitized);
55
+ return { document: sanitized, content_json: stableStringify(sanitized), content_text: contentText(plainText(sanitized)), content_md: markdown(sanitized), images: JSON.stringify(metadata.images), bid_links: JSON.stringify(metadata.bidLinks), contentHash: hashStable(sanitized) };
56
+ }
57
+ export function topNodes(document) {
58
+ return Array.isArray(document.content) ? document.content.filter(node) : [];
59
+ }
60
+ export function parseNodeRangeString(spec, total) {
61
+ const [a, b] = String(spec ?? '').split(':');
62
+ let start = a?.trim() ? Number(a) : 0;
63
+ let end = b?.trim() ? Number(b) : total;
64
+ if (!Number.isFinite(start) || start < 0)
65
+ start = 0;
66
+ if (!Number.isFinite(end) || end < 0)
67
+ end = total;
68
+ if (end < start)
69
+ [start, end] = [end, start];
70
+ return { start: Math.max(0, Math.min(total, Math.floor(start))), end: Math.max(0, Math.min(total, Math.floor(end))) };
71
+ }
72
+ export function serializeAnchoredMarkdown(nodes, blocks, startIndex = 0) {
73
+ return nodes.map((block, offset) => {
74
+ const index = startIndex + offset;
75
+ const descriptor = blocks[index];
76
+ const attrs = safeAttrs(block);
77
+ const header = [`block=${escapeComment(descriptor?.anchorId ?? anchor(block) ?? `pos-${index}`)}`, `index=${index}`, `type=${escapeComment(block.type)}`, `mode=${escapeComment(descriptor?.editMode ?? mode(block.type))}`, descriptor?.textHash ? `hash=${descriptor.textHash}` : '', descriptor?.nodeHash ? `nodeHash=${descriptor.nodeHash}` : '', descriptor?.hasStableAnchor === false ? 'unstableAnchor=true' : '', attrs ? `attrs=${escapeComment(JSON.stringify(attrs))}` : ''].filter(Boolean).join(' ');
78
+ return `<!-- ${header} -->\n${markdown({ type: 'doc', content: [block] }) || `[${block.type}]`}`;
79
+ }).join('\n\n').trim();
80
+ }
81
+ export function serializeAnchoredShorthand(nodes, blocks, startIndex = 0) {
82
+ return nodes.map((block, offset) => {
83
+ const index = startIndex + offset;
84
+ const descriptor = blocks[index];
85
+ const editMode = descriptor?.editMode ?? mode(block.type);
86
+ const attrs = compactAttrs(safeAttrs(block), block.type);
87
+ const header = [`@${index}`, `a=${token(descriptor?.anchorId ?? anchor(block) ?? `pos-${index}`)}`, `t=${token(block.type)}`, `m=${token(editMode)}`, descriptor?.textHash ? `h=${token(descriptor.textHash)}` : '', descriptor?.nodeHash ? `nh=${token(descriptor.nodeHash)}` : '', descriptor?.hasStableAnchor === false ? 'unstable=1' : '', attrs ? `attrs=${token(JSON.stringify(attrs))}` : ''].filter(Boolean).join(' ');
88
+ const body = editMode === 'text' ? markdown({ type: 'doc', content: [block] }) : `[${editMode}:${block.type}] protected block`;
89
+ return [header, body].filter(Boolean).join('\n');
90
+ }).join('\n\n').trim();
91
+ }
92
+ export function blockTextHash(blocks, start, end) {
93
+ return start + 1 === end ? blocks[start]?.textHash ?? '' : hashString(blocks.slice(start, end).map((block) => block.text).join('\n\n').trim());
94
+ }
95
+ export function blockNodeHash(blocks, start, end) {
96
+ return start + 1 === end ? blocks[start]?.nodeHash ?? '' : hashStable(blocks.slice(start, end).map((block) => block.nodeHash));
97
+ }
98
+ export function getNoteContentReadInstructions() {
99
+ return 'dinox-shorthand is the preferred read format. Each header has @index, a=anchorId, t=node type, m=edit mode, h=text hash, nh=node hash. For noteContentEdit, pass preferredTarget when possible. Use replaceText only for short exact substitutions; use replaceBlocks for long rewrites. Treat atomicBlock/container/unsupported as protected.';
100
+ }
101
+ export function getNoteContentSchemaSummary() {
102
+ return { version: 'dinox-note-ai-schema-2026-05-v1', readFormat: 'dinox-shorthand', editProtocol: 'structured-operations', operations: [...NOTE_CONTENT_EDIT_OPERATION_TYPES] };
103
+ }
104
+ function parseMaybeJson(value, depth = 0) {
105
+ if (typeof value !== 'string')
106
+ return value;
107
+ const trimmed = value.trim();
108
+ if (!trimmed || trimmed === 'null' || trimmed === 'undefined')
109
+ return null;
110
+ try {
111
+ const parsed = JSON.parse(trimmed);
112
+ return typeof parsed === 'string' && depth < 2 ? parseMaybeJson(parsed, depth + 1) : parsed;
113
+ }
114
+ catch {
115
+ return value;
116
+ }
117
+ }
118
+ function coerceDocument(value) {
119
+ if (Array.isArray(value))
120
+ return { type: 'doc', content: value.filter(node) };
121
+ if (!node(value))
122
+ return clone(EMPTY_DOC);
123
+ if (value.type === 'doc')
124
+ return clone(value);
125
+ return INLINE.has(value.type) ? { type: 'doc', content: [{ type: 'paragraph', content: [clone(value)] }] } : { type: 'doc', content: [clone(value)] };
126
+ }
127
+ function sanitizeNode(value) {
128
+ if (!node(value) || !value.type)
129
+ return null;
130
+ if (value.type === 'text') {
131
+ const text = typeof value.text === 'string' ? value.text : '';
132
+ return text ? withoutContent({ ...value, type: 'text', text, marks: marks(value.marks) }) : null;
133
+ }
134
+ if (value.type === 'doc')
135
+ return normalizeRoot(value);
136
+ if (value.type === 'paragraph')
137
+ return normalizeParagraph(value);
138
+ if (value.type === 'listItem' || value.type === 'taskItem')
139
+ return normalizeListItem(value);
140
+ const next = withoutContent(value);
141
+ if (Array.isArray(value.content))
142
+ next.content = flatten(value.content);
143
+ return next;
144
+ }
145
+ function normalizeRoot(value) {
146
+ const content = [];
147
+ let inline = [];
148
+ const flush = () => { if (inline.length)
149
+ content.push({ type: 'paragraph', content: inline }); inline = []; };
150
+ for (const child of flatten(value.content))
151
+ INLINE.has(child.type) ? inline.push(child) : (flush(), content.push(child));
152
+ flush();
153
+ return { type: 'doc', ...(value.attrs ? { attrs: value.attrs } : {}), content };
154
+ }
155
+ function normalizeParagraph(value) {
156
+ const content = [];
157
+ let inline = [];
158
+ let usedAttrs = false;
159
+ const flush = () => { if (!inline.length)
160
+ return; content.push({ type: 'paragraph', ...(value.attrs && !usedAttrs ? { attrs: value.attrs } : {}), content: inline }); usedAttrs = true; inline = []; };
161
+ for (const child of flatten(value.content))
162
+ INLINE.has(child.type) ? inline.push(child) : (flush(), content.push(child));
163
+ flush();
164
+ return content.length ? content : [{ type: 'paragraph', ...(value.attrs ? { attrs: value.attrs } : {}), content: [] }];
165
+ }
166
+ function normalizeListItem(value) {
167
+ const next = withoutContent(value);
168
+ const content = flatten(value.content);
169
+ next.content = content[0]?.type === 'paragraph' ? content : [{ type: 'paragraph', content: [] }, ...content];
170
+ return next;
171
+ }
172
+ function flatten(content) {
173
+ return Array.isArray(content) ? content.flatMap((child) => {
174
+ const current = sanitizeNode(child);
175
+ return !current ? [] : Array.isArray(current) ? current : [current];
176
+ }) : [];
177
+ }
178
+ function validatePatch(patch, contentHash, updatedAt, version) {
179
+ if (!patch?.base?.contentHash)
180
+ throw err('invalid_plan', 'AI 编辑补丁缺少 base.contentHash,请先读取笔记内容');
181
+ if (!Array.isArray(patch.operations) || patch.operations.length === 0)
182
+ throw err('invalid_plan', 'AI 编辑补丁至少需要一个操作');
183
+ if (patch.operations.length > 50)
184
+ throw err('invalid_plan', 'AI 编辑补丁一次最多允许 50 个操作');
185
+ if (patch.base.contentHash !== contentHash)
186
+ throw err('content_conflict', '笔记内容已变化,请重新读取后再编辑');
187
+ if (patch.base.updatedAt && patch.base.updatedAt !== (updatedAt ?? null))
188
+ throw err('content_conflict', '笔记更新时间已变化,请重新读取后再编辑');
189
+ if (typeof patch.base.version === 'number' && patch.base.version !== (version ?? null))
190
+ throw err('content_conflict', '笔记版本已变化,请重新读取后再编辑');
191
+ for (const operation of patch.operations) {
192
+ if (!NOTE_CONTENT_EDIT_OPERATION_TYPES.includes(operation?.op))
193
+ throw err('invalid_plan', `不支持的 AI 编辑操作: ${String(operation?.op ?? '')}`);
194
+ if (!operation.target)
195
+ throw err('invalid_plan', `${operation.op} 缺少 target`);
196
+ if (['insertBlocksAfter', 'insertBlocksBefore', 'replaceBlocks', 'replaceContainerContent'].includes(operation.op) && (!('blocks' in operation) || !operation.blocks?.length))
197
+ throw err('invalid_plan', `${operation.op} 需要 blocks`);
198
+ if (operation.op === 'replaceText' && (!operation.matchText || typeof operation.replacementText !== 'string'))
199
+ throw err('invalid_plan', 'replaceText 需要 matchText 和 replacementText');
200
+ }
201
+ }
202
+ function resolveOperation(operation, blocks) {
203
+ const range = resolveTarget(operation.target, blocks);
204
+ validateTarget(operation, blocks, range.startIndex, range.endIndex);
205
+ validateProtected(operation, blocks.slice(range.startIndex, range.endIndex));
206
+ return range;
207
+ }
208
+ function resolveTarget(target, blocks) {
209
+ const anchors = target.anchorIds?.length ? target.anchorIds : target.anchorId ? [target.anchorId] : [];
210
+ if (anchors.length) {
211
+ const indexes = anchors.map((id) => blocks.find((block) => block.anchorId === id)?.nodeIndex);
212
+ if (indexes.every((index) => typeof index === 'number')) {
213
+ const sorted = indexes.sort((a, b) => a - b);
214
+ const range = { startIndex: sorted[0] ?? 0, endIndex: (sorted.at(-1) ?? 0) + 1 };
215
+ if (targetMatches(target, blocks, range.startIndex, range.endIndex))
216
+ return range;
217
+ }
218
+ }
219
+ if (target.nodeRange?.trim()) {
220
+ const parsed = parseNodeRangeString(target.nodeRange, blocks.length);
221
+ if (parsed.start !== parsed.end && targetMatches(target, blocks, parsed.start, parsed.end))
222
+ return { startIndex: parsed.start, endIndex: parsed.end };
223
+ }
224
+ if (typeof target.nodeIndex === 'number') {
225
+ const index = Math.floor(target.nodeIndex);
226
+ if (index < 0 || index >= blocks.length)
227
+ throw err('target_not_found', '目标 nodeIndex 超出范围');
228
+ if (targetMatches(target, blocks, index, index + 1))
229
+ return { startIndex: index, endIndex: index + 1 };
230
+ }
231
+ const hashMatch = uniqueHash(blocks, target.expectedNodeHash, 'node') ?? uniqueHash(blocks, target.expectedTextHash, 'text');
232
+ if (hashMatch)
233
+ return hashMatch;
234
+ if (target.textMatch?.trim()) {
235
+ const needle = target.textMatch.toLowerCase().trim();
236
+ const matches = blocks.filter((block) => block.text.toLowerCase().includes(needle));
237
+ if (matches.length > 1)
238
+ throw err('target_not_found', '目标文本匹配多个块,请使用 nodeRange 或 hash 重新定位');
239
+ if (matches[0] && targetMatches(target, blocks, matches[0].nodeIndex, matches[0].nodeIndex + 1))
240
+ return { startIndex: matches[0].nodeIndex, endIndex: matches[0].nodeIndex + 1 };
241
+ }
242
+ throw err('target_not_found', anchors.length ? '目标 anchorId 不存在,且无法通过 nodeRange/hash/textMatch 唯一恢复' : '无法解析 AI 编辑目标');
243
+ }
244
+ function validateTarget(operation, blocks, start, end) {
245
+ if (operation.target.expectedNodeHash && operation.target.expectedNodeHash !== blockNodeHash(blocks, start, end))
246
+ throw err('target_conflict', '目标节点 hash 不匹配,请重新读取后再编辑');
247
+ if (!operation.target.expectedNodeHash && operation.target.expectedTextHash && operation.target.expectedTextHash !== blockTextHash(blocks, start, end))
248
+ throw err('target_conflict', '目标文本 hash 不匹配,请重新读取后再编辑');
249
+ if (operation.target.expectedTextSnippet && !blocks.slice(start, end).map((block) => block.text).join('\n\n').includes(operation.target.expectedTextSnippet))
250
+ throw err('target_conflict', '目标文本片段不匹配,请重新读取后再编辑');
251
+ if (operation.op === 'replaceText' && end - start !== 1)
252
+ throw err('invalid_plan', 'replaceText 只能作用于单个块;多块结构调整请使用 replaceBlocks');
253
+ if ((operation.op === 'updateBlockAttrs' || operation.op === 'replaceContainerContent') && end - start !== 1)
254
+ throw err('invalid_plan', `${operation.op} 只能作用于单个块`);
255
+ }
256
+ function validateProtected(operation, targetBlocks) {
257
+ if (!targetBlocks.length)
258
+ return;
259
+ if (operation.op === 'updateBlockAttrs') {
260
+ if (!PATCHABLE_ATTRS[targetBlocks[0].type]?.length)
261
+ throw err('invalid_plan', '目标节点不支持属性更新');
262
+ return;
263
+ }
264
+ if (operation.op === 'replaceText' && targetBlocks.some((block) => block.editMode !== 'text'))
265
+ throw err('invalid_plan', '非文本/受保护节点不能使用 replaceText;请使用安全的结构化块操作');
266
+ const protectedBlocks = targetBlocks.filter((block) => block.editMode !== 'text');
267
+ if (protectedBlocks.length && operation.op === 'deleteBlocks' && !operation.allowProtectedDelete)
268
+ throw err('invalid_plan', '目标包含原子/容器节点;删除时必须显式设置 allowProtectedDelete');
269
+ if (protectedBlocks.length && operation.op === 'replaceBlocks' && !operation.allowProtectedReplace)
270
+ throw err('invalid_plan', '目标包含原子/容器节点;替换时必须显式设置 allowProtectedReplace');
271
+ }
272
+ function applyResolved(blocks, item) {
273
+ const { operation, startIndex, endIndex } = item;
274
+ if (operation.op === 'insertBlocksAfter')
275
+ blocks.splice(endIndex, 0, ...specs(operation.blocks));
276
+ if (operation.op === 'insertBlocksBefore')
277
+ blocks.splice(startIndex, 0, ...specs(operation.blocks));
278
+ if (operation.op === 'replaceBlocks')
279
+ blocks.splice(startIndex, endIndex - startIndex, ...specs(operation.blocks));
280
+ if (operation.op === 'deleteBlocks')
281
+ blocks.splice(startIndex, endIndex - startIndex);
282
+ if (operation.op === 'replaceText')
283
+ blocks[startIndex] = replaceText(blocks[startIndex], operation);
284
+ if (operation.op === 'updateBlockAttrs')
285
+ blocks[startIndex] = { ...blocks[startIndex], attrs: { ...(blocks[startIndex].attrs ?? {}), ...patchAttrs(blocks[startIndex].type, operation.attrs) } };
286
+ if (operation.op === 'replaceContainerContent')
287
+ blocks[startIndex] = { ...blocks[startIndex], content: specs(operation.blocks) };
288
+ }
289
+ function specs(values) {
290
+ if (!Array.isArray(values) || values.length === 0)
291
+ throw err('invalid_content', '块编辑操作需要 blocks');
292
+ if (values.length > 100)
293
+ throw err('invalid_content', '单个操作最多允许 100 个块');
294
+ return values.map((value) => normalizeTiptapDocument({ type: 'doc', content: [spec(value)] }).content?.[0] ?? { type: 'paragraph', content: [] });
295
+ }
296
+ function spec(value) {
297
+ if (value.type === 'paragraph')
298
+ return { type: 'paragraph', content: inline(value) };
299
+ if (value.type === 'heading')
300
+ return { type: 'heading', attrs: { level: headingLevel(value.level) }, content: inline(value) };
301
+ if (value.type === 'blockquote')
302
+ return { type: 'blockquote', content: [{ type: 'paragraph', content: inline(value) }] };
303
+ if (value.type === 'bulletList' || value.type === 'orderedList')
304
+ return { type: value.type, content: listItems(value.items).map((item) => ({ type: 'listItem', content: [{ type: 'paragraph', content: textInline(item) }] })) };
305
+ if (value.type === 'taskList')
306
+ return { type: 'taskList', content: taskItems(value.items).map((item) => ({ type: 'taskItem', attrs: { id: generateId(), checked: item.checked }, content: [{ type: 'paragraph', content: textInline(item.text) }] })) };
307
+ if (value.type === 'codeBlock')
308
+ return { type: 'codeBlock', attrs: attr({ language: lang(value.language) }), content: textInline(text(value.text, 'codeBlock.text', true)) };
309
+ if (value.type === 'horizontalRule')
310
+ return { type: 'horizontalRule' };
311
+ if (value.type === 'image')
312
+ return { type: 'image', attrs: attr({ src: optUrl(value.src, 'image.src'), alt: opt(value.alt, 'image.alt', 500), title: opt(value.title, 'image.title', 500), resourceId: opt(value.resourceId, 'image.resourceId', 500), storageKey: opt(value.storageKey, 'image.storageKey', 1_000), caption: opt(value.caption, 'image.caption', 50_000), showCaption: bool(value.showCaption) }) };
313
+ if (value.type === 'video' || value.type === 'audio')
314
+ return { type: value.type, attrs: attr({ src: optUrl(value.src, `${value.type}.src`), title: opt(value.title, `${value.type}.title`, 500), poster: optUrl(value.poster, 'video.poster'), preload: opt(value.preload, 'audio.preload', 40), resourceId: opt(value.resourceId, `${value.type}.resourceId`, 500), storageKey: opt(value.storageKey, `${value.type}.storageKey`, 1_000), autoplay: bool(value.autoplay), loop: bool(value.loop), muted: bool(value.muted), playsInline: bool(value.playsInline) }) };
315
+ if (value.type === 'file')
316
+ return { type: 'file', attrs: attr({ id: opt(value.id, 'file.id', 500) ?? generateId(), src: optUrl(value.src, 'file.src'), name: opt(value.name, 'file.name', 500) ?? '未命名文件', type: opt(value.mimeType, 'file.mimeType', 200) ?? 'application/octet-stream', size: number(value.size, 'file.size'), resourceId: opt(value.resourceId, 'file.resourceId', 500), storageKey: opt(value.storageKey, 'file.storageKey', 1_000) }) };
317
+ if (value.type === 'table')
318
+ return tableSpec(value.rows);
319
+ if (value.type === 'mermaid')
320
+ return { type: 'mermaid', attrs: { code: text(value.code, 'mermaid.code', true) } };
321
+ if (value.type === 'mindgraph')
322
+ return { type: 'mindgraph', attrs: { markdown: text(value.markdown, 'mindgraph.markdown', true) } };
323
+ throw err('invalid_content', '不支持的块类型');
324
+ }
325
+ function markdown(document) {
326
+ return topNodes(document).map((block) => blockMarkdown(block, 0)).filter(Boolean).join('\n\n').trim();
327
+ }
328
+ function blockMarkdown(block, depth) {
329
+ const body = inlineMarkdown(block.content ?? []);
330
+ if (block.type === 'paragraph')
331
+ return body;
332
+ if (block.type === 'heading')
333
+ return `${'#'.repeat(headingLevel(block.attrs?.level))} ${body}`.trim();
334
+ if (block.type === 'blockquote')
335
+ return topNodes({ type: 'doc', content: block.content ?? [] }).map((child) => blockMarkdown(child, depth)).join('\n').split('\n').map((line) => `> ${line}`).join('\n');
336
+ if (block.type === 'codeBlock')
337
+ return `\`\`\`${str(block.attrs?.language) || str(block.attrs?.lang)}\n${plainText(block)}\n\`\`\``;
338
+ if (block.type === 'bulletList' || block.type === 'orderedList')
339
+ return listMarkdown(block, depth, block.type === 'bulletList' ? '-' : '1.');
340
+ if (block.type === 'taskList')
341
+ return taskMarkdown(block, depth);
342
+ if (block.type === 'horizontalRule')
343
+ return '---';
344
+ if (block.type === 'table')
345
+ return tableMarkdown(block);
346
+ if (block.type === 'image')
347
+ return imageMarkdown(block);
348
+ if (block.type === 'mermaid')
349
+ return fenced('mermaid', str(block.attrs?.code));
350
+ if (block.type === 'mindgraph')
351
+ return fenced('mindgraph', str(block.attrs?.markdown));
352
+ if (ATOMIC.has(block.type))
353
+ return placeholder(block.type, block, semantic(block));
354
+ return body || topNodes({ type: 'doc', content: block.content ?? [] }).map((child) => blockMarkdown(child, depth)).join('\n\n');
355
+ }
356
+ function inlineMarkdown(nodes) {
357
+ return nodes.map((item) => {
358
+ if (item.type === 'text')
359
+ return markedText(item);
360
+ if (item.type === 'hardBreak')
361
+ return '\n';
362
+ if (item.type === 'hashtag')
363
+ return `#${str(item.attrs?.fullPath) || str(item.attrs?.path) || str(item.attrs?.label) || str(item.attrs?.id)}`;
364
+ if (item.type === 'bidlink')
365
+ return str(item.attrs?.label) && str(item.attrs?.id) ? `[[${str(item.attrs?.label)}|${str(item.attrs?.id)}]]` : str(item.attrs?.label) || `[[${str(item.attrs?.id)}]]`;
366
+ if (item.type === 'inlineMath')
367
+ return `$${plainText(item) || str(item.attrs?.text) || str(item.attrs?.latex) || str(item.attrs?.code)}$`;
368
+ return semantic(item) || plainText(item);
369
+ }).join('');
370
+ }
371
+ function plainText(value) {
372
+ if (!value)
373
+ return '';
374
+ if (Array.isArray(value))
375
+ return value.map(plainText).filter(Boolean).join('\n');
376
+ if (value.type === 'text')
377
+ return str(value.text);
378
+ if (value.type === 'hardBreak')
379
+ return '\n';
380
+ const label = semantic(value);
381
+ if (label)
382
+ return label;
383
+ const joiner = value.type === 'paragraph' || value.type === 'heading' || value.type === 'codeBlock' ? '' : INLINE.has(value.type) ? '' : '\n';
384
+ return (value.content ?? []).map(plainText).filter(Boolean).join(joiner);
385
+ }
386
+ function contentText(value) {
387
+ return value.replace(/\r\n/g, '\n').split('\n').map((line) => line.trim()).filter(Boolean).filter((line) => !/^\[[^\]]+\]:\s*(?:<)?https?:\/\/\S+/i.test(line)).filter((line) => !/^(https?:\/\/|www\.)\S+$/i.test(line)).join(' ').replace(/\[\^[^\]]+\]/g, '').replace(/\[(\d{1,4})\]/g, '').replace(/\bhttps?:\/\/\S+/gi, '').replace(/\bwww\.\S+/gi, '').replace(/\s{2,}/g, ' ').trim().slice(0, 300);
388
+ }
389
+ function tableSpec(rows) {
390
+ if (!Array.isArray(rows) || !rows.length || rows.length > 200)
391
+ throw err('invalid_content', 'table.rows 数量无效');
392
+ const width = Math.max(...rows.map((row) => Array.isArray(row.cells) ? row.cells.length : 0));
393
+ if (!width || width > 50)
394
+ throw err('invalid_content', 'table.rows.cells 数量无效');
395
+ return { type: 'table', content: rows.map((row, rowIndex) => ({ type: 'tableRow', content: Array.from({ length: width }, (_, cellIndex) => ({ type: row.header === true || rowIndex === 0 ? 'tableHeader' : 'tableCell', content: [{ type: 'paragraph', content: textInline(text(row.cells[cellIndex] ?? '', 'table.cell')) }] })) })) };
396
+ }
397
+ function tableMarkdown(block) {
398
+ const rows = (block.content ?? []).filter((row) => row.type === 'tableRow').map((row) => row.content ?? []);
399
+ if (!rows.length)
400
+ return '';
401
+ const width = Math.max(...rows.map((row) => row.length));
402
+ const line = (cells) => `| ${Array.from({ length: width }, (_, index) => (cells[index]?.content ?? []).map((child) => blockMarkdown(child, 0)).join('<br>').replace(/\|/g, '\\|')).join(' | ')} |`;
403
+ return [line(rows[0] ?? []), `| ${Array.from({ length: width }, () => '---').join(' | ')} |`, ...rows.slice(1).map(line)].join('\n');
404
+ }
405
+ function listMarkdown(block, depth, marker) {
406
+ return (block.content ?? []).map((item) => {
407
+ const [first, ...rest] = item.content ?? [];
408
+ return [`${' '.repeat(depth)}${marker} ${first ? blockMarkdown(first, depth + 1).replace(/\n/g, '\n ') : ''}`, rest.map((child) => blockMarkdown(child, depth + 1)).filter(Boolean).join('\n')].filter(Boolean).join('\n');
409
+ }).join('\n');
410
+ }
411
+ function taskMarkdown(block, depth) {
412
+ return (block.content ?? []).map((item) => {
413
+ const [first, ...rest] = item.content ?? [];
414
+ return [`${' '.repeat(depth)}- [${item.attrs?.checked === true ? 'x' : ' '}] ${first ? blockMarkdown(first, depth + 1).replace(/\n/g, '\n ') : ''}`, rest.map((child) => blockMarkdown(child, depth + 1)).filter(Boolean).join('\n')].filter(Boolean).join('\n');
415
+ }).join('\n');
416
+ }
417
+ function imageMarkdown(block) {
418
+ const src = mediaSrc(block);
419
+ const alt = str(block.attrs?.alt) || str(block.attrs?.title) || inlineMarkdown(block.content ?? []);
420
+ const caption = str(block.attrs?.caption) || inlineMarkdown(block.content ?? []);
421
+ return [src ? `![${alt}](${src})` : placeholder('image', block), caption && caption !== alt ? caption : ''].filter(Boolean).join('\n');
422
+ }
423
+ function validateUrl(value, field) {
424
+ if (/^(https?:\/\/|file:\/\/|data:image\/|\/)/i.test(value))
425
+ return value;
426
+ throw err('invalid_content', `${field} 必须是安全 URL`);
427
+ }
428
+ function uniqueHash(blocks, hash, kind) {
429
+ if (!hash)
430
+ return null;
431
+ const matches = blocks.filter((block) => (kind === 'node' ? block.nodeHash : block.textHash) === hash);
432
+ if (matches.length > 1)
433
+ throw err('target_not_found', '目标 hash 匹配多个块,请使用 nodeRange 或 anchorId 重新定位');
434
+ return matches[0] ? { startIndex: matches[0].nodeIndex, endIndex: matches[0].nodeIndex + 1 } : null;
435
+ }
436
+ function targetMatches(target, blocks, start, end) {
437
+ return (!target.expectedNodeHash || target.expectedNodeHash === blockNodeHash(blocks, start, end)) && (!target.expectedTextHash || target.expectedTextHash === blockTextHash(blocks, start, end)) && (!target.expectedTextSnippet || blocks.slice(start, end).map((block) => block.text).join('\n\n').includes(target.expectedTextSnippet));
438
+ }
439
+ function assertNoRangeConflicts(items) {
440
+ const destructive = items.filter((item) => !['insertBlocksAfter', 'insertBlocksBefore'].includes(item.operation.op)).sort((a, b) => a.startIndex - b.startIndex);
441
+ for (let index = 1; index < destructive.length; index += 1)
442
+ if (destructive[index].startIndex < destructive[index - 1].endIndex)
443
+ throw err('invalid_plan', 'AI 编辑补丁包含重叠的破坏性操作');
444
+ }
445
+ function replaceText(block, operation) {
446
+ let changed = false;
447
+ const visit = (item) => {
448
+ if (changed && !operation.replaceAll)
449
+ return item;
450
+ if (item.type === 'text' && typeof item.text === 'string' && item.text.includes(operation.matchText)) {
451
+ changed = true;
452
+ return { ...item, text: operation.replaceAll ? item.text.split(operation.matchText).join(operation.replacementText) : item.text.replace(operation.matchText, operation.replacementText) };
453
+ }
454
+ return item.content ? { ...item, content: item.content.map(visit) } : item;
455
+ };
456
+ const next = visit(clone(block));
457
+ if (!changed)
458
+ throw err('target_conflict', '未能在目标块中找到待替换文本');
459
+ return next;
460
+ }
461
+ function inline(value) {
462
+ return value.inline?.length ? value.inline.map(inlineSpec) : textInline(text(value.text ?? '', `${value.type}.text`));
463
+ }
464
+ function inlineSpec(value) {
465
+ if (value.type === 'text')
466
+ return { type: 'text', text: text(value.text, 'inline.text', true) };
467
+ if (value.type === 'hardBreak')
468
+ return { type: 'hardBreak' };
469
+ if (value.type === 'inlineMath')
470
+ return { type: 'inlineMath', attrs: { text: text(value.text, 'inlineMath.text', true) } };
471
+ if (value.type === 'bidlink')
472
+ return { type: 'bidlink', attrs: attr({ id: text(value.id, 'bidlink.id'), label: opt(value.label, 'bidlink.label', 500), href: opt(value.href, 'bidlink.href', 1_000), type: opt(value.linkType, 'bidlink.linkType', 100) }) };
473
+ return { type: 'hashtag', attrs: attr({ id: opt(value.id, 'hashtag.id', 500), label: opt(value.label, 'hashtag.label', 500), path: opt(value.path, 'hashtag.path', 1_000), fullPath: opt(value.fullPath, 'hashtag.fullPath', 1_000) }) };
474
+ }
475
+ function textInline(value) {
476
+ return value ? value.split('\n').flatMap((part, index, parts) => [...(part ? [{ type: 'text', text: part }] : []), ...(index < parts.length - 1 ? [{ type: 'hardBreak' }] : [])]) : [];
477
+ }
478
+ function listItems(values) {
479
+ if (!Array.isArray(values) || !values.length || values.length > 200)
480
+ throw err('invalid_content', '列表块 items 数量无效');
481
+ return values.map((item, index) => text(item, `items[${index}]`));
482
+ }
483
+ function taskItems(values) {
484
+ if (!Array.isArray(values) || !values.length || values.length > 200)
485
+ throw err('invalid_content', '任务列表块 items 数量无效');
486
+ return values.map((item, index) => typeof item === 'string' ? { text: text(item, `items[${index}]`), checked: false } : { text: text(item?.text, `items[${index}].text`), checked: item?.checked === true });
487
+ }
488
+ function text(value, field, preserve = false) {
489
+ if (typeof value !== 'string')
490
+ throw err('invalid_content', `${field} 必须是字符串`);
491
+ const normalized = value.replace(/\r\n/g, '\n');
492
+ if (normalized.length > 50_000)
493
+ throw err('invalid_content', `${field} 超过 50000 字符限制`);
494
+ return preserve ? normalized : normalized.trim();
495
+ }
496
+ function opt(value, field, max) {
497
+ if (value == null)
498
+ return undefined;
499
+ if (typeof value !== 'string')
500
+ throw err('invalid_content', `${field} 必须是字符串`);
501
+ const normalized = value.replace(/\r\n/g, '\n').trim();
502
+ if (normalized.length > max)
503
+ throw err('invalid_content', `${field} 超过 ${max} 字符限制`);
504
+ return normalized || undefined;
505
+ }
506
+ function optUrl(value, field) {
507
+ const normalized = opt(value, field, 4_000);
508
+ return normalized ? validateUrl(normalized, field) : undefined;
509
+ }
510
+ function number(value, field) {
511
+ if (value == null)
512
+ return undefined;
513
+ const parsed = Number(value);
514
+ if (!Number.isFinite(parsed))
515
+ throw err('invalid_content', `${field} 必须是数字`);
516
+ return parsed;
517
+ }
518
+ function headingLevel(value) {
519
+ const parsed = Number(value);
520
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 6)
521
+ throw err('invalid_content', 'heading.level 必须是 1 到 6 的整数');
522
+ return parsed;
523
+ }
524
+ function lang(value) {
525
+ return typeof value === 'string' ? value.trim().replace(/[^\w.+-]/g, '').slice(0, 40) || undefined : undefined;
526
+ }
527
+ function bool(value) {
528
+ return typeof value === 'boolean' ? value : undefined;
529
+ }
530
+ function patchAttrs(type, attrs) {
531
+ return Object.fromEntries((PATCHABLE_ATTRS[type] ?? []).flatMap((key) => Object.hasOwn(attrs, key) ? [[key, safe(attrs[key])]] : []).filter(([, value]) => value !== undefined));
532
+ }
533
+ function safeAttrs(block) {
534
+ const attrs = block.attrs ?? {};
535
+ const result = Object.fromEntries((SAFE_ATTRS[block.type] ?? ['anchorKey', 'id', 'uniqueId']).flatMap((key) => Object.hasOwn(attrs, key) ? [[key, safe(attrs[key])]] : []).filter(([, value]) => value !== undefined));
536
+ return Object.keys(result).length ? result : undefined;
537
+ }
538
+ function safe(value) {
539
+ if (value == null)
540
+ return undefined;
541
+ if (typeof value === 'boolean')
542
+ return value;
543
+ if (typeof value === 'number')
544
+ return Number.isFinite(value) ? value : undefined;
545
+ if (typeof value !== 'string')
546
+ return undefined;
547
+ const normalized = value.replace(/\s+/g, ' ').trim();
548
+ return normalized ? normalized.slice(0, 2_000) : undefined;
549
+ }
550
+ function markedText(item) {
551
+ let value = str(item.text);
552
+ for (const mark of Array.isArray(item.marks) ? item.marks : []) {
553
+ if (mark.type === 'bold')
554
+ value = `**${value}**`;
555
+ if (mark.type === 'italic')
556
+ value = `*${value}*`;
557
+ if (mark.type === 'strike')
558
+ value = `~~${value}~~`;
559
+ if (mark.type === 'code')
560
+ value = `\`${value.replace(/`/g, '\\`')}\``;
561
+ const attrs = mark.attrs;
562
+ if (mark.type === 'link' && str(attrs?.href))
563
+ value = `[${value}](${str(attrs?.href)})`;
564
+ }
565
+ return value;
566
+ }
567
+ function semantic(item) {
568
+ if (item.type === 'hashtag')
569
+ return str(item.attrs?.fullPath) || str(item.attrs?.path) || str(item.attrs?.label) || str(item.attrs?.id);
570
+ if (item.type === 'bidlink')
571
+ return str(item.attrs?.label) || str(item.attrs?.id);
572
+ if (item.type === 'image')
573
+ return str(item.attrs?.caption) || str(item.attrs?.alt) || str(item.attrs?.title) || inlineMarkdown(item.content ?? []);
574
+ if (item.type === 'video' || item.type === 'audio')
575
+ return str(item.attrs?.title) || mediaSrc(item);
576
+ if (item.type === 'file')
577
+ return str(item.attrs?.name) || mediaSrc(item);
578
+ if (item.type === 'mermaid')
579
+ return str(item.attrs?.code);
580
+ if (item.type === 'mindgraph')
581
+ return str(item.attrs?.markdown);
582
+ return '';
583
+ }
584
+ function metadataFrom(document) {
585
+ const images = new Set();
586
+ const bidLinks = new Map();
587
+ const visit = (item) => {
588
+ if ((item.type === 'image' || item.type === 'video') && /^https?:\/\//i.test(mediaSrc(item)))
589
+ images.add(stripUrl(mediaSrc(item)));
590
+ if (item.type === 'bidlink' && str(item.attrs?.id)) {
591
+ const id = str(item.attrs?.id);
592
+ const type = str(item.attrs?.type) || 'reference';
593
+ bidLinks.set(`${id}::${type}`, { id, type: type === 'note' ? 'reference' : type });
594
+ }
595
+ for (const child of item.content ?? [])
596
+ visit(child);
597
+ };
598
+ visit(document);
599
+ return { images: Array.from(images), bidLinks: Array.from(bidLinks.values()) };
600
+ }
601
+ function mode(type) {
602
+ return ATOMIC.has(type) ? 'atomicBlock' : CONTAINER.has(type) ? 'container' : INLINE.has(type) ? 'inlineAtom' : SAFE_ATTRS[type] || ['listItem', 'taskItem', 'tableRow', 'tableCell', 'tableHeader'].includes(type) ? 'text' : 'unsupported';
603
+ }
604
+ function withStableAnchors(document, createAnchorId) {
605
+ if (!createAnchorId)
606
+ return document;
607
+ let changed = false;
608
+ const content = topNodes(document).map((block, index) => {
609
+ if (anchor(block))
610
+ return block;
611
+ const anchorId = createAnchorId(block, index).trim();
612
+ if (!anchorId)
613
+ return block;
614
+ changed = true;
615
+ return { ...block, attrs: { ...(block.attrs ?? {}), anchorKey: anchorId } };
616
+ });
617
+ return changed ? { ...document, content } : document;
618
+ }
619
+ function chunks(blocks) {
620
+ return blocks.map((block, index) => ({ index, nodeRange: block.nodeRange, anchorIds: [block.anchorId], textPreview: block.textPreview, charCount: block.charCount, estimatedTokens: Math.ceil(block.charCount / 2.5) }));
621
+ }
622
+ function hashNode(item) {
623
+ const value = { type: item.type };
624
+ const attrs = item.attrs ? Object.fromEntries(Object.entries(item.attrs).filter(([key]) => !HASH_IGNORED_ATTRS.has(key))) : {};
625
+ if (Object.keys(attrs).length)
626
+ value.attrs = attrs;
627
+ if (typeof item.text === 'string')
628
+ value.text = item.text;
629
+ if (item.marks?.length)
630
+ value.marks = item.marks;
631
+ if (item.content?.length)
632
+ value.content = item.content.map(hashNodeValue);
633
+ return hashStable(value);
634
+ }
635
+ function hashNodeValue(item) {
636
+ const value = { type: item.type };
637
+ const attrs = item.attrs ? Object.fromEntries(Object.entries(item.attrs).filter(([key]) => !HASH_IGNORED_ATTRS.has(key))) : {};
638
+ if (Object.keys(attrs).length)
639
+ value.attrs = attrs;
640
+ if (typeof item.text === 'string')
641
+ value.text = item.text;
642
+ if (item.marks?.length)
643
+ value.marks = item.marks;
644
+ if (item.content?.length)
645
+ value.content = item.content.map(hashNodeValue);
646
+ return value;
647
+ }
648
+ function hashStable(value) {
649
+ return hashString(stableStringify(value));
650
+ }
651
+ function hashString(value) {
652
+ let hash = 0x811c9dc5;
653
+ for (let index = 0; index < value.length; index += 1) {
654
+ hash ^= value.charCodeAt(index);
655
+ hash = Math.imul(hash, 0x01000193);
656
+ }
657
+ return (hash >>> 0).toString(36);
658
+ }
659
+ function stableStringify(value) {
660
+ return JSON.stringify(sortJson(value ?? null));
661
+ }
662
+ function sortJson(value) {
663
+ if (value == null || typeof value !== 'object')
664
+ return value;
665
+ if (Array.isArray(value))
666
+ return value.map(sortJson);
667
+ return Object.fromEntries(Object.keys(value).sort().map((key) => [key, sortJson(value[key])]));
668
+ }
669
+ function anchor(item) {
670
+ for (const key of ANCHOR_KEYS)
671
+ if (str(item?.attrs?.[key]))
672
+ return str(item?.attrs?.[key]);
673
+ return null;
674
+ }
675
+ function node(value) {
676
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value) && typeof value.type === 'string');
677
+ }
678
+ function clone(value) {
679
+ return JSON.parse(JSON.stringify(value));
680
+ }
681
+ function withoutContent(value) {
682
+ const next = clone(value);
683
+ delete next.content;
684
+ return next;
685
+ }
686
+ function marks(value) {
687
+ if (!Array.isArray(value))
688
+ return undefined;
689
+ const normalized = value.filter((mark) => Boolean(mark && typeof mark === 'object' && !Array.isArray(mark) && typeof mark.type === 'string'));
690
+ return normalized.length ? normalized : undefined;
691
+ }
692
+ function attr(values) {
693
+ return Object.fromEntries(Object.entries(values).filter(([, value]) => value !== undefined && value !== null && value !== ''));
694
+ }
695
+ function num(value) {
696
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
697
+ }
698
+ function str(value) {
699
+ return typeof value === 'string' ? value.trim() : '';
700
+ }
701
+ function mediaSrc(item) {
702
+ const raw = item.attrs?.src;
703
+ return typeof raw === 'string' ? raw.trim() : raw && typeof raw === 'object' && !Array.isArray(raw) && typeof raw.url === 'string' ? String(raw.url).trim() : '';
704
+ }
705
+ function placeholder(type, item, label = '') {
706
+ const attrs = [['id', str(item.attrs?.id)], ['resourceId', str(item.attrs?.resourceId)], ['url', str(item.attrs?.url) || str(item.attrs?.src)], ['title', str(item.attrs?.title) || str(item.attrs?.name)]].filter(([, value]) => value).map(([key, value]) => `${key}="${String(value).replace(/"/g, '\\"').slice(0, 240)}"`).join(' ');
707
+ return `[${type}${attrs ? ` ${attrs}` : ''}]${label ? ` ${label}` : ''}`;
708
+ }
709
+ function fenced(language, value) {
710
+ return value ? `\`\`\`${language}\n${value}\n\`\`\`` : `[${language}]`;
711
+ }
712
+ function compactAttrs(attrs, type) {
713
+ if (!attrs)
714
+ return undefined;
715
+ const result = Object.fromEntries(Object.entries(attrs).filter(([key]) => key !== 'anchorKey' && (mode(type) !== 'text' || !['id', 'uniqueId'].includes(key))));
716
+ return Object.keys(result).length ? result : undefined;
717
+ }
718
+ function token(value) {
719
+ return /^[A-Za-z0-9_.:/@-]+$/.test(value) ? value : JSON.stringify(value);
720
+ }
721
+ function escapeComment(value) {
722
+ return value.replace(/-->/g, '--&gt;').replace(/\s+/g, ' ').trim();
723
+ }
724
+ function describeTarget(target) {
725
+ return target.anchorId ?? target.anchorIds?.join(',') ?? target.nodeRange ?? (typeof target.nodeIndex === 'number' ? String(target.nodeIndex) : target.expectedNodeHash ?? target.expectedTextHash ?? target.textMatch ?? 'unknown');
726
+ }
727
+ function stripUrl(value) {
728
+ try {
729
+ const url = new URL(value);
730
+ url.search = '';
731
+ url.hash = '';
732
+ return url.toString();
733
+ }
734
+ catch {
735
+ return value;
736
+ }
737
+ }
738
+ function err(code, message, details) {
739
+ return new NoteContentEditError(code, message, details);
740
+ }