@xignature/docx-editor 1.0.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,969 @@
1
+ // src/core/core-plugins/registry.ts
2
+ var PluginRegistry = class {
3
+ plugins = /* @__PURE__ */ new Map();
4
+ commandHandlers = /* @__PURE__ */ new Map();
5
+ eventListeners = /* @__PURE__ */ new Set();
6
+ initialized = /* @__PURE__ */ new Set();
7
+ // ==========================================================================
8
+ // REGISTRATION
9
+ // ==========================================================================
10
+ /**
11
+ * Register a plugin
12
+ *
13
+ * @param plugin - The plugin to register
14
+ * @param options - Optional configuration
15
+ * @returns Registration result
16
+ */
17
+ register(plugin, options) {
18
+ const warnings = [];
19
+ if (!plugin.id) {
20
+ return { success: false, error: "Plugin must have an id" };
21
+ }
22
+ if (this.plugins.has(plugin.id)) {
23
+ return { success: false, error: `Plugin '${plugin.id}' is already registered` };
24
+ }
25
+ if (plugin.dependencies) {
26
+ for (const depId of plugin.dependencies) {
27
+ if (!this.plugins.has(depId)) {
28
+ return {
29
+ success: false,
30
+ error: `Plugin '${plugin.id}' requires '${depId}' which is not registered`
31
+ };
32
+ }
33
+ }
34
+ }
35
+ if (plugin.commandHandlers) {
36
+ for (const [commandType, handler] of Object.entries(plugin.commandHandlers)) {
37
+ if (this.commandHandlers.has(commandType)) {
38
+ const existing = this.commandHandlers.get(commandType);
39
+ warnings.push(
40
+ `Command '${commandType}' from '${plugin.id}' overrides handler from '${existing.pluginId}'`
41
+ );
42
+ }
43
+ this.commandHandlers.set(commandType, { pluginId: plugin.id, handler });
44
+ }
45
+ }
46
+ this.plugins.set(plugin.id, plugin);
47
+ if (plugin.initialize && !this.initialized.has(plugin.id)) {
48
+ try {
49
+ const result = plugin.initialize();
50
+ if (result instanceof Promise) {
51
+ result.then(() => {
52
+ this.initialized.add(plugin.id);
53
+ }).catch((err) => {
54
+ this.emit({ type: "error", pluginId: plugin.id, error: err });
55
+ });
56
+ } else {
57
+ this.initialized.add(plugin.id);
58
+ }
59
+ } catch (err) {
60
+ this.emit({ type: "error", pluginId: plugin.id, error: err });
61
+ }
62
+ }
63
+ if (options?.debug) {
64
+ console.log(`[PluginRegistry] Registered plugin: ${plugin.id}`);
65
+ }
66
+ this.emit({ type: "registered", plugin });
67
+ return {
68
+ success: true,
69
+ plugin,
70
+ warnings: warnings.length > 0 ? warnings : void 0
71
+ };
72
+ }
73
+ /**
74
+ * Unregister a plugin
75
+ *
76
+ * @param pluginId - ID of the plugin to unregister
77
+ * @returns Whether unregistration succeeded
78
+ */
79
+ unregister(pluginId) {
80
+ const plugin = this.plugins.get(pluginId);
81
+ if (!plugin) {
82
+ return false;
83
+ }
84
+ for (const [id, p] of this.plugins) {
85
+ if (p.dependencies?.includes(pluginId)) {
86
+ console.warn(`Cannot unregister '${pluginId}': '${id}' depends on it`);
87
+ return false;
88
+ }
89
+ }
90
+ for (const [commandType, { pluginId: pid }] of this.commandHandlers) {
91
+ if (pid === pluginId) {
92
+ this.commandHandlers.delete(commandType);
93
+ }
94
+ }
95
+ if (plugin.destroy) {
96
+ try {
97
+ const result = plugin.destroy();
98
+ if (result instanceof Promise) {
99
+ result.catch((err) => {
100
+ this.emit({ type: "error", pluginId, error: err });
101
+ });
102
+ }
103
+ } catch (err) {
104
+ this.emit({ type: "error", pluginId, error: err });
105
+ }
106
+ }
107
+ this.plugins.delete(pluginId);
108
+ this.initialized.delete(pluginId);
109
+ this.emit({ type: "unregistered", pluginId });
110
+ return true;
111
+ }
112
+ // ==========================================================================
113
+ // QUERIES
114
+ // ==========================================================================
115
+ /**
116
+ * Get a registered plugin by ID
117
+ *
118
+ * @param id - Plugin ID
119
+ * @returns The plugin or undefined
120
+ */
121
+ get(id) {
122
+ return this.plugins.get(id);
123
+ }
124
+ /**
125
+ * Get all registered plugins
126
+ *
127
+ * @returns Array of all plugins
128
+ */
129
+ getAll() {
130
+ return Array.from(this.plugins.values());
131
+ }
132
+ /**
133
+ * Check if a plugin is registered
134
+ *
135
+ * @param id - Plugin ID
136
+ * @returns Whether the plugin is registered
137
+ */
138
+ has(id) {
139
+ return this.plugins.has(id);
140
+ }
141
+ /**
142
+ * Get number of registered plugins
143
+ */
144
+ get size() {
145
+ return this.plugins.size;
146
+ }
147
+ // ==========================================================================
148
+ // COMMAND HANDLERS
149
+ // ==========================================================================
150
+ /**
151
+ * Get a command handler for a command type
152
+ *
153
+ * @param commandType - The command type
154
+ * @returns The handler or undefined
155
+ */
156
+ getCommandHandler(commandType) {
157
+ const entry = this.commandHandlers.get(commandType);
158
+ return entry?.handler;
159
+ }
160
+ /**
161
+ * Get all registered command types
162
+ *
163
+ * @returns Array of command type strings
164
+ */
165
+ getCommandTypes() {
166
+ return Array.from(this.commandHandlers.keys());
167
+ }
168
+ /**
169
+ * Check if a command type has a handler
170
+ *
171
+ * @param commandType - The command type
172
+ * @returns Whether a handler exists
173
+ */
174
+ hasCommandHandler(commandType) {
175
+ return this.commandHandlers.has(commandType);
176
+ }
177
+ // ==========================================================================
178
+ // MCP TOOLS
179
+ // ==========================================================================
180
+ /**
181
+ * Get all MCP tools from all registered plugins
182
+ *
183
+ * @returns Array of MCP tool definitions
184
+ */
185
+ getMcpTools() {
186
+ const tools = [];
187
+ for (const plugin of this.plugins.values()) {
188
+ if (plugin.mcpTools) {
189
+ tools.push(...plugin.mcpTools);
190
+ }
191
+ }
192
+ return tools;
193
+ }
194
+ /**
195
+ * Get MCP tools from a specific plugin
196
+ *
197
+ * @param pluginId - Plugin ID
198
+ * @returns Array of MCP tool definitions
199
+ */
200
+ getMcpToolsForPlugin(pluginId) {
201
+ const plugin = this.plugins.get(pluginId);
202
+ return plugin?.mcpTools || [];
203
+ }
204
+ /**
205
+ * Get an MCP tool by name
206
+ *
207
+ * @param toolName - Tool name
208
+ * @returns The tool definition or undefined
209
+ */
210
+ getMcpTool(toolName) {
211
+ for (const plugin of this.plugins.values()) {
212
+ if (plugin.mcpTools) {
213
+ const tool = plugin.mcpTools.find((t) => t.name === toolName);
214
+ if (tool) return tool;
215
+ }
216
+ }
217
+ return void 0;
218
+ }
219
+ // ==========================================================================
220
+ // EVENTS
221
+ // ==========================================================================
222
+ /**
223
+ * Add an event listener
224
+ *
225
+ * @param listener - Event listener function
226
+ */
227
+ addEventListener(listener) {
228
+ this.eventListeners.add(listener);
229
+ }
230
+ /**
231
+ * Remove an event listener
232
+ *
233
+ * @param listener - Event listener function
234
+ */
235
+ removeEventListener(listener) {
236
+ this.eventListeners.delete(listener);
237
+ }
238
+ /**
239
+ * Emit an event to all listeners
240
+ */
241
+ emit(event) {
242
+ for (const listener of this.eventListeners) {
243
+ try {
244
+ listener(event);
245
+ } catch (err) {
246
+ console.error("[PluginRegistry] Event listener error:", err);
247
+ }
248
+ }
249
+ }
250
+ // ==========================================================================
251
+ // UTILITIES
252
+ // ==========================================================================
253
+ /**
254
+ * Clear all registered plugins
255
+ *
256
+ * Useful for testing or resetting state.
257
+ */
258
+ clear() {
259
+ for (const plugin of this.plugins.values()) {
260
+ if (plugin.destroy) {
261
+ try {
262
+ plugin.destroy();
263
+ } catch {
264
+ }
265
+ }
266
+ }
267
+ this.plugins.clear();
268
+ this.commandHandlers.clear();
269
+ this.initialized.clear();
270
+ }
271
+ /**
272
+ * Get registry state for debugging
273
+ */
274
+ getDebugInfo() {
275
+ return {
276
+ plugins: Array.from(this.plugins.keys()),
277
+ commandTypes: Array.from(this.commandHandlers.keys()),
278
+ mcpTools: this.getMcpTools().map((t) => t.name),
279
+ initialized: Array.from(this.initialized)
280
+ };
281
+ }
282
+ };
283
+ var pluginRegistry = new PluginRegistry();
284
+ function registerPlugins(plugins, options) {
285
+ return plugins.map((plugin) => pluginRegistry.register(plugin, options));
286
+ }
287
+
288
+ // src/core/agent/executor.ts
289
+ function executeCommand(doc, command) {
290
+ const pluginHandler = pluginRegistry.getCommandHandler(command.type);
291
+ if (pluginHandler) {
292
+ return pluginHandler(doc, command);
293
+ }
294
+ switch (command.type) {
295
+ case "insertText":
296
+ return executeInsertText(doc, command);
297
+ case "replaceText":
298
+ return executeReplaceText(doc, command);
299
+ case "deleteText":
300
+ return executeDeleteText(doc, command);
301
+ case "formatText":
302
+ return executeFormatText(doc, command);
303
+ case "formatParagraph":
304
+ return executeFormatParagraph(doc, command);
305
+ case "applyStyle":
306
+ return executeApplyStyle(doc, command);
307
+ case "insertTable":
308
+ return executeInsertTable(doc, command);
309
+ case "insertImage":
310
+ return executeInsertImage(doc, command);
311
+ case "insertHyperlink":
312
+ return executeInsertHyperlink(doc, command);
313
+ case "removeHyperlink":
314
+ return executeRemoveHyperlink(doc, command);
315
+ case "insertParagraphBreak":
316
+ return executeInsertParagraphBreak(doc, command);
317
+ case "mergeParagraphs":
318
+ return executeMergeParagraphs(doc, command);
319
+ case "splitParagraph":
320
+ return executeSplitParagraph(doc, command);
321
+ case "setVariable":
322
+ return executeSetVariable(doc, command);
323
+ case "applyVariables":
324
+ return executeApplyVariables(doc, command);
325
+ default:
326
+ const _exhaustive = command;
327
+ throw new Error(`Unknown command type: ${_exhaustive.type}`);
328
+ }
329
+ }
330
+ function executeCommands(doc, commands) {
331
+ return commands.reduce((currentDoc, command) => executeCommand(currentDoc, command), doc);
332
+ }
333
+ function cloneDocument(doc) {
334
+ return JSON.parse(JSON.stringify(doc));
335
+ }
336
+ function getBlockIndexForParagraph(body, paragraphIndex) {
337
+ let currentParagraphIndex = 0;
338
+ for (let i = 0; i < body.content.length; i++) {
339
+ if (body.content[i].type === "paragraph") {
340
+ if (currentParagraphIndex === paragraphIndex) {
341
+ return i;
342
+ }
343
+ currentParagraphIndex++;
344
+ }
345
+ }
346
+ return -1;
347
+ }
348
+ function getParagraphText(paragraph) {
349
+ let text = "";
350
+ for (const item of paragraph.content) {
351
+ if (item.type === "run") {
352
+ for (const content of item.content) {
353
+ if (content.type === "text") {
354
+ text += content.text;
355
+ }
356
+ }
357
+ } else if (item.type === "hyperlink") {
358
+ for (const child of item.children) {
359
+ if (child.type === "run") {
360
+ for (const content of child.content) {
361
+ if (content.type === "text") {
362
+ text += content.text;
363
+ }
364
+ }
365
+ }
366
+ }
367
+ }
368
+ }
369
+ return text;
370
+ }
371
+ function createTextRun(text, formatting) {
372
+ return {
373
+ type: "run",
374
+ formatting,
375
+ content: [
376
+ {
377
+ type: "text",
378
+ text
379
+ }
380
+ ]
381
+ };
382
+ }
383
+ function insertTextAtOffset(paragraph, offset, text, formatting) {
384
+ const newContent = [];
385
+ let currentOffset = 0;
386
+ let inserted = false;
387
+ for (const item of paragraph.content) {
388
+ if (item.type === "run") {
389
+ const runText = item.content.filter((c) => c.type === "text").map((c) => c.text).join("");
390
+ const runStart = currentOffset;
391
+ const runEnd = currentOffset + runText.length;
392
+ if (!inserted && offset >= runStart && offset <= runEnd) {
393
+ const insertPos = offset - runStart;
394
+ if (insertPos > 0) {
395
+ newContent.push({
396
+ ...item,
397
+ content: [{ type: "text", text: runText.slice(0, insertPos) }]
398
+ });
399
+ }
400
+ newContent.push(createTextRun(text, formatting || item.formatting));
401
+ if (insertPos < runText.length) {
402
+ newContent.push({
403
+ ...item,
404
+ content: [{ type: "text", text: runText.slice(insertPos) }]
405
+ });
406
+ }
407
+ inserted = true;
408
+ } else {
409
+ newContent.push(item);
410
+ }
411
+ currentOffset = runEnd;
412
+ } else {
413
+ newContent.push(item);
414
+ }
415
+ }
416
+ if (!inserted) {
417
+ newContent.push(createTextRun(text, formatting));
418
+ }
419
+ return newContent;
420
+ }
421
+ function deleteTextInParagraph(paragraph, startOffset, endOffset) {
422
+ const newContent = [];
423
+ let currentOffset = 0;
424
+ for (const item of paragraph.content) {
425
+ if (item.type === "run") {
426
+ const runText = item.content.filter((c) => c.type === "text").map((c) => c.text).join("");
427
+ const runStart = currentOffset;
428
+ const runEnd = currentOffset + runText.length;
429
+ if (runEnd <= startOffset || runStart >= endOffset) {
430
+ newContent.push(item);
431
+ } else {
432
+ let newText = "";
433
+ if (runStart < startOffset) {
434
+ newText += runText.slice(0, startOffset - runStart);
435
+ }
436
+ if (runEnd > endOffset) {
437
+ newText += runText.slice(endOffset - runStart);
438
+ }
439
+ if (newText.length > 0) {
440
+ newContent.push({
441
+ ...item,
442
+ content: [{ type: "text", text: newText }]
443
+ });
444
+ }
445
+ }
446
+ currentOffset = runEnd;
447
+ } else {
448
+ newContent.push(item);
449
+ }
450
+ }
451
+ return newContent;
452
+ }
453
+ function applyFormattingInParagraph(paragraph, startOffset, endOffset, formatting) {
454
+ const newContent = [];
455
+ let currentOffset = 0;
456
+ for (const item of paragraph.content) {
457
+ if (item.type === "run") {
458
+ const runText = item.content.filter((c) => c.type === "text").map((c) => c.text).join("");
459
+ const runStart = currentOffset;
460
+ const runEnd = currentOffset + runText.length;
461
+ if (runEnd <= startOffset || runStart >= endOffset) {
462
+ newContent.push(item);
463
+ } else if (runStart >= startOffset && runEnd <= endOffset) {
464
+ newContent.push({
465
+ ...item,
466
+ formatting: { ...item.formatting, ...formatting }
467
+ });
468
+ } else {
469
+ const overlapStart = Math.max(startOffset, runStart);
470
+ const overlapEnd = Math.min(endOffset, runEnd);
471
+ if (runStart < overlapStart) {
472
+ newContent.push({
473
+ ...item,
474
+ content: [{ type: "text", text: runText.slice(0, overlapStart - runStart) }]
475
+ });
476
+ }
477
+ newContent.push({
478
+ ...item,
479
+ formatting: { ...item.formatting, ...formatting },
480
+ content: [
481
+ {
482
+ type: "text",
483
+ text: runText.slice(overlapStart - runStart, overlapEnd - runStart)
484
+ }
485
+ ]
486
+ });
487
+ if (runEnd > overlapEnd) {
488
+ newContent.push({
489
+ ...item,
490
+ content: [{ type: "text", text: runText.slice(overlapEnd - runStart) }]
491
+ });
492
+ }
493
+ }
494
+ currentOffset = runEnd;
495
+ } else {
496
+ newContent.push(item);
497
+ }
498
+ }
499
+ return newContent;
500
+ }
501
+ function executeInsertText(doc, command) {
502
+ const newDoc = cloneDocument(doc);
503
+ const body = newDoc.package.document;
504
+ const blockIndex = getBlockIndexForParagraph(body, command.position.paragraphIndex);
505
+ if (blockIndex === -1) {
506
+ throw new Error(`Paragraph index ${command.position.paragraphIndex} not found`);
507
+ }
508
+ const paragraph = body.content[blockIndex];
509
+ paragraph.content = insertTextAtOffset(
510
+ paragraph,
511
+ command.position.offset,
512
+ command.text,
513
+ command.formatting
514
+ );
515
+ return newDoc;
516
+ }
517
+ function executeReplaceText(doc, command) {
518
+ const newDoc = cloneDocument(doc);
519
+ const body = newDoc.package.document;
520
+ const { start, end } = command.range;
521
+ if (start.paragraphIndex === end.paragraphIndex) {
522
+ const blockIndex = getBlockIndexForParagraph(body, start.paragraphIndex);
523
+ if (blockIndex === -1) {
524
+ throw new Error(`Paragraph index ${start.paragraphIndex} not found`);
525
+ }
526
+ const paragraph = body.content[blockIndex];
527
+ paragraph.content = deleteTextInParagraph(paragraph, start.offset, end.offset);
528
+ paragraph.content = insertTextAtOffset(
529
+ paragraph,
530
+ start.offset,
531
+ command.text,
532
+ command.formatting
533
+ );
534
+ } else {
535
+ const startBlockIndex = getBlockIndexForParagraph(body, start.paragraphIndex);
536
+ const startParagraph = body.content[startBlockIndex];
537
+ const startText = getParagraphText(startParagraph);
538
+ startParagraph.content = deleteTextInParagraph(startParagraph, start.offset, startText.length);
539
+ startParagraph.content = insertTextAtOffset(
540
+ startParagraph,
541
+ start.offset,
542
+ command.text,
543
+ command.formatting
544
+ );
545
+ const paragraphsToRemove = [];
546
+ for (let i = start.paragraphIndex + 1; i <= end.paragraphIndex; i++) {
547
+ paragraphsToRemove.push(getBlockIndexForParagraph(body, i));
548
+ }
549
+ for (let i = paragraphsToRemove.length - 1; i >= 0; i--) {
550
+ if (paragraphsToRemove[i] !== -1) {
551
+ body.content.splice(paragraphsToRemove[i], 1);
552
+ }
553
+ }
554
+ }
555
+ return newDoc;
556
+ }
557
+ function executeDeleteText(doc, command) {
558
+ const newDoc = cloneDocument(doc);
559
+ const body = newDoc.package.document;
560
+ const { start, end } = command.range;
561
+ if (start.paragraphIndex === end.paragraphIndex) {
562
+ const blockIndex = getBlockIndexForParagraph(body, start.paragraphIndex);
563
+ if (blockIndex === -1) {
564
+ throw new Error(`Paragraph index ${start.paragraphIndex} not found`);
565
+ }
566
+ const paragraph = body.content[blockIndex];
567
+ paragraph.content = deleteTextInParagraph(paragraph, start.offset, end.offset);
568
+ } else {
569
+ const startBlockIndex = getBlockIndexForParagraph(body, start.paragraphIndex);
570
+ const startParagraph = body.content[startBlockIndex];
571
+ const startText = getParagraphText(startParagraph);
572
+ startParagraph.content = deleteTextInParagraph(startParagraph, start.offset, startText.length);
573
+ const endBlockIndex = getBlockIndexForParagraph(body, end.paragraphIndex);
574
+ const endParagraph = body.content[endBlockIndex];
575
+ endParagraph.content = deleteTextInParagraph(endParagraph, 0, end.offset);
576
+ startParagraph.content.push(...endParagraph.content);
577
+ const indicesToRemove = [];
578
+ for (let i = start.paragraphIndex + 1; i <= end.paragraphIndex; i++) {
579
+ indicesToRemove.push(getBlockIndexForParagraph(body, i));
580
+ }
581
+ for (let i = indicesToRemove.length - 1; i >= 0; i--) {
582
+ if (indicesToRemove[i] !== -1) {
583
+ body.content.splice(indicesToRemove[i], 1);
584
+ }
585
+ }
586
+ }
587
+ return newDoc;
588
+ }
589
+ function executeFormatText(doc, command) {
590
+ const newDoc = cloneDocument(doc);
591
+ const body = newDoc.package.document;
592
+ const { start, end } = command.range;
593
+ if (start.paragraphIndex === end.paragraphIndex) {
594
+ const blockIndex = getBlockIndexForParagraph(body, start.paragraphIndex);
595
+ if (blockIndex === -1) {
596
+ throw new Error(`Paragraph index ${start.paragraphIndex} not found`);
597
+ }
598
+ const paragraph = body.content[blockIndex];
599
+ paragraph.content = applyFormattingInParagraph(
600
+ paragraph,
601
+ start.offset,
602
+ end.offset,
603
+ command.formatting
604
+ );
605
+ } else {
606
+ for (let i = start.paragraphIndex; i <= end.paragraphIndex; i++) {
607
+ const blockIndex = getBlockIndexForParagraph(body, i);
608
+ if (blockIndex === -1) continue;
609
+ const paragraph = body.content[blockIndex];
610
+ const paragraphText = getParagraphText(paragraph);
611
+ let startOffset = 0;
612
+ let endOffset = paragraphText.length;
613
+ if (i === start.paragraphIndex) {
614
+ startOffset = start.offset;
615
+ }
616
+ if (i === end.paragraphIndex) {
617
+ endOffset = end.offset;
618
+ }
619
+ paragraph.content = applyFormattingInParagraph(
620
+ paragraph,
621
+ startOffset,
622
+ endOffset,
623
+ command.formatting
624
+ );
625
+ }
626
+ }
627
+ return newDoc;
628
+ }
629
+ function executeFormatParagraph(doc, command) {
630
+ const newDoc = cloneDocument(doc);
631
+ const body = newDoc.package.document;
632
+ const blockIndex = getBlockIndexForParagraph(body, command.paragraphIndex);
633
+ if (blockIndex === -1) {
634
+ throw new Error(`Paragraph index ${command.paragraphIndex} not found`);
635
+ }
636
+ const paragraph = body.content[blockIndex];
637
+ paragraph.formatting = { ...paragraph.formatting, ...command.formatting };
638
+ if ("numPr" in command.formatting) {
639
+ const numPr = command.formatting.numPr;
640
+ if (numPr && numPr.numId !== void 0 && numPr.numId !== 0) {
641
+ const ilvl = numPr.ilvl ?? 0;
642
+ const isBullet = numPr.numId === 1;
643
+ let marker = isBullet ? "\u2022" : `${1}.`;
644
+ if (newDoc.package.numbering) {
645
+ const num = newDoc.package.numbering.nums.find((n) => n.numId === numPr.numId);
646
+ if (num) {
647
+ const abstractNum = newDoc.package.numbering.abstractNums.find(
648
+ (a) => a.abstractNumId === num.abstractNumId
649
+ );
650
+ if (abstractNum) {
651
+ const level = abstractNum.levels.find((l) => l.ilvl === ilvl);
652
+ if (level) {
653
+ marker = level.lvlText || marker;
654
+ }
655
+ }
656
+ }
657
+ }
658
+ paragraph.listRendering = {
659
+ level: ilvl,
660
+ numId: numPr.numId,
661
+ marker,
662
+ isBullet
663
+ };
664
+ } else {
665
+ delete paragraph.listRendering;
666
+ }
667
+ }
668
+ return newDoc;
669
+ }
670
+ function executeApplyStyle(doc, command) {
671
+ const newDoc = cloneDocument(doc);
672
+ const body = newDoc.package.document;
673
+ const blockIndex = getBlockIndexForParagraph(body, command.paragraphIndex);
674
+ if (blockIndex === -1) {
675
+ throw new Error(`Paragraph index ${command.paragraphIndex} not found`);
676
+ }
677
+ const paragraph = body.content[blockIndex];
678
+ paragraph.formatting = {
679
+ ...paragraph.formatting,
680
+ styleId: command.styleId
681
+ };
682
+ return newDoc;
683
+ }
684
+ function executeInsertTable(doc, command) {
685
+ const newDoc = cloneDocument(doc);
686
+ const body = newDoc.package.document;
687
+ const rows = [];
688
+ for (let r = 0; r < command.rows; r++) {
689
+ const cells = [];
690
+ for (let c = 0; c < command.columns; c++) {
691
+ const cellText = command.data?.[r]?.[c] || "";
692
+ cells.push({
693
+ type: "tableCell",
694
+ content: [
695
+ {
696
+ type: "paragraph",
697
+ content: cellText ? [createTextRun(cellText)] : []
698
+ }
699
+ ]
700
+ });
701
+ }
702
+ rows.push({
703
+ type: "tableRow",
704
+ formatting: r === 0 && command.hasHeader ? { header: true } : void 0,
705
+ cells
706
+ });
707
+ }
708
+ const table = {
709
+ type: "table",
710
+ rows
711
+ };
712
+ const blockIndex = getBlockIndexForParagraph(body, command.position.paragraphIndex);
713
+ if (blockIndex === -1) {
714
+ body.content.push(table);
715
+ } else {
716
+ body.content.splice(blockIndex + 1, 0, table);
717
+ }
718
+ return newDoc;
719
+ }
720
+ function executeInsertImage(doc, command) {
721
+ const newDoc = cloneDocument(doc);
722
+ const body = newDoc.package.document;
723
+ const blockIndex = getBlockIndexForParagraph(body, command.position.paragraphIndex);
724
+ if (blockIndex === -1) {
725
+ throw new Error(`Paragraph index ${command.position.paragraphIndex} not found`);
726
+ }
727
+ const paragraph = body.content[blockIndex];
728
+ const image = {
729
+ type: "image",
730
+ rId: `rId_img_${Date.now()}`,
731
+ src: command.src,
732
+ alt: command.alt,
733
+ size: {
734
+ width: (command.width || 100) * 914400,
735
+ // Convert pixels to EMU
736
+ height: (command.height || 100) * 914400
737
+ },
738
+ wrap: { type: "inline" }
739
+ };
740
+ const imageRun = {
741
+ type: "run",
742
+ content: [
743
+ {
744
+ type: "drawing",
745
+ image
746
+ }
747
+ ]
748
+ };
749
+ const newContent = insertTextAtOffset(paragraph, command.position.offset, "", void 0);
750
+ let inserted = false;
751
+ let currentOffset = 0;
752
+ for (let i = 0; i < newContent.length; i++) {
753
+ const item = newContent[i];
754
+ if (item.type === "run") {
755
+ const runText = item.content.filter((c) => c.type === "text").map((c) => c.text).join("");
756
+ currentOffset += runText.length;
757
+ if (!inserted && currentOffset >= command.position.offset) {
758
+ newContent.splice(i + 1, 0, imageRun);
759
+ inserted = true;
760
+ break;
761
+ }
762
+ }
763
+ }
764
+ if (!inserted) {
765
+ newContent.push(imageRun);
766
+ }
767
+ paragraph.content = newContent;
768
+ return newDoc;
769
+ }
770
+ function executeInsertHyperlink(doc, command) {
771
+ const newDoc = cloneDocument(doc);
772
+ const body = newDoc.package.document;
773
+ const { start, end } = command.range;
774
+ if (start.paragraphIndex !== end.paragraphIndex) {
775
+ throw new Error("Hyperlinks cannot span multiple paragraphs");
776
+ }
777
+ const blockIndex = getBlockIndexForParagraph(body, start.paragraphIndex);
778
+ if (blockIndex === -1) {
779
+ throw new Error(`Paragraph index ${start.paragraphIndex} not found`);
780
+ }
781
+ const paragraph = body.content[blockIndex];
782
+ const paragraphText = getParagraphText(paragraph);
783
+ const linkText = command.displayText || paragraphText.slice(start.offset, end.offset);
784
+ paragraph.content = deleteTextInParagraph(paragraph, start.offset, end.offset);
785
+ const hyperlink = {
786
+ type: "hyperlink",
787
+ href: command.url,
788
+ tooltip: command.tooltip,
789
+ children: [createTextRun(linkText)]
790
+ };
791
+ let inserted = false;
792
+ let currentOffset = 0;
793
+ const newContent = [];
794
+ for (const item of paragraph.content) {
795
+ if (item.type === "run") {
796
+ const runText = item.content.filter((c) => c.type === "text").map((c) => c.text).join("");
797
+ const runEnd = currentOffset + runText.length;
798
+ if (!inserted && currentOffset <= start.offset && start.offset <= runEnd) {
799
+ const insertPos = start.offset - currentOffset;
800
+ if (insertPos > 0) {
801
+ newContent.push({
802
+ ...item,
803
+ content: [{ type: "text", text: runText.slice(0, insertPos) }]
804
+ });
805
+ }
806
+ newContent.push(hyperlink);
807
+ if (insertPos < runText.length) {
808
+ newContent.push({
809
+ ...item,
810
+ content: [{ type: "text", text: runText.slice(insertPos) }]
811
+ });
812
+ }
813
+ inserted = true;
814
+ } else {
815
+ newContent.push(item);
816
+ }
817
+ currentOffset = runEnd;
818
+ } else {
819
+ newContent.push(item);
820
+ }
821
+ }
822
+ if (!inserted) {
823
+ newContent.push(hyperlink);
824
+ }
825
+ paragraph.content = newContent;
826
+ return newDoc;
827
+ }
828
+ function executeRemoveHyperlink(doc, command) {
829
+ const newDoc = cloneDocument(doc);
830
+ const body = newDoc.package.document;
831
+ const { start } = command.range;
832
+ const blockIndex = getBlockIndexForParagraph(body, start.paragraphIndex);
833
+ if (blockIndex === -1) {
834
+ throw new Error(`Paragraph index ${start.paragraphIndex} not found`);
835
+ }
836
+ const paragraph = body.content[blockIndex];
837
+ const newContent = [];
838
+ for (const item of paragraph.content) {
839
+ if (item.type === "hyperlink") {
840
+ for (const child of item.children) {
841
+ if (child.type === "run") {
842
+ newContent.push(child);
843
+ }
844
+ }
845
+ } else {
846
+ newContent.push(item);
847
+ }
848
+ }
849
+ paragraph.content = newContent;
850
+ return newDoc;
851
+ }
852
+ function executeInsertParagraphBreak(doc, command) {
853
+ const newDoc = cloneDocument(doc);
854
+ const body = newDoc.package.document;
855
+ const blockIndex = getBlockIndexForParagraph(body, command.position.paragraphIndex);
856
+ if (blockIndex === -1) {
857
+ throw new Error(`Paragraph index ${command.position.paragraphIndex} not found`);
858
+ }
859
+ const paragraph = body.content[blockIndex];
860
+ const paragraphText = getParagraphText(paragraph);
861
+ const beforeContent = deleteTextInParagraph(
862
+ { ...paragraph, content: [...paragraph.content] },
863
+ command.position.offset,
864
+ paragraphText.length
865
+ );
866
+ const afterContent = deleteTextInParagraph(
867
+ { ...paragraph, content: [...paragraph.content] },
868
+ 0,
869
+ command.position.offset
870
+ );
871
+ paragraph.content = beforeContent;
872
+ const newParagraph = {
873
+ type: "paragraph",
874
+ formatting: paragraph.formatting,
875
+ content: afterContent
876
+ };
877
+ body.content.splice(blockIndex + 1, 0, newParagraph);
878
+ return newDoc;
879
+ }
880
+ function executeMergeParagraphs(doc, command) {
881
+ const newDoc = cloneDocument(doc);
882
+ const body = newDoc.package.document;
883
+ const startBlockIndex = getBlockIndexForParagraph(body, command.paragraphIndex);
884
+ if (startBlockIndex === -1) {
885
+ throw new Error(`Paragraph index ${command.paragraphIndex} not found`);
886
+ }
887
+ const baseParagraph = body.content[startBlockIndex];
888
+ const indicesToRemove = [];
889
+ for (let i = 1; i <= command.count; i++) {
890
+ const blockIndex = getBlockIndexForParagraph(body, command.paragraphIndex + i);
891
+ if (blockIndex !== -1) {
892
+ const para = body.content[blockIndex];
893
+ baseParagraph.content.push(...para.content);
894
+ indicesToRemove.push(blockIndex);
895
+ }
896
+ }
897
+ for (let i = indicesToRemove.length - 1; i >= 0; i--) {
898
+ body.content.splice(indicesToRemove[i], 1);
899
+ }
900
+ return newDoc;
901
+ }
902
+ function executeSplitParagraph(doc, command) {
903
+ return executeInsertParagraphBreak(doc, {
904
+ type: "insertParagraphBreak",
905
+ position: command.position
906
+ });
907
+ }
908
+ function executeSetVariable(doc, command) {
909
+ const newDoc = cloneDocument(doc);
910
+ if (!newDoc.templateVariables) {
911
+ newDoc.templateVariables = [];
912
+ }
913
+ if (!newDoc.templateVariables.includes(command.name)) {
914
+ newDoc.templateVariables.push(command.name);
915
+ }
916
+ return newDoc;
917
+ }
918
+ function executeApplyVariables(doc, command) {
919
+ const newDoc = cloneDocument(doc);
920
+ const body = newDoc.package.document;
921
+ function replaceVariablesInRun(run) {
922
+ for (const content of run.content) {
923
+ if (content.type === "text") {
924
+ for (const [name, value] of Object.entries(command.values)) {
925
+ const pattern = new RegExp(`\\{${name}\\}`, "g");
926
+ content.text = content.text.replace(pattern, value);
927
+ }
928
+ }
929
+ }
930
+ }
931
+ function replaceVariablesInParagraph(paragraph) {
932
+ for (const item of paragraph.content) {
933
+ if (item.type === "run") {
934
+ replaceVariablesInRun(item);
935
+ } else if (item.type === "hyperlink") {
936
+ for (const child of item.children) {
937
+ if (child.type === "run") {
938
+ replaceVariablesInRun(child);
939
+ }
940
+ }
941
+ }
942
+ }
943
+ }
944
+ function replaceVariablesInBlock(block) {
945
+ if (block.type === "paragraph") {
946
+ replaceVariablesInParagraph(block);
947
+ } else if (block.type === "table") {
948
+ for (const row of block.rows) {
949
+ for (const cell of row.cells) {
950
+ for (const cellBlock of cell.content) {
951
+ replaceVariablesInBlock(cellBlock);
952
+ }
953
+ }
954
+ }
955
+ }
956
+ }
957
+ for (const block of body.content) {
958
+ replaceVariablesInBlock(block);
959
+ }
960
+ return newDoc;
961
+ }
962
+
963
+ export {
964
+ PluginRegistry,
965
+ pluginRegistry,
966
+ registerPlugins,
967
+ executeCommand,
968
+ executeCommands
969
+ };