@4399ywkf/editor 0.1.2 → 0.3.0

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.
@@ -0,0 +1,774 @@
1
+ // src/doc-runtime/litexml.ts
2
+ var NODE_TAG = {
3
+ paragraph: "p",
4
+ bulletList: "ul",
5
+ orderedList: "ol",
6
+ listItem: "li",
7
+ taskList: "tasks",
8
+ taskItem: "task",
9
+ blockquote: "quote",
10
+ codeBlock: "code",
11
+ horizontalRule: "hr",
12
+ table: "table",
13
+ tableRow: "tr",
14
+ tableCell: "td",
15
+ tableHeader: "th",
16
+ image: "img",
17
+ video: "video",
18
+ audio: "audio",
19
+ fileAttachment: "file",
20
+ columns: "cols",
21
+ column: "col",
22
+ tocNode: "toc",
23
+ mediaUploadPlaceholder: "uploading",
24
+ imageUpload: "uploading"
25
+ };
26
+ var TYPE_BY_TAG = Object.fromEntries(
27
+ Object.entries(NODE_TAG).map(([type, tag]) => [tag, type])
28
+ );
29
+ var VOID_TAGS = /* @__PURE__ */ new Set([
30
+ "hr",
31
+ "img",
32
+ "video",
33
+ "audio",
34
+ "file",
35
+ "toc",
36
+ "uploading"
37
+ ]);
38
+ var READONLY_TAGS = /* @__PURE__ */ new Set(["toc", "uploading"]);
39
+ var ATTR_ALIAS = {
40
+ textAlign: "align",
41
+ nodeTextAlign: "align",
42
+ nodeVerticalAlign: "valign",
43
+ backgroundColor: "bg",
44
+ language: "lang"
45
+ };
46
+ var ATTR_UNALIAS = {
47
+ align: "textAlign",
48
+ valign: "nodeVerticalAlign",
49
+ bg: "backgroundColor",
50
+ lang: "language"
51
+ };
52
+ var NOISE_ATTRS = /* @__PURE__ */ new Set(["data-toc-id"]);
53
+ var NUM_ATTRS = /* @__PURE__ */ new Set(["level", "indent", "colspan", "rowspan"]);
54
+ var BOOL_ATTRS = /* @__PURE__ */ new Set(["checked"]);
55
+ var MARK_TAG = {
56
+ bold: "b",
57
+ italic: "i",
58
+ underline: "u",
59
+ strike: "s",
60
+ code: "c",
61
+ superscript: "sup",
62
+ subscript: "sub"
63
+ };
64
+ var MARK_BY_TAG = Object.fromEntries(
65
+ Object.entries(MARK_TAG).map(([mark, tag]) => [tag, mark])
66
+ );
67
+ var FORMATTABLE = Object.keys(MARK_TAG);
68
+ var esc = (s) => String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
69
+ function attrsToXml(node) {
70
+ const out = [];
71
+ const attrs = node.attrs ?? {};
72
+ if (attrs.id) out.push(` id="${esc(attrs.id)}"`);
73
+ for (const [k, v] of Object.entries(attrs)) {
74
+ if (k === "id" || v === null || v === void 0 || v === "" || v === 0 || v === false) continue;
75
+ if (NOISE_ATTRS.has(k)) continue;
76
+ if ((k === "colspan" || k === "rowspan") && v === 1) continue;
77
+ const name = ATTR_ALIAS[k] ?? k;
78
+ if (name === "id") continue;
79
+ out.push(` ${name}="${esc(Array.isArray(v) ? v.join(",") : v)}"`);
80
+ }
81
+ return out.join("");
82
+ }
83
+ function inlineToXml(nodes = []) {
84
+ return nodes.map((n) => {
85
+ if (n.type === "text") {
86
+ let out = esc(n.text ?? "");
87
+ for (const m of n.marks ?? []) {
88
+ const tag = MARK_TAG[m.type];
89
+ if (tag) out = `<${tag}>${out}</${tag}>`;
90
+ else if (m.type === "link") out = `<a href="${esc(m.attrs?.href ?? "")}">${out}</a>`;
91
+ else if (m.type === "highlight")
92
+ out = `<mark${m.attrs?.color ? ` color="${esc(m.attrs.color)}"` : ""}>${out}</mark>`;
93
+ else if (m.type === "textStyle" && m.attrs?.color)
94
+ out = `<color v="${esc(m.attrs.color)}">${out}</color>`;
95
+ }
96
+ return out;
97
+ }
98
+ if (n.type === "hardBreak") return "<br/>";
99
+ return `<node type="${esc(n.type)}"${attrsToXml(n)}/>`;
100
+ }).join("");
101
+ }
102
+ function isBlock(n) {
103
+ return Boolean(n.type && NODE_TAG[n.type]) || n.type === "heading";
104
+ }
105
+ function nodeToXml(node, depth = 0) {
106
+ const pad = " ".repeat(depth);
107
+ const attrs = attrsToXml(node);
108
+ if (node.type === "heading") {
109
+ const lv = node.attrs?.level ?? 1;
110
+ const a = attrs.replace(/ level="\d+"/, "");
111
+ return `${pad}<h${lv}${a}>${inlineToXml(node.content)}</h${lv}>`;
112
+ }
113
+ const tag = node.type ? NODE_TAG[node.type] : void 0;
114
+ if (!tag) return `${pad}<node type="${esc(node.type)}"${attrs}/>`;
115
+ if (VOID_TAGS.has(tag)) return `${pad}<${tag}${attrs}/>`;
116
+ const kids = node.content ?? [];
117
+ if (kids.some(isBlock)) {
118
+ const inner = kids.map((c) => nodeToXml(c, depth + 1)).join("\n");
119
+ return `${pad}<${tag}${attrs}>
120
+ ${inner}
121
+ ${pad}</${tag}>`;
122
+ }
123
+ return `${pad}<${tag}${attrs}>${inlineToXml(kids)}</${tag}>`;
124
+ }
125
+ function docToXml(doc) {
126
+ return (doc.content ?? []).map((n) => nodeToXml(n, 0)).join("\n");
127
+ }
128
+ function coerce(name, raw) {
129
+ if (NUM_ATTRS.has(name)) {
130
+ const n = Number(raw);
131
+ return Number.isNaN(n) ? raw : n;
132
+ }
133
+ if (BOOL_ATTRS.has(name)) return raw === "true" || raw === "";
134
+ if (name === "colwidth")
135
+ return raw.split(",").map(Number).filter((n) => !Number.isNaN(n));
136
+ return raw;
137
+ }
138
+ function childElements(el) {
139
+ const out = [];
140
+ const kids = el.childNodes;
141
+ for (let i = 0; i < kids.length; i++) {
142
+ const n = kids.item(i);
143
+ if (n && n.nodeType === 1) out.push(n);
144
+ }
145
+ return out;
146
+ }
147
+ function xmlAttrsToPm(el, skip = /* @__PURE__ */ new Set()) {
148
+ const attrs = {};
149
+ const list = el.attributes;
150
+ for (let i = 0; i < list.length; i++) {
151
+ const a = list.item(i);
152
+ if (!a) continue;
153
+ const name = ATTR_UNALIAS[a.name] ?? a.name;
154
+ if (skip.has(name)) continue;
155
+ attrs[name] = coerce(name, a.value);
156
+ }
157
+ return attrs;
158
+ }
159
+ function xmlInlineToPm(el) {
160
+ const out = [];
161
+ const kids = el.childNodes;
162
+ for (let i = 0; i < kids.length; i++) {
163
+ const child = kids.item(i);
164
+ if (!child) continue;
165
+ if (child.nodeType === 3) {
166
+ if (child.nodeValue) out.push({ type: "text", text: child.nodeValue });
167
+ continue;
168
+ }
169
+ if (child.nodeType !== 1) continue;
170
+ const childEl = child;
171
+ const tag = childEl.tagName.toLowerCase();
172
+ if (tag === "br") {
173
+ out.push({ type: "hardBreak" });
174
+ continue;
175
+ }
176
+ if (tag === "node") {
177
+ const type = childEl.getAttribute("type");
178
+ if (!type) throw new Error("<node> 必须带 type 属性");
179
+ out.push({ type, attrs: xmlAttrsToPm(childEl, /* @__PURE__ */ new Set(["type"])) });
180
+ continue;
181
+ }
182
+ let mark = null;
183
+ if (MARK_BY_TAG[tag]) mark = { type: MARK_BY_TAG[tag] };
184
+ else if (tag === "a")
185
+ mark = { type: "link", attrs: { href: childEl.getAttribute("href") ?? "" } };
186
+ else if (tag === "mark")
187
+ mark = { type: "highlight", attrs: { color: childEl.getAttribute("color") ?? null } };
188
+ else if (tag === "color")
189
+ mark = { type: "textStyle", attrs: { color: childEl.getAttribute("v") ?? null } };
190
+ const inner = xmlInlineToPm(childEl);
191
+ if (!mark) {
192
+ out.push(...inner);
193
+ continue;
194
+ }
195
+ for (const t of inner) {
196
+ if (t.type === "text") t.marks = [...t.marks ?? [], mark];
197
+ out.push(t);
198
+ }
199
+ }
200
+ return out;
201
+ }
202
+ function xmlElToPm(el) {
203
+ const tag = el.tagName.toLowerCase();
204
+ if (tag === "node") {
205
+ const type2 = el.getAttribute("type");
206
+ if (!type2) throw new Error("<node> 必须带 type 属性");
207
+ return { type: type2, attrs: xmlAttrsToPm(el, /* @__PURE__ */ new Set(["type"])) };
208
+ }
209
+ const hm = /^h([1-6])$/.exec(tag);
210
+ if (hm) {
211
+ return {
212
+ type: "heading",
213
+ attrs: { level: Number(hm[1]), ...xmlAttrsToPm(el) },
214
+ content: xmlInlineToPm(el)
215
+ };
216
+ }
217
+ const type = TYPE_BY_TAG[tag];
218
+ if (!type) {
219
+ throw new Error(
220
+ `未知标签 <${tag}>。可用:${Object.values(NODE_TAG).join(" ")} h1-h6 node,内联:${Object.values(MARK_TAG).join(" ")} a mark color br`
221
+ );
222
+ }
223
+ if (READONLY_TAGS.has(tag)) {
224
+ throw new Error(`<${tag}> 由扩展自动维护,不能直接写入`);
225
+ }
226
+ const attrs = xmlAttrsToPm(el);
227
+ if (VOID_TAGS.has(tag)) return { type, attrs };
228
+ const blockKids = childElements(el).filter((c) => {
229
+ const t = c.tagName.toLowerCase();
230
+ return Boolean(TYPE_BY_TAG[t]) || /^h[1-6]$/.test(t);
231
+ });
232
+ if (blockKids.length > 0) {
233
+ return { type, attrs, content: blockKids.map(xmlElToPm) };
234
+ }
235
+ const inline = xmlInlineToPm(el);
236
+ return inline.length ? { type, attrs, content: inline } : { type, attrs };
237
+ }
238
+ function parseXmlFragment(xml, opts = {}) {
239
+ const g = globalThis;
240
+ const Parser = opts.DOMParser ?? g.DOMParser;
241
+ if (!Parser) {
242
+ throw new Error(
243
+ "没有可用的 XML 解析器:浏览器外请通过 opts.DOMParser 注入(如 @xmldom/xmldom)"
244
+ );
245
+ }
246
+ let doc;
247
+ try {
248
+ doc = new Parser().parseFromString(`<root>${xml}</root>`, "text/xml");
249
+ } catch (e) {
250
+ throw new Error(`XML 解析失败:${String(e.message ?? e).slice(0, 200)}`);
251
+ }
252
+ const err = doc.getElementsByTagName("parsererror")[0];
253
+ if (err) throw new Error(`XML 解析失败:${err.textContent?.slice(0, 200)}`);
254
+ const root = doc.documentElement;
255
+ if (!root) throw new Error("XML 解析失败:没有根元素");
256
+ return childElements(root).map(xmlElToPm);
257
+ }
258
+ function mergeParsedNode(originalAttrs, parsed) {
259
+ const incoming = Object.fromEntries(
260
+ Object.entries(parsed.attrs ?? {}).filter(([, v]) => v !== null && v !== void 0)
261
+ );
262
+ return { ...parsed, attrs: { ...originalAttrs, ...incoming } };
263
+ }
264
+
265
+ // src/doc-runtime/operations.ts
266
+ function findById(editor, id) {
267
+ let hit = null;
268
+ editor.state.doc.descendants((node, pos) => {
269
+ if (hit) return false;
270
+ if (node.attrs?.id === id) {
271
+ hit = { node, pos };
272
+ return false;
273
+ }
274
+ return true;
275
+ });
276
+ return hit;
277
+ }
278
+ function findCell(editor, tableId, row, col) {
279
+ const t = findById(editor, tableId);
280
+ if (!t) throw new Error(`找不到表格 id="${tableId}"`);
281
+ if (t.node.type.name !== "table")
282
+ throw new Error(`id="${tableId}" 不是表格,是 ${t.node.type.name}`);
283
+ if (row < 0 || row >= t.node.childCount)
284
+ throw new Error(`表格只有 ${t.node.childCount} 行,取不到第 ${row} 行(0-based)`);
285
+ const rowNode = t.node.child(row);
286
+ if (col < 0 || col >= rowNode.childCount)
287
+ throw new Error(`第 ${row} 行只有 ${rowNode.childCount} 列,取不到第 ${col} 列(0-based)`);
288
+ const cellNode = rowNode.child(col);
289
+ let pos = t.pos + 1;
290
+ for (let r = 0; r < row; r++) pos += t.node.child(r).nodeSize;
291
+ pos += 1;
292
+ for (let c = 0; c < col; c++) pos += rowNode.child(c).nodeSize;
293
+ return { node: cellNode, pos, table: t };
294
+ }
295
+ function textOffsetMap(node, pos) {
296
+ const segs = [];
297
+ let textOffset = 0;
298
+ node.descendants((child, childPos) => {
299
+ if (child.isText) {
300
+ const len = child.text?.length ?? 0;
301
+ segs.push({ textStart: textOffset, from: pos + 1 + childPos, len });
302
+ textOffset += len;
303
+ }
304
+ return true;
305
+ });
306
+ return {
307
+ start(offset) {
308
+ for (const s of segs) {
309
+ if (offset < s.textStart + s.len) return s.from + (offset - s.textStart);
310
+ }
311
+ const last = segs[segs.length - 1];
312
+ return last ? last.from + last.len : pos + 1;
313
+ },
314
+ end(offset) {
315
+ for (const s of segs) {
316
+ if (offset <= s.textStart + s.len) return s.from + (offset - s.textStart);
317
+ }
318
+ return pos + 1 + node.content.size;
319
+ }
320
+ };
321
+ }
322
+ function applyOperations(editor, operations, parseOpts = {}) {
323
+ const results = [];
324
+ const touchedIds = [];
325
+ for (const op of operations) {
326
+ try {
327
+ if (op.action === "remove") {
328
+ const hit = findById(editor, op.id);
329
+ if (!hit) throw new Error(`找不到 id="${op.id}"`);
330
+ editor.chain().command(({ tr }) => {
331
+ tr.delete(hit.pos, hit.pos + hit.node.nodeSize);
332
+ return true;
333
+ }).run();
334
+ results.push({ action: "remove", success: true, id: op.id });
335
+ continue;
336
+ }
337
+ if (op.action === "insert") {
338
+ const anchorId = op.afterId ?? op.beforeId;
339
+ if (!anchorId) throw new Error("insert 必须给 afterId 或 beforeId");
340
+ const hit = findById(editor, anchorId);
341
+ if (!hit) throw new Error(`找不到锚点 id="${anchorId}"`);
342
+ const nodes = parseXmlFragment(op.litexml, parseOpts);
343
+ const at = op.afterId ? hit.pos + hit.node.nodeSize : hit.pos;
344
+ editor.chain().insertContentAt(at, nodes).run();
345
+ results.push({ action: "insert", success: true, anchor: anchorId, count: nodes.length });
346
+ continue;
347
+ }
348
+ if (op.action === "modify") {
349
+ const xmls = Array.isArray(op.litexml) ? op.litexml : [op.litexml];
350
+ const modifiedIds = [];
351
+ for (const xml of xmls) {
352
+ for (const parsed of parseXmlFragment(xml, parseOpts)) {
353
+ const id = parsed.attrs?.id;
354
+ if (!id) throw new Error("modify 的 litexml 必须带 id 属性");
355
+ const hit = findById(editor, id);
356
+ if (!hit) throw new Error(`找不到 id="${id}"`);
357
+ editor.chain().command(({ tr, state }) => {
358
+ const merged = mergeParsedNode(hit.node.attrs, parsed);
359
+ tr.replaceWith(
360
+ hit.pos,
361
+ hit.pos + hit.node.nodeSize,
362
+ state.schema.nodeFromJSON(merged)
363
+ );
364
+ return true;
365
+ }).run();
366
+ modifiedIds.push(id);
367
+ touchedIds.push(id);
368
+ }
369
+ }
370
+ results.push({ action: "modify", success: true, count: xmls.length, modifiedIds });
371
+ continue;
372
+ }
373
+ throw new Error(`未知 action: ${op.action}`);
374
+ } catch (e) {
375
+ results.push({
376
+ action: op.action,
377
+ success: false,
378
+ error: String(e.message ?? e)
379
+ });
380
+ }
381
+ }
382
+ return {
383
+ results,
384
+ successCount: results.filter((r) => r.success).length,
385
+ totalCount: results.length,
386
+ touchedIds
387
+ };
388
+ }
389
+ function replaceText(editor, { searchText, newText, nodeIds, replaceAll = false }) {
390
+ if (!searchText) throw new Error("searchText 不能为空");
391
+ const targets = [];
392
+ editor.state.doc.descendants((node, pos) => {
393
+ if (!node.isTextblock) return true;
394
+ if (nodeIds?.length && !nodeIds.includes(node.attrs?.id)) return true;
395
+ targets.push({ node, pos, id: node.attrs?.id });
396
+ return true;
397
+ });
398
+ const replaceInBlock = (t) => {
399
+ const text = t.node.textContent;
400
+ const offsets = [];
401
+ let i = text.indexOf(searchText);
402
+ while (i !== -1) {
403
+ offsets.push(i);
404
+ if (!replaceAll) break;
405
+ i = text.indexOf(searchText, i + searchText.length);
406
+ }
407
+ if (!offsets.length) return 0;
408
+ const map = textOffsetMap(t.node, t.pos);
409
+ for (const off of offsets.reverse()) {
410
+ const from = map.start(off);
411
+ const to = map.end(off + searchText.length);
412
+ editor.chain().command(({ tr }) => {
413
+ tr.insertText(newText, from, to);
414
+ return true;
415
+ }).run();
416
+ }
417
+ return offsets.length;
418
+ };
419
+ if (!replaceAll) {
420
+ const first = targets.find((t) => t.node.textContent.includes(searchText));
421
+ if (!first) return { replacementCount: 0, modifiedNodeIds: [] };
422
+ const n = replaceInBlock(first);
423
+ return { replacementCount: n, modifiedNodeIds: first.id ? [first.id] : [] };
424
+ }
425
+ const modified = [];
426
+ let count = 0;
427
+ for (const t of [...targets].reverse()) {
428
+ if (!t.node.textContent.includes(searchText)) continue;
429
+ count += replaceInBlock(t);
430
+ if (t.id) modified.push(t.id);
431
+ }
432
+ modified.reverse();
433
+ return { replacementCount: count, modifiedNodeIds: modified };
434
+ }
435
+ function formatText(editor, { nodeId, searchText, marks, remove = false, occurrence = 1 }) {
436
+ const bad = (marks ?? []).filter((m) => !FORMATTABLE.includes(m));
437
+ if (bad.length) throw new Error(`不支持的标记 ${bad.join(",")};可用:${FORMATTABLE.join(" / ")}`);
438
+ const hit = findById(editor, nodeId);
439
+ if (!hit) throw new Error(`找不到 id="${nodeId}"`);
440
+ const text = hit.node.textContent;
441
+ let idx = -1;
442
+ for (let n = 0; n < occurrence; n++) {
443
+ idx = text.indexOf(searchText, idx + 1);
444
+ if (idx === -1) break;
445
+ }
446
+ if (idx === -1)
447
+ throw new Error(
448
+ `块 ${nodeId} 里找不到第 ${occurrence} 处 "${searchText}"(内容:${text.slice(0, 80)})`
449
+ );
450
+ const map = textOffsetMap(hit.node, hit.pos);
451
+ const from = map.start(idx);
452
+ const to = map.end(idx + searchText.length);
453
+ let chain = editor.chain().setTextSelection({ from, to });
454
+ for (const m of marks) chain = remove ? chain.unsetMark(m) : chain.setMark(m);
455
+ chain.run();
456
+ return { nodeId, marks, removed: remove, matched: searchText };
457
+ }
458
+ var TABLE_COMMANDS = {
459
+ insert_row_after: "addRowAfter",
460
+ insert_row_before: "addRowBefore",
461
+ insert_col_after: "addColumnAfter",
462
+ insert_col_before: "addColumnBefore",
463
+ delete_row: "deleteRow",
464
+ delete_col: "deleteColumn",
465
+ toggle_header_row: "toggleHeaderRow",
466
+ merge_cells: "mergeCells",
467
+ split_cell: "splitCell"
468
+ };
469
+ function tableEdit(editor, { tableId, action, row, col, values }) {
470
+ const t = findById(editor, tableId);
471
+ if (!t) throw new Error(`找不到表格 id="${tableId}"`);
472
+ if (action === "set_cells") {
473
+ if (!Array.isArray(values)) throw new Error("set_cells 需要 values: [{row, col, text}]");
474
+ const sorted = [...values].sort((a, b) => b.row - a.row || b.col - a.col);
475
+ for (const v of sorted) {
476
+ const cell2 = findCell(editor, tableId, v.row, v.col);
477
+ const para = cell2.node.firstChild;
478
+ const from = cell2.pos + 1 + 1;
479
+ const to = from + (para ? para.content.size : 0);
480
+ editor.chain().command(({ tr }) => {
481
+ tr.insertText(String(v.text), from, to);
482
+ return true;
483
+ }).run();
484
+ }
485
+ return { tableId, updated: values.length };
486
+ }
487
+ if (row == null || col == null) {
488
+ throw new Error(
489
+ `${action} 必须显式指定 row 和 col(0-based,表头是第 0 行)作为锚点单元格。该表格共 ${t.node.childCount} 行。例如要在最后一行后面加一行:row=${t.node.childCount - 1}, col=0`
490
+ );
491
+ }
492
+ const cmd = TABLE_COMMANDS[action];
493
+ if (!cmd)
494
+ throw new Error(
495
+ `未知 action: ${action};可用 ${Object.keys(TABLE_COMMANDS).join(" / ")} / set_cells`
496
+ );
497
+ const cell = findCell(editor, tableId, row, col);
498
+ editor.chain().setTextSelection(cell.pos + 2).run();
499
+ const chain = editor.chain().focus();
500
+ const ok = chain[cmd]().run();
501
+ return { tableId, action, ok };
502
+ }
503
+ function findTextInDoc(editor, query, limit = 20) {
504
+ if (!query) throw new Error("query 不能为空");
505
+ const hits = [];
506
+ editor.state.doc.descendants((node) => {
507
+ if (hits.length >= limit) return false;
508
+ if (!node.isTextblock) return true;
509
+ const text = node.textContent;
510
+ let i = text.indexOf(query);
511
+ while (i !== -1 && hits.length < limit) {
512
+ hits.push({
513
+ nodeId: node.attrs?.id ?? null,
514
+ nodeType: node.type.name,
515
+ contextBefore: text.slice(Math.max(0, i - 30), i),
516
+ match: query,
517
+ contextAfter: text.slice(i + query.length, i + query.length + 30)
518
+ });
519
+ i = text.indexOf(query, i + query.length);
520
+ }
521
+ return true;
522
+ });
523
+ return { hits, total: hits.length };
524
+ }
525
+ function getSelection(editor) {
526
+ const { from, to, empty } = editor.state.selection;
527
+ if (empty) return { selection: null, note: "本轮无选区,不要从历史推断" };
528
+ const blocks = [];
529
+ editor.state.doc.nodesBetween(from, to, (node) => {
530
+ if (node.attrs?.id) blocks.push({ id: node.attrs.id, type: node.type.name });
531
+ return true;
532
+ });
533
+ return { selection: { text: editor.state.doc.textBetween(from, to, "\n"), blocks } };
534
+ }
535
+ function docSchema(editor) {
536
+ const idTypes = editor.extensionManager.extensions.find((e) => e.name === "uniqueID")?.options?.types ?? [];
537
+ return {
538
+ nodeTypes: Object.keys(editor.schema.nodes),
539
+ markTypes: Object.keys(editor.schema.marks),
540
+ litexmlTags: NODE_TAG,
541
+ readonlyTags: [...READONLY_TAGS],
542
+ idBearingTypes: idTypes,
543
+ note: "只有 idBearingTypes 里的节点带稳定 id,可被直接寻址。表格单元格的结构性改动用 doc_table_edit 的 (tableId,row,col) 定位。"
544
+ };
545
+ }
546
+ function docStats(editor) {
547
+ const nodes = {};
548
+ const marks = {};
549
+ const headings = {};
550
+ let textLength = 0;
551
+ editor.state.doc.descendants((n) => {
552
+ nodes[n.type.name] = (nodes[n.type.name] ?? 0) + 1;
553
+ if (n.type.name === "heading") {
554
+ const l = `h${n.attrs.level}`;
555
+ headings[l] = (headings[l] ?? 0) + 1;
556
+ }
557
+ if (n.isText) {
558
+ textLength += n.text?.length ?? 0;
559
+ for (const m of n.marks) marks[m.type.name] = (marks[m.type.name] ?? 0) + 1;
560
+ }
561
+ return true;
562
+ });
563
+ return { nodes, marks, headings, textLength, topLevelBlocks: editor.state.doc.childCount };
564
+ }
565
+
566
+ // src/doc-runtime/runtime.ts
567
+ function base64ToBytes(b64) {
568
+ const bin = atob(b64.replace(/^data:[^,]*,/, ""));
569
+ const out = new Uint8Array(bin.length);
570
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
571
+ return out;
572
+ }
573
+ function sanitizeFilename(name) {
574
+ if (!name) return null;
575
+ const base = name.split(/[/\\]/).pop()?.trim();
576
+ if (!base || base === "." || base === "..") return null;
577
+ return /\.docx$/i.test(base) ? base : `${base}.docx`;
578
+ }
579
+ function domFlash(id) {
580
+ if (typeof document === "undefined") return;
581
+ requestAnimationFrame(() => {
582
+ const sel = typeof CSS !== "undefined" && CSS.escape ? CSS.escape(id) : id;
583
+ const el = document.querySelector(`[data-id="${sel}"], #${sel}`);
584
+ if (!el) return;
585
+ el.classList.add("ai-flash");
586
+ setTimeout(() => el.classList.remove("ai-flash"), 1400);
587
+ });
588
+ }
589
+ var DocRuntime = class {
590
+ editor;
591
+ highlight;
592
+ parseOpts;
593
+ constructor(options = {}) {
594
+ this.editor = options.editor ?? null;
595
+ this.highlight = options.highlight === false ? null : options.highlight ?? domFlash;
596
+ this.parseOpts = options.DOMParser ? { DOMParser: options.DOMParser } : {};
597
+ }
598
+ /** 编辑器实例就绪 / 卸载时调用。配合 `<NotionEditor onEditorReady>` 用。 */
599
+ setEditor(editor) {
600
+ this.editor = editor;
601
+ }
602
+ isReady() {
603
+ return Boolean(this.editor && !this.editor.isDestroyed);
604
+ }
605
+ use() {
606
+ if (!this.editor) throw new Error("编辑器尚未就绪:先调 setEditor(editor)");
607
+ if (this.editor.isDestroyed) throw new Error("编辑器实例已销毁");
608
+ return this.editor;
609
+ }
610
+ flash(ids) {
611
+ if (!this.highlight) return;
612
+ for (const id of ids) this.highlight(id);
613
+ }
614
+ /* ------------------------------ 读 ------------------------------ */
615
+ /** 读全文。要编辑之前必须先调它拿 id。 */
616
+ getPageContent({ format = "xml" } = {}) {
617
+ const editor = this.use();
618
+ const out = {
619
+ blockCount: editor.state.doc.childCount
620
+ };
621
+ if (format === "xml" || format === "both") out.xml = docToXml(editor.getJSON());
622
+ if (format === "json" || format === "both") out.json = editor.getJSON();
623
+ return out;
624
+ }
625
+ /** 运行时反射出的节点·标记·标签映射·哪些类型带 id。永不与实现漂移。 */
626
+ getSchema() {
627
+ return docSchema(this.use());
628
+ }
629
+ /** 按节点类型点数,用来和导入源做对账。 */
630
+ getStats() {
631
+ return docStats(this.use());
632
+ }
633
+ /** 检索,返回可直接回填给写工具的 nodeId。 */
634
+ find(args) {
635
+ return findTextInDoc(this.use(), args.query, args.limit);
636
+ }
637
+ /** 用户此刻的选区;无选区时显式声明「不要沿用历史」。 */
638
+ getSelection() {
639
+ return getSelection(this.use());
640
+ }
641
+ /* ------------------------------ 写 ------------------------------ */
642
+ /** 块内文本替换(细粒度改动首选)。 */
643
+ replaceText(args) {
644
+ const result = replaceText(this.use(), args);
645
+ this.flash(result.modifiedNodeIds);
646
+ return result;
647
+ }
648
+ /** 对某段文字加/去 bold·italic·underline·strike·code·sup·sub(改格式首选)。 */
649
+ formatText(args) {
650
+ const result = formatText(this.use(), args);
651
+ this.flash([result.nodeId]);
652
+ return result;
653
+ }
654
+ /** 结构化 insert / remove / modify,一次提交多个操作。 */
655
+ modifyNodes(args) {
656
+ const result = applyOperations(this.use(), args.operations ?? [], this.parseOpts);
657
+ this.flash(result.touchedIds);
658
+ return result;
659
+ }
660
+ /** 表格:增删行列、合并拆分、批量写值。 */
661
+ tableEdit(args) {
662
+ const result = tableEdit(this.use(), args);
663
+ this.flash([args.tableId]);
664
+ return result;
665
+ }
666
+ /* ---------------------------- 导入 / 导出 ---------------------------- */
667
+ /**
668
+ * 导出成 .docx,返回 base64。
669
+ *
670
+ * 走 `editor.commands.exportDocx` 而不是直接调 `serializeDocx`:页眉页脚的原始
671
+ * 部件存在 `editor.storage.importDocx.section` 上,表格列宽的换算基准要量
672
+ * `editor.view.dom` —— 这两样只有命令那条路会自动接上,绕过去就会丢页眉、
673
+ * 并且把表格导得超出页面。
674
+ *
675
+ * base64 是**给传输层的**,不是给模型看的。hub 收到后落盘、只把路径回给模型;
676
+ * 一份带图的 docx 几 MB,塞进上下文会直接把窗口撑爆。
677
+ */
678
+ exportDocx(args = {}) {
679
+ const editor = this.use();
680
+ const run = editor.commands.exportDocx;
681
+ if (typeof run !== "function") {
682
+ throw new Error(
683
+ "编辑器没装 ExportDocx 扩展:从 @4399ywkf/editor/docx 引入 ExportDocx,通过 <NotionEditor extensions={[ExportDocx]}> 注册后再试"
684
+ );
685
+ }
686
+ let payload = null;
687
+ run({
688
+ exportType: "string",
689
+ // base64
690
+ onCompleteExport: (result, meta2) => {
691
+ payload = { base64: String(result), meta: meta2 };
692
+ }
693
+ });
694
+ if (!payload) throw new Error("导出失败:exportDocx 没有回调(多半是序列化时抛了异常)");
695
+ const { base64, meta } = payload;
696
+ return {
697
+ filename: sanitizeFilename(args.filename) ?? "document.docx",
698
+ base64,
699
+ bytes: meta?.bytes ?? 0,
700
+ stats: meta?.stats ?? {},
701
+ warnings: meta?.warnings ?? []
702
+ };
703
+ }
704
+ /**
705
+ * 把一份 .docx 灌进编辑器,**整篇替换**当前内容。
706
+ *
707
+ * 同样走命令:`ImportDocx` 会按宿主 schema 降级未知节点、量版心换算表格列宽、
708
+ * 把页眉页脚记进 storage 供导出时写回。
709
+ */
710
+ importDocx(args) {
711
+ const editor = this.use();
712
+ const run = editor.commands.importDocx;
713
+ if (typeof run !== "function") {
714
+ throw new Error(
715
+ "编辑器没装 ImportDocx 扩展:从 @4399ywkf/editor/docx 引入 ImportDocx,通过 <NotionEditor extensions={[ImportDocx]}> 注册后再试"
716
+ );
717
+ }
718
+ if (!args?.base64) throw new Error("importDocx 需要 base64(hub 会把 path 读成 base64 再转发)");
719
+ return new Promise((resolve, reject) => {
720
+ run({
721
+ file: base64ToBytes(args.base64),
722
+ onImport: (ctx) => {
723
+ if (ctx.error) {
724
+ reject(ctx.error);
725
+ return;
726
+ }
727
+ ctx.setEditorContent();
728
+ resolve({
729
+ blockCount: this.editor?.state.doc.childCount ?? 0,
730
+ recovered: ctx.recovered ?? {},
731
+ warnings: ctx.warnings ?? []
732
+ });
733
+ }
734
+ });
735
+ });
736
+ }
737
+ /* ---------------------------- 统一入口 ---------------------------- */
738
+ /**
739
+ * 工具名 → 方法。给传输层(MCP 桥、agent executor)用的单一分发口,
740
+ * 这样加一个工具只需要改这里,不需要每个传输各写一遍 switch。
741
+ */
742
+ async call(tool, args = {}) {
743
+ switch (tool) {
744
+ case "doc_read":
745
+ return this.getPageContent(args);
746
+ case "doc_schema":
747
+ return this.getSchema();
748
+ case "doc_stats":
749
+ return this.getStats();
750
+ case "doc_find":
751
+ return this.find(args);
752
+ case "doc_get_selection":
753
+ return this.getSelection();
754
+ case "doc_replace_text":
755
+ return this.replaceText(args);
756
+ case "doc_format_text":
757
+ return this.formatText(args);
758
+ case "doc_modify_nodes":
759
+ return this.modifyNodes(args);
760
+ case "doc_table_edit":
761
+ return this.tableEdit(args);
762
+ case "doc_export_docx":
763
+ return this.exportDocx(args);
764
+ case "doc_import_docx":
765
+ return this.importDocx(args);
766
+ default:
767
+ throw new Error(`未知工具 ${tool}`);
768
+ }
769
+ }
770
+ };
771
+
772
+ export { DocRuntime, FORMATTABLE, MARK_TAG, NODE_TAG, READONLY_TAGS, VOID_TAGS, applyOperations, docSchema, docStats, docToXml, findById, findTextInDoc, formatText, getSelection, mergeParsedNode, nodeToXml, parseXmlFragment, replaceText, tableEdit, xmlElToPm };
773
+ //# sourceMappingURL=doc-runtime.js.map
774
+ //# sourceMappingURL=doc-runtime.js.map