mcp-word-bridge 3.4.1 → 3.4.2
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.
- package/index.js +1 -1
- package/lib/tools.js +5 -5
- package/package.json +1 -1
- package/taskpane-app.js +153 -25
package/index.js
CHANGED
package/lib/tools.js
CHANGED
|
@@ -23,18 +23,18 @@ const tools = [
|
|
|
23
23
|
{ name: 'word_set_paragraph_style', description: 'Change the style or alignment of a paragraph by index. Alignment accepts: Left, Center, Right, Justified.', inputSchema: { type: 'object', properties: { index: { type: 'number' }, style: { type: 'string' }, alignment: { type: 'string', description: 'Paragraph alignment: Left, Center, Right, or Justified' } }, required: ['index'] } },
|
|
24
24
|
{ name: 'word_set_paragraph_spacing', description: 'Set line spacing (in points, e.g. 12=single for 12pt font, 24=double), before/after spacing (points), and indentation (points) on a paragraph by index.', inputSchema: { type: 'object', properties: { index: { type: 'number' }, lineSpacing: { type: 'number', description: 'Line spacing in points (e.g. 12=single for 12pt, 18=1.5x, 24=double)' }, spaceBefore: { type: 'number', description: 'Space before paragraph in points' }, spaceAfter: { type: 'number', description: 'Space after paragraph in points' }, firstLineIndent: { type: 'number', description: 'First line indent in points' }, leftIndent: { type: 'number', description: 'Left indent in points' }, rightIndent: { type: 'number', description: 'Right indent in points' } }, required: ['index'] } },
|
|
25
25
|
// 3. SEARCH & TEXT
|
|
26
|
-
{ name: 'word_search', description: 'Search for text in the document (case-insensitive by default). Returns match count and up to 30 matches with their text.', inputSchema: { type: 'object', properties: { query: { type: 'string' }, matchCase: { type: 'boolean', description: 'Case-sensitive search. Default: false' }, matchWholeWord: { type: 'boolean' } }, required: ['query'] } },
|
|
27
|
-
{ name: 'word_search_and_replace', description: 'Find and replace all occurrences of text (case-insensitive by default). Returns replacement count.', inputSchema: { type: 'object', properties: { find: { type: 'string' }, replace: { type: 'string' }, matchCase: { type: 'boolean', description: 'Case-sensitive matching. Default: false' }, matchWholeWord: { type: 'boolean' } }, required: ['find', 'replace'] } },
|
|
26
|
+
{ name: 'word_search', description: 'Search for text in the document (case-insensitive by default). Query must be non-empty and ≤255 chars. Returns match count and up to 30 matches with their text. Supports Word wildcards (e.g. ^p for paragraph mark).', inputSchema: { type: 'object', properties: { query: { type: 'string' }, matchCase: { type: 'boolean', description: 'Case-sensitive search. Default: false' }, matchWholeWord: { type: 'boolean' } }, required: ['query'] } },
|
|
27
|
+
{ name: 'word_search_and_replace', description: 'Find and replace all occurrences of text (case-insensitive by default). Returns replacement count. Note: if find equals replace, Word still reports it as a replacement (the text is rewritten in-place). Supports Word wildcards (e.g. ^p for paragraph mark).', inputSchema: { type: 'object', properties: { find: { type: 'string' }, replace: { type: 'string' }, matchCase: { type: 'boolean', description: 'Case-sensitive matching. Default: false' }, matchWholeWord: { type: 'boolean' } }, required: ['find', 'replace'] } },
|
|
28
28
|
{ name: 'word_insert_text', description: 'Insert text before or after a search match. Provide "after" OR "before" (not both) as the anchor string to locate.', inputSchema: { type: 'object', properties: { text: { type: 'string' }, after: { type: 'string' }, before: { type: 'string' }, occurrence: { type: 'number', description: '0-indexed match to target (0=first, 1=second, etc)' }, matchCase: { type: 'boolean', description: 'Case-sensitive matching. Default: false (case-insensitive)' } }, required: ['text'] } },
|
|
29
29
|
{ name: 'word_get_selection_info', description: 'Get the current selection text with full font and style details.', inputSchema: { type: 'object', properties: {} } },
|
|
30
30
|
{ name: 'word_insert_text_at_selection', description: 'Insert text at the current cursor position, or replace the current selection (set replace=true).', inputSchema: { type: 'object', properties: { text: { type: 'string' }, replace: { type: 'boolean', description: 'Replace current selection instead of appending. Default: false' } }, required: ['text'] } },
|
|
31
31
|
{ name: 'word_insert_line_break', description: 'Insert a soft line break (Shift+Enter) before or after a text match.', inputSchema: { type: 'object', properties: { anchorText: { type: 'string' }, before: { type: 'boolean' }, occurrence: { type: 'number', description: '0-indexed match to target (0=first, 1=second, etc)' }, matchCase: { type: 'boolean', description: 'Case-sensitive matching. Default: false (case-insensitive)' } }, required: ['anchorText'] } },
|
|
32
32
|
// 4. FORMATTING
|
|
33
|
-
{ name: 'word_format_text', description: 'Apply formatting (bold, italic, color, size, font) to a text match found by search.', inputSchema: { type: 'object', properties: { text: { type: 'string' }, occurrence: { type: 'number', description: '0-indexed match to target (0=first, 1=second, etc)' }, bold: { type: 'boolean' }, italic: { type: 'boolean' }, underline: { type: 'boolean' }, strikeThrough: { type: 'boolean' }, color: { type: 'string', description: 'Hex color e.g. #FF0000' }, highlightColor: { type: 'string' }, size: { type: 'number' }, name: { type: 'string', description: 'Font name' }, matchCase: { type: 'boolean', description: 'Case-sensitive matching. Default: false (case-insensitive)' } }, required: ['text'] } },
|
|
33
|
+
{ name: 'word_format_text', description: 'Apply formatting (bold, italic, color, size, font) to a text match found by search. Color must be hex (e.g. #FF0000). Size range: 1-1638 points.', inputSchema: { type: 'object', properties: { text: { type: 'string' }, occurrence: { type: 'number', description: '0-indexed match to target (0=first, 1=second, etc)' }, bold: { type: 'boolean' }, italic: { type: 'boolean' }, underline: { type: 'boolean' }, strikeThrough: { type: 'boolean' }, color: { type: 'string', description: 'Hex color e.g. #FF0000' }, highlightColor: { type: 'string' }, size: { type: 'number', description: 'Font size in points (1-1638)' }, name: { type: 'string', description: 'Font name' }, matchCase: { type: 'boolean', description: 'Case-sensitive matching. Default: false (case-insensitive)' } }, required: ['text'] } },
|
|
34
34
|
{ name: 'word_clear_formatting', description: 'Clear direct formatting from a text match, reverting it to the paragraph style defaults.', inputSchema: { type: 'object', properties: { text: { type: 'string' }, occurrence: { type: 'number', description: '0-indexed match to target (0=first, 1=second, etc)' }, matchCase: { type: 'boolean', description: 'Case-sensitive matching. Default: false (case-insensitive)' } }, required: ['text'] } },
|
|
35
35
|
{ name: 'word_get_font_info', description: 'Inspect font properties (name, size, bold, italic, color) of a text match.', inputSchema: { type: 'object', properties: { text: { type: 'string' }, occurrence: { type: 'number', description: '0-indexed match to target (0=first, 1=second, etc)' }, matchCase: { type: 'boolean', description: 'Case-sensitive matching. Default: false (case-insensitive)' } }, required: ['text'] } },
|
|
36
36
|
// 5. TABLES
|
|
37
|
-
{ name: 'word_insert_table', description: 'Insert a table with data. Provide rows, cols, and data as array of arrays (e.g. [["A","B"],["C","D"]]).', inputSchema: { type: 'object', properties: { rows: { type: 'number' }, cols: { type: 'number' }, data: { type: 'array', items: { type: 'array', items: { type: 'string' } } }, location: { type: 'string', enum: ['Start', 'End'] }, style: { type: 'string' }, headerRowCount: { type: 'number' } }, required: ['rows', 'cols'] } },
|
|
37
|
+
{ name: 'word_insert_table', description: 'Insert a table with data. Provide rows (1-500), cols (1-63), and data as array of arrays (e.g. [["A","B"],["C","D"]]).', inputSchema: { type: 'object', properties: { rows: { type: 'number' }, cols: { type: 'number' }, data: { type: 'array', items: { type: 'array', items: { type: 'string' } } }, location: { type: 'string', enum: ['Start', 'End'] }, style: { type: 'string' }, headerRowCount: { type: 'number' } }, required: ['rows', 'cols'] } },
|
|
38
38
|
{ name: 'word_get_tables', description: 'Get all tables with row counts, styles, and cell values.', inputSchema: { type: 'object', properties: {} } },
|
|
39
39
|
{ name: 'word_get_table_data', description: 'Get all cell values from a specific table by index.', inputSchema: { type: 'object', properties: { index: { type: 'number' } }, required: ['index'] } },
|
|
40
40
|
{ name: 'word_set_table_cell', description: 'Set text in a specific table cell by tableIndex, row, and col.', inputSchema: { type: 'object', properties: { tableIndex: { type: 'number' }, row: { type: 'number' }, col: { type: 'number' }, text: { type: 'string' } }, required: ['tableIndex', 'row', 'col', 'text'] } },
|
|
@@ -77,7 +77,7 @@ const tools = [
|
|
|
77
77
|
{ name: 'word_set_content_control_text', description: 'Set text in a content control by ID or tag.', inputSchema: { type: 'object', properties: { id: { type: 'number' }, tag: { type: 'string' }, text: { type: 'string' } }, required: ['text'] } },
|
|
78
78
|
// 11. BOOKMARKS
|
|
79
79
|
{ name: 'word_get_bookmarks', description: 'Get all bookmark names.', inputSchema: { type: 'object', properties: {} } },
|
|
80
|
-
{ name: 'word_insert_bookmark', description: 'Insert a bookmark at anchor text.', inputSchema: { type: 'object', properties: { name: { type: 'string' }, anchorText: { type: 'string' }, occurrence: { type: 'number', description: '0-indexed match to target (0=first, 1=second, etc)' }, matchCase: { type: 'boolean', description: 'Case-sensitive matching. Default: false (case-insensitive)' } }, required: ['name', 'anchorText'] } },
|
|
80
|
+
{ name: 'word_insert_bookmark', description: 'Insert a bookmark at anchor text. If a bookmark with the same name already exists, it is moved to the new location (returns warning).', inputSchema: { type: 'object', properties: { name: { type: 'string' }, anchorText: { type: 'string' }, occurrence: { type: 'number', description: '0-indexed match to target (0=first, 1=second, etc)' }, matchCase: { type: 'boolean', description: 'Case-sensitive matching. Default: false (case-insensitive)' } }, required: ['name', 'anchorText'] } },
|
|
81
81
|
{ name: 'word_delete_bookmark', description: 'Delete a bookmark by name.', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } },
|
|
82
82
|
{ name: 'word_go_to_bookmark', description: 'Navigate to a bookmark and select its text range.', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } },
|
|
83
83
|
{ name: 'word_get_bookmark_text', description: 'Get the text content within a named bookmark.', inputSchema: { type: 'object', properties: { name: { type: 'string' } }, required: ['name'] } },
|
package/package.json
CHANGED
package/taskpane-app.js
CHANGED
|
@@ -138,9 +138,12 @@ commands.setDocumentProperties = (p) => Word.run(async (ctx) => {
|
|
|
138
138
|
|
|
139
139
|
commands.setChangeTracking = (p) => Word.run(async (ctx) => {
|
|
140
140
|
// mode: 'TrackAll', 'TrackMineOnly', or 'Off'
|
|
141
|
-
|
|
141
|
+
const validModes = ['TrackAll', 'TrackMineOnly', 'Off'];
|
|
142
|
+
const mode = p.mode || 'TrackAll';
|
|
143
|
+
if (!validModes.includes(mode)) throw new Error('Invalid mode: "' + mode + '". Valid values: TrackAll, TrackMineOnly, Off');
|
|
144
|
+
ctx.document.changeTrackingMode = mode;
|
|
142
145
|
await ctx.sync();
|
|
143
|
-
return { success: true, mode:
|
|
146
|
+
return { success: true, mode: mode };
|
|
144
147
|
});
|
|
145
148
|
|
|
146
149
|
// == PARAGRAPHS ==
|
|
@@ -150,8 +153,9 @@ commands.getParagraphs = (p) => Word.run(async (ctx) => {
|
|
|
150
153
|
const paragraphs = ctx.document.body.paragraphs;
|
|
151
154
|
paragraphs.load('text,style,alignment,firstLineIndent,leftIndent,lineSpacing,isListItem,parentTableCellOrNullObject');
|
|
152
155
|
await ctx.sync();
|
|
153
|
-
const start = p.start
|
|
154
|
-
const end = p.end
|
|
156
|
+
const start = p.start !== undefined ? p.start : 0;
|
|
157
|
+
const end = p.end !== undefined ? p.end : paragraphs.items.length;
|
|
158
|
+
if (start > end) throw new Error('start (' + start + ') must be less than or equal to end (' + end + ')');
|
|
155
159
|
const items = [];
|
|
156
160
|
for (let i = start; i < Math.min(end, paragraphs.items.length); i++) {
|
|
157
161
|
const para = paragraphs.items[i];
|
|
@@ -163,6 +167,10 @@ commands.getParagraphs = (p) => Word.run(async (ctx) => {
|
|
|
163
167
|
});
|
|
164
168
|
|
|
165
169
|
commands.insertParagraph = (p) => Word.run(async (ctx) => {
|
|
170
|
+
// Validate location enum
|
|
171
|
+
if (p.location && p.location !== 'Start' && p.location !== 'End') {
|
|
172
|
+
throw new Error('Invalid location: "' + p.location + '". Valid values: Start, End');
|
|
173
|
+
}
|
|
166
174
|
// Normalize and validate alignment before any document mutation
|
|
167
175
|
// Official Word.Alignment enum: Left, Centered, Right, Justified (Mixed/Unknown are read-only)
|
|
168
176
|
const ALIGNMENT_MAP = { 'Left': 'Left', 'Center': 'Centered', 'Centered': 'Centered', 'Right': 'Right', 'Justify': 'Justified', 'Justified': 'Justified' };
|
|
@@ -267,9 +275,17 @@ commands.setParagraphStyle = (p) => Word.run(async (ctx) => {
|
|
|
267
275
|
|
|
268
276
|
// == SEARCH & REPLACE ==
|
|
269
277
|
commands.search = (p) => Word.run(async (ctx) => {
|
|
278
|
+
if (!p.query || typeof p.query !== 'string' || p.query.trim() === '') throw new Error('Search query cannot be empty. Provide a non-empty search string.');
|
|
270
279
|
const results = ctx.document.body.search(p.query, { matchCase: p.matchCase || false, matchWholeWord: p.matchWholeWord || false });
|
|
271
280
|
results.load('text');
|
|
272
|
-
|
|
281
|
+
try {
|
|
282
|
+
await ctx.sync();
|
|
283
|
+
} catch (e) {
|
|
284
|
+
if (e.message && e.message.includes('SearchStringInvalidOrTooLong')) {
|
|
285
|
+
throw new Error('Search query is too long (max ~255 characters). Shorten the query text.');
|
|
286
|
+
}
|
|
287
|
+
throw e;
|
|
288
|
+
}
|
|
273
289
|
return { count: results.items.length, matches: results.items.slice(0, 30).map((r, i) => ({ index: i, text: r.text })) };
|
|
274
290
|
});
|
|
275
291
|
|
|
@@ -327,6 +343,9 @@ commands.replaceSelection = (p) => Word.run(async (ctx) => {
|
|
|
327
343
|
// == FORMATTING ==
|
|
328
344
|
commands.formatRange = (p) => Word.run(async (ctx) => {
|
|
329
345
|
if (p.occurrence !== undefined && p.occurrence < 0) throw new Error('occurrence must be non-negative (0-indexed)');
|
|
346
|
+
if (p.size !== undefined && p.size <= 0) throw new Error('size must be positive');
|
|
347
|
+
if (p.size !== undefined && p.size > 1638) throw new Error('size must not exceed 1638 points (Word maximum)');
|
|
348
|
+
if (p.color && !/^#[0-9A-Fa-f]{6}$/.test(p.color)) throw new Error('color must be a valid hex color (e.g. #FF0000)');
|
|
330
349
|
const results = ctx.document.body.search(p.text, { matchCase: p.matchCase || false });
|
|
331
350
|
results.load('font');
|
|
332
351
|
await ctx.sync();
|
|
@@ -350,6 +369,11 @@ commands.formatRange = (p) => Word.run(async (ctx) => {
|
|
|
350
369
|
|
|
351
370
|
// == TABLES ==
|
|
352
371
|
commands.insertTable = (p) => Word.run(async (ctx) => {
|
|
372
|
+
// Validate rows and cols
|
|
373
|
+
if (!p.rows || p.rows <= 0) throw new Error('rows must be a positive integer (minimum 1)');
|
|
374
|
+
if (!p.cols || p.cols <= 0) throw new Error('cols must be a positive integer (minimum 1)');
|
|
375
|
+
if (p.cols > 63) throw new Error('cols must not exceed 63 (Word maximum column limit)');
|
|
376
|
+
if (p.rows > 500) throw new Error('rows must not exceed 500 (practical limit for performance)');
|
|
353
377
|
// Validate data dimensions match rows/cols if data is provided
|
|
354
378
|
if (p.data) {
|
|
355
379
|
if (!Array.isArray(p.data)) throw new Error('data must be an array of arrays');
|
|
@@ -383,6 +407,7 @@ commands.getTables = () => Word.run(async (ctx) => {
|
|
|
383
407
|
});
|
|
384
408
|
|
|
385
409
|
commands.getTableData = (p) => Word.run(async (ctx) => {
|
|
410
|
+
if (p.index < 0) throw new Error('Table index must be non-negative');
|
|
386
411
|
const tables = ctx.document.body.tables;
|
|
387
412
|
tables.load('rowCount,values');
|
|
388
413
|
await ctx.sync();
|
|
@@ -412,11 +437,16 @@ commands.setTableCell = (p) => Word.run(async (ctx) => {
|
|
|
412
437
|
});
|
|
413
438
|
|
|
414
439
|
commands.addTableRow = (p) => Word.run(async (ctx) => {
|
|
440
|
+
if (p.tableIndex < 0) throw new Error('Table index must be non-negative');
|
|
415
441
|
const tables = ctx.document.body.tables;
|
|
416
|
-
tables.load('rowCount');
|
|
442
|
+
tables.load('rowCount,values');
|
|
417
443
|
await ctx.sync();
|
|
418
444
|
if (p.tableIndex >= tables.items.length) throw new Error('Table index out of range');
|
|
419
445
|
const table = tables.items[p.tableIndex];
|
|
446
|
+
if (p.values && p.values.length > 0) {
|
|
447
|
+
const colCount = table.values[0].length;
|
|
448
|
+
if (p.values.length > colCount) throw new Error('values has ' + p.values.length + ' items but table only has ' + colCount + ' columns.');
|
|
449
|
+
}
|
|
420
450
|
table.addRows(p.location || 'End', 1, p.values ? [p.values] : undefined);
|
|
421
451
|
await ctx.sync();
|
|
422
452
|
// Move cursor to end of table after adding row
|
|
@@ -427,6 +457,8 @@ commands.addTableRow = (p) => Word.run(async (ctx) => {
|
|
|
427
457
|
});
|
|
428
458
|
|
|
429
459
|
commands.deleteTableRow = (p) => Word.run(async (ctx) => {
|
|
460
|
+
if (p.tableIndex < 0) throw new Error('Table index must be non-negative');
|
|
461
|
+
if (p.rowIndex < 0) throw new Error('Row index must be non-negative');
|
|
430
462
|
const tables = ctx.document.body.tables;
|
|
431
463
|
tables.load('rowCount');
|
|
432
464
|
await ctx.sync();
|
|
@@ -477,6 +509,7 @@ commands.insertList = (p) => Word.run(async (ctx) => {
|
|
|
477
509
|
// == COMMENTS ==
|
|
478
510
|
commands.addComment = (p) => Word.run(async (ctx) => {
|
|
479
511
|
if (p.occurrence !== undefined && p.occurrence < 0) throw new Error('occurrence must be non-negative (0-indexed)');
|
|
512
|
+
if (!p.comment || typeof p.comment !== 'string' || p.comment.trim() === '') throw new Error('comment text must be a non-empty string');
|
|
480
513
|
const results = ctx.document.body.search(p.anchorText, { matchCase: p.matchCase || false });
|
|
481
514
|
results.load('text');
|
|
482
515
|
await ctx.sync();
|
|
@@ -541,6 +574,7 @@ commands.deleteComment = (p) => Word.run(async (ctx) => {
|
|
|
541
574
|
|
|
542
575
|
// == FOOTNOTES & ENDNOTES ==
|
|
543
576
|
commands.insertFootnote = (p) => Word.run(async (ctx) => {
|
|
577
|
+
if (!p.text || typeof p.text !== 'string' || p.text.trim() === '') throw new Error('Footnote text must be a non-empty string');
|
|
544
578
|
const results = ctx.document.body.search(p.anchorText, { matchCase: p.matchCase || false });
|
|
545
579
|
results.load('text');
|
|
546
580
|
await ctx.sync();
|
|
@@ -555,6 +589,7 @@ commands.insertFootnote = (p) => Word.run(async (ctx) => {
|
|
|
555
589
|
});
|
|
556
590
|
|
|
557
591
|
commands.insertEndnote = (p) => Word.run(async (ctx) => {
|
|
592
|
+
if (!p.text || typeof p.text !== 'string' || p.text.trim() === '') throw new Error('Endnote text must be a non-empty string');
|
|
558
593
|
const results = ctx.document.body.search(p.anchorText, { matchCase: p.matchCase || false });
|
|
559
594
|
results.load('text');
|
|
560
595
|
await ctx.sync();
|
|
@@ -572,6 +607,9 @@ commands.getFootnotes = () => Word.run(async (ctx) => {
|
|
|
572
607
|
const footnotes = ctx.document.body.footnotes;
|
|
573
608
|
footnotes.load('items');
|
|
574
609
|
await ctx.sync();
|
|
610
|
+
// Re-load to avoid stale collection after recent insertions
|
|
611
|
+
footnotes.load('items');
|
|
612
|
+
await ctx.sync();
|
|
575
613
|
const items = [];
|
|
576
614
|
for (const fn of footnotes.items) {
|
|
577
615
|
fn.body.load('text');
|
|
@@ -585,6 +623,9 @@ commands.getEndnotes = () => Word.run(async (ctx) => {
|
|
|
585
623
|
const endnotes = ctx.document.body.endnotes;
|
|
586
624
|
endnotes.load('items');
|
|
587
625
|
await ctx.sync();
|
|
626
|
+
// Re-load to avoid stale collection after recent insertions
|
|
627
|
+
endnotes.load('items');
|
|
628
|
+
await ctx.sync();
|
|
588
629
|
const items = [];
|
|
589
630
|
for (const en of endnotes.items) {
|
|
590
631
|
en.body.load('text');
|
|
@@ -604,9 +645,13 @@ commands.getTrackedChanges = () => Word.run(async (ctx) => {
|
|
|
604
645
|
});
|
|
605
646
|
|
|
606
647
|
commands.acceptTrackedChange = (p) => Word.run(async (ctx) => {
|
|
648
|
+
if (p.index < 0) throw new Error('Index must be non-negative');
|
|
607
649
|
const changes = ctx.document.body.getTrackedChanges();
|
|
608
650
|
changes.load('items');
|
|
609
651
|
await ctx.sync();
|
|
652
|
+
// Re-load to avoid stale collection after recent document mutations
|
|
653
|
+
changes.load('items');
|
|
654
|
+
await ctx.sync();
|
|
610
655
|
if (p.index >= changes.items.length) throw new Error('Change index out of range. Document has ' + changes.items.length + ' tracked change(s).');
|
|
611
656
|
changes.items[p.index].accept();
|
|
612
657
|
await ctx.sync();
|
|
@@ -614,9 +659,13 @@ commands.acceptTrackedChange = (p) => Word.run(async (ctx) => {
|
|
|
614
659
|
});
|
|
615
660
|
|
|
616
661
|
commands.rejectTrackedChange = (p) => Word.run(async (ctx) => {
|
|
662
|
+
if (p.index < 0) throw new Error('Index must be non-negative');
|
|
617
663
|
const changes = ctx.document.body.getTrackedChanges();
|
|
618
664
|
changes.load('items');
|
|
619
665
|
await ctx.sync();
|
|
666
|
+
// Re-load to avoid stale collection after recent document mutations
|
|
667
|
+
changes.load('items');
|
|
668
|
+
await ctx.sync();
|
|
620
669
|
if (p.index >= changes.items.length) throw new Error('Change index out of range. Document has ' + changes.items.length + ' tracked change(s).');
|
|
621
670
|
changes.items[p.index].reject();
|
|
622
671
|
await ctx.sync();
|
|
@@ -682,7 +731,7 @@ commands.setContentControlText = (p) => Word.run(async (ctx) => {
|
|
|
682
731
|
}
|
|
683
732
|
}
|
|
684
733
|
}
|
|
685
|
-
if (!target) throw new Error('Content control not found. Use word_get_content_controls to list available controls.');
|
|
734
|
+
if (!target) throw new Error('Content control not found. Provide "id" or "tag" to identify the control. Use word_get_content_controls to list available controls.');
|
|
686
735
|
const inserted = target.insertText(p.text, Word.InsertLocation.replace);
|
|
687
736
|
inserted.getRange('End').select();
|
|
688
737
|
await ctx.sync();
|
|
@@ -694,7 +743,9 @@ commands.getHeaderFooter = (p) => Word.run(async (ctx) => {
|
|
|
694
743
|
const sections = ctx.document.sections;
|
|
695
744
|
sections.load('items');
|
|
696
745
|
await ctx.sync();
|
|
697
|
-
const
|
|
746
|
+
const idx = p.sectionIndex || 0;
|
|
747
|
+
if (idx < 0 || idx >= sections.items.length) throw new Error('Section index out of range. Document has ' + sections.items.length + ' section(s) (0-indexed).');
|
|
748
|
+
const section = sections.items[idx];
|
|
698
749
|
const target = p.type === 'footer' ? section.getFooter(p.headerType || 'Primary') : section.getHeader(p.headerType || 'Primary');
|
|
699
750
|
target.load('text');
|
|
700
751
|
await ctx.sync();
|
|
@@ -705,7 +756,9 @@ commands.setHeaderFooter = (p) => Word.run(async (ctx) => {
|
|
|
705
756
|
const sections = ctx.document.sections;
|
|
706
757
|
sections.load('items');
|
|
707
758
|
await ctx.sync();
|
|
708
|
-
const
|
|
759
|
+
const idx = p.sectionIndex || 0;
|
|
760
|
+
if (idx < 0 || idx >= sections.items.length) throw new Error('Section index out of range. Document has ' + sections.items.length + ' section(s) (0-indexed).');
|
|
761
|
+
const section = sections.items[idx];
|
|
709
762
|
const target = p.type === 'footer' ? section.getFooter(p.headerType || 'Primary') : section.getHeader(p.headerType || 'Primary');
|
|
710
763
|
target.clear();
|
|
711
764
|
target.insertText(p.text, Word.InsertLocation.start);
|
|
@@ -715,6 +768,7 @@ commands.setHeaderFooter = (p) => Word.run(async (ctx) => {
|
|
|
715
768
|
|
|
716
769
|
// == IMAGES ==
|
|
717
770
|
commands.insertImage = (p) => Word.run(async (ctx) => {
|
|
771
|
+
if (!p.base64 || typeof p.base64 !== 'string' || p.base64.trim() === '') throw new Error('Invalid image data. Ensure the base64 string is a valid PNG, JPEG, or GIF image.');
|
|
718
772
|
const body = ctx.document.body;
|
|
719
773
|
const picture = body.insertInlinePictureFromBase64(p.base64, p.location || 'End');
|
|
720
774
|
if (p.width) picture.width = p.width;
|
|
@@ -757,15 +811,24 @@ commands.getBookmarks = () => Word.run(async (ctx) => {
|
|
|
757
811
|
});
|
|
758
812
|
|
|
759
813
|
commands.insertBookmark = (p) => Word.run(async (ctx) => {
|
|
814
|
+
if (p.occurrence !== undefined && p.occurrence < 0) throw new Error('occurrence must be non-negative (0-indexed)');
|
|
760
815
|
const results = ctx.document.body.search(p.anchorText, { matchCase: p.matchCase || false });
|
|
761
816
|
results.load('text');
|
|
817
|
+
// Check for existing bookmark with same name
|
|
818
|
+
const range = ctx.document.body.getRange();
|
|
819
|
+
const existingBookmarks = range.getBookmarks(true, true);
|
|
762
820
|
await ctx.sync();
|
|
821
|
+
const isDuplicate = existingBookmarks.value && existingBookmarks.value.includes(p.name);
|
|
763
822
|
if (results.items.length === 0) throw new Error('Anchor not found: ' + p.anchorText);
|
|
764
|
-
const
|
|
823
|
+
const idx = p.occurrence || 0;
|
|
824
|
+
if (idx >= results.items.length) throw new Error('Occurrence ' + idx + ' not found (only ' + results.items.length + ' match' + (results.items.length === 1 ? '' : 'es') + ')');
|
|
825
|
+
const target = results.items[idx];
|
|
765
826
|
target.insertBookmark(p.name);
|
|
766
827
|
target.getRange('End').select();
|
|
767
828
|
await ctx.sync();
|
|
768
|
-
|
|
829
|
+
const result = { success: true };
|
|
830
|
+
if (isDuplicate) result.warning = 'Bookmark "' + p.name + '" already existed and was moved to the new location.';
|
|
831
|
+
return result;
|
|
769
832
|
});
|
|
770
833
|
|
|
771
834
|
commands.deleteBookmark = (p) => Word.run(async (ctx) => {
|
|
@@ -805,7 +868,24 @@ commands.getCoauthors = () => Word.run(async (ctx) => {
|
|
|
805
868
|
commands.saveDocument = () => Word.run(async (ctx) => {
|
|
806
869
|
ctx.document.save();
|
|
807
870
|
await ctx.sync();
|
|
808
|
-
|
|
871
|
+
// Check if document has a file path (has been saved to a location)
|
|
872
|
+
let path = null;
|
|
873
|
+
try {
|
|
874
|
+
const fileProps = await new Promise((resolve) => {
|
|
875
|
+
Office.context.document.getFilePropertiesAsync(resolve);
|
|
876
|
+
});
|
|
877
|
+
if (fileProps.status === Office.AsyncResultStatus.Succeeded && fileProps.value.url) {
|
|
878
|
+
const url = fileProps.value.url;
|
|
879
|
+
if (/\.(docx?|docm|dotx?|dotm)$/i.test(url)) {
|
|
880
|
+
path = url;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
} catch (_e) {}
|
|
884
|
+
const result = { success: true };
|
|
885
|
+
if (!path) {
|
|
886
|
+
result.warning = 'Document has no file path — use Save As in Word to set a location.';
|
|
887
|
+
}
|
|
888
|
+
return result;
|
|
809
889
|
});
|
|
810
890
|
|
|
811
891
|
commands.clearDocument = () => Word.run(async (ctx) => {
|
|
@@ -897,6 +977,8 @@ commands.insertSectionBreak = (p) => Word.run(async (ctx) => {
|
|
|
897
977
|
// == HYPERLINKS ==
|
|
898
978
|
commands.insertHyperlink = (p) => Word.run(async (ctx) => {
|
|
899
979
|
if (!p.url || !/^https?:\/\/.+/i.test(p.url)) throw new Error('URL must be a valid HTTP or HTTPS URL (e.g. https://example.com)');
|
|
980
|
+
// Reject URLs containing characters that must be percent-encoded (RFC 3986 unsafe chars)
|
|
981
|
+
if (/[<>"{}|\\^`]/.test(p.url)) throw new Error('Malformed URL: "' + p.url + '". URL contains invalid characters that must be percent-encoded.');
|
|
900
982
|
const results = ctx.document.body.search(p.anchorText, { matchCase: p.matchCase || false });
|
|
901
983
|
results.load('text');
|
|
902
984
|
await ctx.sync();
|
|
@@ -952,7 +1034,9 @@ commands.setPageLayout = (p) => Word.run(async (ctx) => {
|
|
|
952
1034
|
const sections = ctx.document.sections;
|
|
953
1035
|
sections.load('items');
|
|
954
1036
|
await ctx.sync();
|
|
955
|
-
const
|
|
1037
|
+
const idx = p.sectionIndex || 0;
|
|
1038
|
+
if (idx < 0 || idx >= sections.items.length) throw new Error('Section index out of range. Document has ' + sections.items.length + ' section(s) (0-indexed).');
|
|
1039
|
+
const section = sections.items[idx];
|
|
956
1040
|
const pageSetup = section.pageSetup;
|
|
957
1041
|
if (p.orientation) pageSetup.orientation = p.orientation; // 'Portrait' or 'Landscape'
|
|
958
1042
|
if (p.topMargin !== undefined) pageSetup.topMargin = p.topMargin;
|
|
@@ -968,7 +1052,9 @@ commands.getPageLayout = (p) => Word.run(async (ctx) => {
|
|
|
968
1052
|
const sections = ctx.document.sections;
|
|
969
1053
|
sections.load('items');
|
|
970
1054
|
await ctx.sync();
|
|
971
|
-
const
|
|
1055
|
+
const idx = p.sectionIndex || 0;
|
|
1056
|
+
if (idx < 0 || idx >= sections.items.length) throw new Error('Section index out of range. Document has ' + sections.items.length + ' section(s) (0-indexed).');
|
|
1057
|
+
const section = sections.items[idx];
|
|
972
1058
|
const ps = section.pageSetup;
|
|
973
1059
|
ps.load('orientation,topMargin,bottomMargin,leftMargin,rightMargin,paperSize,headerDistance,footerDistance');
|
|
974
1060
|
await ctx.sync();
|
|
@@ -1020,6 +1106,7 @@ commands.deleteCustomProperty = (p) => Word.run(async (ctx) => {
|
|
|
1020
1106
|
|
|
1021
1107
|
// == ADVANCED TEXT INSERTION ==
|
|
1022
1108
|
commands.insertHtml = (p) => Word.run(async (ctx) => {
|
|
1109
|
+
if (!p.html || typeof p.html !== 'string' || p.html.trim() === '') throw new Error('html parameter must be a non-empty string');
|
|
1023
1110
|
const body = ctx.document.body;
|
|
1024
1111
|
const loc = p.location || 'End';
|
|
1025
1112
|
const range = body.insertHtml(p.html, loc);
|
|
@@ -1095,17 +1182,33 @@ commands.setParagraphSpacing = (p) => Word.run(async (ctx) => {
|
|
|
1095
1182
|
|
|
1096
1183
|
// == BOOKMARK NAVIGATION ==
|
|
1097
1184
|
commands.goToBookmark = (p) => Word.run(async (ctx) => {
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1185
|
+
let range;
|
|
1186
|
+
try {
|
|
1187
|
+
range = ctx.document.getBookmarkRange(p.name);
|
|
1188
|
+
range.load('text');
|
|
1189
|
+
range.select();
|
|
1190
|
+
await ctx.sync();
|
|
1191
|
+
} catch (e) {
|
|
1192
|
+
if (e.message && e.message.includes('ItemNotFound')) {
|
|
1193
|
+
throw new Error('Bookmark not found: ' + p.name);
|
|
1194
|
+
}
|
|
1195
|
+
throw e;
|
|
1196
|
+
}
|
|
1102
1197
|
return { success: true, text: range.text };
|
|
1103
1198
|
});
|
|
1104
1199
|
|
|
1105
1200
|
commands.getBookmarkText = (p) => Word.run(async (ctx) => {
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1201
|
+
let range;
|
|
1202
|
+
try {
|
|
1203
|
+
range = ctx.document.getBookmarkRange(p.name);
|
|
1204
|
+
range.load('text');
|
|
1205
|
+
await ctx.sync();
|
|
1206
|
+
} catch (e) {
|
|
1207
|
+
if (e.message && e.message.includes('ItemNotFound')) {
|
|
1208
|
+
throw new Error('Bookmark not found: ' + p.name);
|
|
1209
|
+
}
|
|
1210
|
+
throw e;
|
|
1211
|
+
}
|
|
1109
1212
|
return { text: range.text };
|
|
1110
1213
|
});
|
|
1111
1214
|
|
|
@@ -1126,14 +1229,25 @@ commands.getSections = () => Word.run(async (ctx) => {
|
|
|
1126
1229
|
|
|
1127
1230
|
// == TABLE ADVANCED ==
|
|
1128
1231
|
commands.mergeTableCells = (p) => Word.run(async (ctx) => {
|
|
1232
|
+
if (p.topRow < 0 || p.bottomRow < 0 || p.firstCell < 0 || p.lastCell < 0) throw new Error('All cell indices must be non-negative');
|
|
1129
1233
|
if (p.topRow > p.bottomRow) throw new Error('topRow (' + p.topRow + ') must be less than or equal to bottomRow (' + p.bottomRow + ')');
|
|
1130
1234
|
if (p.firstCell > p.lastCell) throw new Error('firstCell (' + p.firstCell + ') must be less than or equal to lastCell (' + p.lastCell + ')');
|
|
1235
|
+
if (p.tableIndex < 0) throw new Error('Table index must be non-negative');
|
|
1131
1236
|
const tables = ctx.document.body.tables;
|
|
1132
1237
|
tables.load('rowCount');
|
|
1133
1238
|
await ctx.sync();
|
|
1134
1239
|
if (p.tableIndex >= tables.items.length) throw new Error('Table index out of range');
|
|
1135
|
-
tables.items[p.tableIndex]
|
|
1136
|
-
|
|
1240
|
+
const table = tables.items[p.tableIndex];
|
|
1241
|
+
if (p.bottomRow >= table.rowCount) throw new Error('bottomRow (' + p.bottomRow + ') exceeds table row count (' + table.rowCount + ')');
|
|
1242
|
+
try {
|
|
1243
|
+
table.mergeCells(p.topRow, p.firstCell, p.bottomRow, p.lastCell);
|
|
1244
|
+
await ctx.sync();
|
|
1245
|
+
} catch (e) {
|
|
1246
|
+
if (e.message && e.message.includes('InvalidArgument')) {
|
|
1247
|
+
throw new Error('Cannot merge cells: range is out of bounds. Table has ' + table.rowCount + ' rows. Use word_get_table_data to inspect the table.');
|
|
1248
|
+
}
|
|
1249
|
+
throw e;
|
|
1250
|
+
}
|
|
1137
1251
|
return { success: true };
|
|
1138
1252
|
});
|
|
1139
1253
|
|
|
@@ -1151,12 +1265,20 @@ commands.splitTableCell = (p) => Word.run(async (ctx) => {
|
|
|
1151
1265
|
});
|
|
1152
1266
|
|
|
1153
1267
|
commands.setTableStyle = (p) => Word.run(async (ctx) => {
|
|
1268
|
+
if (p.tableIndex < 0) throw new Error('Table index must be non-negative');
|
|
1154
1269
|
const tables = ctx.document.body.tables;
|
|
1155
1270
|
tables.load('rowCount');
|
|
1156
1271
|
await ctx.sync();
|
|
1157
1272
|
if (p.tableIndex >= tables.items.length) throw new Error('Table index out of range');
|
|
1158
|
-
|
|
1159
|
-
|
|
1273
|
+
try {
|
|
1274
|
+
tables.items[p.tableIndex].style = p.style;
|
|
1275
|
+
await ctx.sync();
|
|
1276
|
+
} catch (e) {
|
|
1277
|
+
if (e.message && e.message.includes('InvalidArgument')) {
|
|
1278
|
+
throw new Error('Table style not found: "' + p.style + '". Use a built-in style name like "Grid Table 4 - Accent 1".');
|
|
1279
|
+
}
|
|
1280
|
+
throw e;
|
|
1281
|
+
}
|
|
1160
1282
|
return { success: true };
|
|
1161
1283
|
});
|
|
1162
1284
|
|
|
@@ -1283,6 +1405,7 @@ commands.getImages = () => Word.run(async (ctx) => {
|
|
|
1283
1405
|
});
|
|
1284
1406
|
|
|
1285
1407
|
commands.deleteImage = (p) => Word.run(async (ctx) => {
|
|
1408
|
+
if (p.index < 0) throw new Error('Index must be non-negative');
|
|
1286
1409
|
const pics = ctx.document.body.inlinePictures;
|
|
1287
1410
|
pics.load('altTextDescription');
|
|
1288
1411
|
await ctx.sync();
|
|
@@ -1345,6 +1468,7 @@ commands.removeHyperlink = (p) => Word.run(async (ctx) => {
|
|
|
1345
1468
|
|
|
1346
1469
|
// == FOOTNOTE/ENDNOTE MANAGEMENT ==
|
|
1347
1470
|
commands.insertFootnoteAtIndex = (p) => Word.run(async (ctx) => {
|
|
1471
|
+
if (p.paragraphIndex < 0) throw new Error('Index must be non-negative');
|
|
1348
1472
|
const paragraphs = ctx.document.body.paragraphs;
|
|
1349
1473
|
paragraphs.load('text');
|
|
1350
1474
|
await ctx.sync();
|
|
@@ -1358,6 +1482,7 @@ commands.insertFootnoteAtIndex = (p) => Word.run(async (ctx) => {
|
|
|
1358
1482
|
});
|
|
1359
1483
|
|
|
1360
1484
|
commands.deleteFootnote = (p) => Word.run(async (ctx) => {
|
|
1485
|
+
if (p.index < 0) throw new Error('Index must be non-negative');
|
|
1361
1486
|
const footnotes = ctx.document.body.footnotes;
|
|
1362
1487
|
footnotes.load('items');
|
|
1363
1488
|
await ctx.sync();
|
|
@@ -1368,6 +1493,7 @@ commands.deleteFootnote = (p) => Word.run(async (ctx) => {
|
|
|
1368
1493
|
});
|
|
1369
1494
|
|
|
1370
1495
|
commands.deleteEndnote = (p) => Word.run(async (ctx) => {
|
|
1496
|
+
if (p.index < 0) throw new Error('Index must be non-negative');
|
|
1371
1497
|
const endnotes = ctx.document.body.endnotes;
|
|
1372
1498
|
endnotes.load('items');
|
|
1373
1499
|
await ctx.sync();
|
|
@@ -1379,6 +1505,7 @@ commands.deleteEndnote = (p) => Word.run(async (ctx) => {
|
|
|
1379
1505
|
|
|
1380
1506
|
// == LIST OPERATIONS ==
|
|
1381
1507
|
commands.getListInfo = (p) => Word.run(async (ctx) => {
|
|
1508
|
+
if (p.index < 0) throw new Error('Index must be non-negative');
|
|
1382
1509
|
const paragraphs = ctx.document.body.paragraphs;
|
|
1383
1510
|
paragraphs.load('text,isListItem');
|
|
1384
1511
|
await ctx.sync();
|
|
@@ -1395,6 +1522,7 @@ commands.getListInfo = (p) => Word.run(async (ctx) => {
|
|
|
1395
1522
|
|
|
1396
1523
|
commands.setListLevel = (p) => Word.run(async (ctx) => {
|
|
1397
1524
|
if (p.index < 0) throw new Error('Index must be non-negative');
|
|
1525
|
+
if (p.level < 0 || p.level > 8) throw new Error('level must be between 0 and 8 (inclusive)');
|
|
1398
1526
|
const paragraphs = ctx.document.body.paragraphs;
|
|
1399
1527
|
paragraphs.load('text,isListItem');
|
|
1400
1528
|
await ctx.sync();
|