@eigenpal/docx-editor-agents 0.0.28

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/dist/index.js ADDED
@@ -0,0 +1,896 @@
1
+ import { parseDocx, isHeadingStyle, parseHeadingLevel, getRunText, getHyperlinkText } from '@eigenpal/docx-core/headless';
2
+
3
+ // src/DocxReviewer.ts
4
+ function isTrackedChange(item) {
5
+ return item.type === "insertion" || item.type === "deletion" || item.type === "moveFrom" || item.type === "moveTo";
6
+ }
7
+ function getTrackedChangeText(content) {
8
+ const parts = [];
9
+ for (const item of content) {
10
+ if (item.type === "run") {
11
+ parts.push(getRunText(item));
12
+ } else if (item.type === "hyperlink") {
13
+ parts.push(getHyperlinkText(item));
14
+ }
15
+ }
16
+ return parts.join("");
17
+ }
18
+ function getParagraphAtIndex(body, paragraphIndex) {
19
+ let index = 0;
20
+ for (const block of body.content) {
21
+ if (block.type === "paragraph") {
22
+ if (index === paragraphIndex) return block;
23
+ index++;
24
+ } else if (block.type === "table") {
25
+ for (const row of block.rows) {
26
+ for (const cell of row.cells) {
27
+ for (const cellBlock of cell.content) {
28
+ if (cellBlock.type === "paragraph") {
29
+ if (index === paragraphIndex) return cellBlock;
30
+ index++;
31
+ }
32
+ }
33
+ }
34
+ }
35
+ } else {
36
+ index++;
37
+ }
38
+ }
39
+ throw new Error(`Paragraph index ${paragraphIndex} out of bounds (max: ${index - 1})`);
40
+ }
41
+ function forEachParagraph(body, fn) {
42
+ let index = 0;
43
+ for (const block of body.content) {
44
+ if (block.type === "paragraph") {
45
+ if (fn(block, index) === false) return;
46
+ index++;
47
+ } else if (block.type === "table") {
48
+ for (const row of block.rows) {
49
+ for (const cell of row.cells) {
50
+ for (const cellBlock of cell.content) {
51
+ if (cellBlock.type === "paragraph") {
52
+ if (fn(cellBlock, index) === false) return;
53
+ index++;
54
+ }
55
+ }
56
+ }
57
+ }
58
+ } else {
59
+ index++;
60
+ }
61
+ }
62
+ }
63
+
64
+ // src/content.ts
65
+ function getContent(body, options = {}) {
66
+ const {
67
+ fromIndex,
68
+ toIndex,
69
+ includeTrackedChanges = true,
70
+ includeCommentAnchors = true
71
+ } = options;
72
+ const blocks = [];
73
+ let index = 0;
74
+ for (const block of body.content) {
75
+ if (block.type === "paragraph") {
76
+ if (isInRange(index, fromIndex, toIndex)) {
77
+ blocks.push(
78
+ buildParagraphBlock(block, index, includeTrackedChanges, includeCommentAnchors)
79
+ );
80
+ }
81
+ index++;
82
+ } else if (block.type === "table") {
83
+ if (isInRange(index, fromIndex, toIndex)) {
84
+ blocks.push(buildTableBlock(block, index, includeTrackedChanges, includeCommentAnchors));
85
+ }
86
+ for (const row of block.rows) {
87
+ for (const cell of row.cells) {
88
+ for (const cellBlock of cell.content) {
89
+ if (cellBlock.type === "paragraph") {
90
+ index++;
91
+ }
92
+ }
93
+ }
94
+ }
95
+ } else {
96
+ index++;
97
+ }
98
+ }
99
+ return blocks;
100
+ }
101
+ function isInRange(index, from, to) {
102
+ return (from === void 0 || index >= from) && (to === void 0 || index <= to);
103
+ }
104
+ function buildParagraphBlock(para, index, includeTrackedChanges, includeCommentAnchors) {
105
+ const text = buildParagraphText(para, includeTrackedChanges, includeCommentAnchors);
106
+ const styleId = para.formatting?.styleId;
107
+ if (isHeadingStyle(styleId)) {
108
+ return {
109
+ type: "heading",
110
+ index,
111
+ level: parseHeadingLevel(styleId) ?? 1,
112
+ text
113
+ };
114
+ }
115
+ if (para.listRendering) {
116
+ return {
117
+ type: "list-item",
118
+ index,
119
+ text,
120
+ listLevel: para.listRendering.level ?? 0,
121
+ listType: para.listRendering.isBullet ? "bullet" : "number"
122
+ };
123
+ }
124
+ return { type: "paragraph", index, text };
125
+ }
126
+ function buildTableBlock(table, index, includeTrackedChanges, includeCommentAnchors) {
127
+ const rows = [];
128
+ for (const row of table.rows) {
129
+ const cells = [];
130
+ for (const cell of row.cells) {
131
+ const cellTexts = [];
132
+ for (const block of cell.content) {
133
+ if (block.type === "paragraph") {
134
+ cellTexts.push(buildParagraphText(block, includeTrackedChanges, includeCommentAnchors));
135
+ }
136
+ }
137
+ cells.push(cellTexts.join("\n"));
138
+ }
139
+ rows.push(cells);
140
+ }
141
+ return { type: "table", index, rows };
142
+ }
143
+ function formatContentForLLM(blocks) {
144
+ const lines = [];
145
+ for (const block of blocks) {
146
+ switch (block.type) {
147
+ case "heading":
148
+ lines.push(`[${block.index}] (h${block.level}) ${block.text}`);
149
+ break;
150
+ case "paragraph":
151
+ lines.push(`[${block.index}] ${block.text}`);
152
+ break;
153
+ case "list-item": {
154
+ const indent = " ".repeat(block.listLevel);
155
+ const bullet = block.listType === "bullet" ? "\u2022" : "-";
156
+ lines.push(`[${block.index}] ${indent}${bullet} ${block.text}`);
157
+ break;
158
+ }
159
+ case "table": {
160
+ let idx = block.index;
161
+ for (let r = 0; r < block.rows.length; r++) {
162
+ for (let c = 0; c < block.rows[r].length; c++) {
163
+ const cellText = block.rows[r][c];
164
+ const paras = cellText.split("\n");
165
+ for (const para of paras) {
166
+ lines.push(`[${idx}] (table, row ${r + 1}, col ${c + 1}) ${para}`);
167
+ idx++;
168
+ }
169
+ }
170
+ }
171
+ break;
172
+ }
173
+ }
174
+ }
175
+ return lines.join("\n");
176
+ }
177
+ function buildParagraphText(para, includeTrackedChanges, includeCommentAnchors) {
178
+ const parts = [];
179
+ const activeCommentIds = /* @__PURE__ */ new Set();
180
+ for (const item of para.content) {
181
+ if (item.type === "commentRangeStart" && includeCommentAnchors) {
182
+ activeCommentIds.add(item.id);
183
+ parts.push(`[comment:${item.id}]`);
184
+ continue;
185
+ }
186
+ if (item.type === "commentRangeEnd" && includeCommentAnchors) {
187
+ if (activeCommentIds.has(item.id)) {
188
+ activeCommentIds.delete(item.id);
189
+ parts.push("[/comment]");
190
+ }
191
+ continue;
192
+ }
193
+ if (item.type === "run") {
194
+ parts.push(getRunText(item));
195
+ } else if (item.type === "hyperlink") {
196
+ parts.push(getHyperlinkText(item));
197
+ } else if (isTrackedChange(item)) {
198
+ const text = getTrackedChangeText(item.content);
199
+ if (item.type === "insertion" || item.type === "moveTo") {
200
+ if (includeTrackedChanges) {
201
+ parts.push(`[+${text}+]{by:${item.info.author}}`);
202
+ } else {
203
+ parts.push(text);
204
+ }
205
+ } else {
206
+ if (includeTrackedChanges) {
207
+ parts.push(`[-${text}-]{by:${item.info.author}}`);
208
+ }
209
+ }
210
+ }
211
+ }
212
+ return parts.join("");
213
+ }
214
+
215
+ // src/errors.ts
216
+ var TextNotFoundError = class extends Error {
217
+ constructor(search, paragraphIndex) {
218
+ const location = paragraphIndex !== void 0 ? ` in paragraph ${paragraphIndex}` : " in document";
219
+ super(`Text not found${location}: "${search}"`);
220
+ this.name = "TextNotFoundError";
221
+ }
222
+ };
223
+ var ChangeNotFoundError = class extends Error {
224
+ constructor(id) {
225
+ super(`Tracked change not found: id=${id}`);
226
+ this.name = "ChangeNotFoundError";
227
+ }
228
+ };
229
+ var CommentNotFoundError = class extends Error {
230
+ constructor(id) {
231
+ super(`Comment not found: id=${id}`);
232
+ this.name = "CommentNotFoundError";
233
+ }
234
+ };
235
+
236
+ // src/textSearch.ts
237
+ function flattenRuns(paragraph) {
238
+ const result = [];
239
+ let pos = 0;
240
+ for (let ci = 0; ci < paragraph.content.length; ci++) {
241
+ const item = paragraph.content[ci];
242
+ if (item.type === "run") {
243
+ const text = getRunText(item);
244
+ result.push({ contentIndex: ci, run: item, text, startPos: pos });
245
+ pos += text.length;
246
+ } else if (item.type === "hyperlink") {
247
+ for (let hi = 0; hi < item.children.length; hi++) {
248
+ const child = item.children[hi];
249
+ if (child.type === "run") {
250
+ const text = getRunText(child);
251
+ result.push({ contentIndex: ci, run: child, text, startPos: pos });
252
+ pos += text.length;
253
+ }
254
+ }
255
+ } else if (isTrackedChange(item)) {
256
+ for (let ri = 0; ri < item.content.length; ri++) {
257
+ const child = item.content[ri];
258
+ if (child.type === "run") {
259
+ const text = getRunText(child);
260
+ result.push({ contentIndex: ci, run: child, text, startPos: pos });
261
+ pos += text.length;
262
+ } else if (child.type === "hyperlink") {
263
+ for (const hc of child.children) {
264
+ if (hc.type === "run") {
265
+ const text = getRunText(hc);
266
+ result.push({ contentIndex: ci, run: hc, text, startPos: pos });
267
+ pos += text.length;
268
+ }
269
+ }
270
+ }
271
+ }
272
+ }
273
+ }
274
+ return result;
275
+ }
276
+ function getParagraphPlainText(paragraph) {
277
+ return flattenRuns(paragraph).map((r) => r.text).join("");
278
+ }
279
+ function findTextInParagraph(paragraph, search, paragraphIndex) {
280
+ const runs = flattenRuns(paragraph);
281
+ const fullText = runs.map((r) => r.text).join("");
282
+ const match = findMatch(fullText, search);
283
+ if (!match) throw new TextNotFoundError(search, paragraphIndex);
284
+ let startRunIdx = -1;
285
+ let startOffset = 0;
286
+ for (let i = 0; i < runs.length; i++) {
287
+ if (match.start < runs[i].startPos + runs[i].text.length) {
288
+ startRunIdx = i;
289
+ startOffset = match.start - runs[i].startPos;
290
+ break;
291
+ }
292
+ }
293
+ let endRunIdx = -1;
294
+ let endOffset = 0;
295
+ for (let i = runs.length - 1; i >= 0; i--) {
296
+ if (match.end > runs[i].startPos) {
297
+ endRunIdx = i;
298
+ endOffset = match.end - runs[i].startPos;
299
+ break;
300
+ }
301
+ }
302
+ if (startRunIdx === -1 || endRunIdx === -1) {
303
+ throw new TextNotFoundError(search, paragraphIndex);
304
+ }
305
+ return {
306
+ startRunIndex: runs[startRunIdx].contentIndex,
307
+ startOffset,
308
+ endRunIndex: runs[endRunIdx].contentIndex,
309
+ endOffset
310
+ };
311
+ }
312
+ function isolateMatchedText(paragraph, search, paragraphIndex) {
313
+ const result = findTextInParagraph(paragraph, search, paragraphIndex);
314
+ let { startRunIndex, endRunIndex } = result;
315
+ const { startOffset, endOffset } = result;
316
+ const endItem = paragraph.content[endRunIndex];
317
+ if (endItem.type === "run") {
318
+ const endText = getRunText(endItem);
319
+ if (endOffset < endText.length) {
320
+ const afterRun = makeRunWithText(endText.slice(endOffset), endItem);
321
+ setRunText(endItem, endText.slice(0, endOffset));
322
+ paragraph.content.splice(endRunIndex + 1, 0, afterRun);
323
+ }
324
+ }
325
+ const startItem = paragraph.content[startRunIndex];
326
+ if (startItem.type === "run" && startOffset > 0) {
327
+ const startText = getRunText(startItem);
328
+ const beforeRun = makeRunWithText(startText.slice(0, startOffset), startItem);
329
+ setRunText(startItem, startText.slice(startOffset));
330
+ paragraph.content.splice(startRunIndex, 0, beforeRun);
331
+ startRunIndex++;
332
+ endRunIndex++;
333
+ }
334
+ return { startIndex: startRunIndex, endIndex: endRunIndex };
335
+ }
336
+ function makeRunWithText(text, template) {
337
+ return {
338
+ type: "run",
339
+ content: [{ type: "text", text }],
340
+ formatting: template.formatting ? { ...template.formatting } : void 0
341
+ };
342
+ }
343
+ function setRunText(run, text) {
344
+ const textContent = run.content.find((c) => c.type === "text");
345
+ if (textContent) {
346
+ textContent.text = text;
347
+ }
348
+ }
349
+ function normalize(original) {
350
+ const chars = [];
351
+ const posMap = [];
352
+ let prevSpace = true;
353
+ for (let i = 0; i < original.length; i++) {
354
+ let ch = original[i];
355
+ if ("\u200B\u200C\u200D\uFEFF\xAD".includes(ch)) continue;
356
+ if ("\u201C\u201D\u201E\u201F".includes(ch)) ch = '"';
357
+ if ("\u2018\u2019\u201A\u201B".includes(ch)) ch = "'";
358
+ if ("\u2013\u2014\u2012\u2015".includes(ch)) ch = "-";
359
+ if (ch === "\u2026") {
360
+ chars.push(".", ".", ".");
361
+ posMap.push(i, i, i);
362
+ prevSpace = false;
363
+ continue;
364
+ }
365
+ if (/\s/.test(ch) || ch === "\xA0") {
366
+ if (!prevSpace) {
367
+ chars.push(" ");
368
+ posMap.push(i);
369
+ prevSpace = true;
370
+ }
371
+ continue;
372
+ }
373
+ chars.push(ch.toLowerCase());
374
+ posMap.push(i);
375
+ prevSpace = false;
376
+ }
377
+ if (chars.length > 0 && chars[chars.length - 1] === " ") {
378
+ chars.pop();
379
+ posMap.pop();
380
+ }
381
+ return { text: chars.join(""), posMap };
382
+ }
383
+ function mapBack(norm, idx, len, original) {
384
+ const start = norm.posMap[idx];
385
+ let end = norm.posMap[idx + len - 1] + 1;
386
+ while (end < original.length && "\u200B\u200C\u200D\uFEFF\xAD".includes(original[end])) end++;
387
+ return { start, end };
388
+ }
389
+ function findMatch(text, search) {
390
+ if (!search || !text) return null;
391
+ const exact = text.indexOf(search);
392
+ if (exact !== -1) return { start: exact, end: exact + search.length };
393
+ const normText = normalize(text);
394
+ const normSearch = normalize(search);
395
+ if (!normSearch.text) return null;
396
+ const idx = normText.text.indexOf(normSearch.text);
397
+ if (idx !== -1) return mapBack(normText, idx, normSearch.text.length, text);
398
+ const words = normSearch.text.split(" ");
399
+ if (words.length >= 3) {
400
+ for (let drop = 1; drop <= Math.min(2, words.length - 2); drop++) {
401
+ const trimmed = words.slice(0, -drop).join(" ");
402
+ const trimIdx = normText.text.indexOf(trimmed);
403
+ if (trimIdx !== -1) return mapBack(normText, trimIdx, trimmed.length, text);
404
+ }
405
+ }
406
+ return null;
407
+ }
408
+
409
+ // src/discovery.ts
410
+ function getChanges(body, filter) {
411
+ const grouped = /* @__PURE__ */ new Map();
412
+ forEachParagraph(body, (para, paragraphIndex) => {
413
+ let context = null;
414
+ for (const item of para.content) {
415
+ if (isTrackedChange(item)) {
416
+ if (context === null) context = getParagraphPlainText(para);
417
+ const text = getTrackedChangeText(item.content);
418
+ const id = item.info.id;
419
+ const existing = grouped.get(id);
420
+ if (existing && existing.paragraphIndex === paragraphIndex) {
421
+ existing.text += text;
422
+ } else {
423
+ grouped.set(id, {
424
+ id,
425
+ type: item.type,
426
+ author: item.info.author,
427
+ date: item.info.date ?? null,
428
+ text,
429
+ context,
430
+ paragraphIndex
431
+ });
432
+ }
433
+ }
434
+ }
435
+ });
436
+ const changes = Array.from(grouped.values());
437
+ return changes.filter((c) => {
438
+ if (filter?.author && c.author !== filter.author) return false;
439
+ if (filter?.type && c.type !== filter.type) return false;
440
+ return true;
441
+ });
442
+ }
443
+ function getComments(body, filter) {
444
+ const comments = body.comments ?? [];
445
+ if (comments.length === 0) return [];
446
+ const anchoredTextMap = buildAnchoredTextMap(body);
447
+ const topLevel = [];
448
+ const repliesByParent = /* @__PURE__ */ new Map();
449
+ for (const c of comments) {
450
+ if (c.parentId !== void 0) {
451
+ const existing = repliesByParent.get(c.parentId) ?? [];
452
+ existing.push(c);
453
+ repliesByParent.set(c.parentId, existing);
454
+ } else {
455
+ topLevel.push(c);
456
+ }
457
+ }
458
+ const result = topLevel.map((c) => {
459
+ const anchor = anchoredTextMap.get(c.id);
460
+ const replies = (repliesByParent.get(c.id) ?? []).map((r) => ({
461
+ id: r.id,
462
+ author: r.author,
463
+ date: r.date ?? null,
464
+ text: getCommentText(r)
465
+ }));
466
+ return {
467
+ id: c.id,
468
+ author: c.author,
469
+ date: c.date ?? null,
470
+ text: getCommentText(c),
471
+ anchoredText: anchor?.text ?? "",
472
+ paragraphIndex: anchor?.paragraphIndex ?? -1,
473
+ replies,
474
+ done: c.done ?? false
475
+ };
476
+ });
477
+ return result.filter((c) => {
478
+ if (filter?.author && c.author !== filter.author) return false;
479
+ if (filter?.done !== void 0 && c.done !== filter.done) return false;
480
+ return true;
481
+ });
482
+ }
483
+ function getCommentText(comment) {
484
+ return comment.content.map((para) => getParagraphPlainText(para)).join("\n");
485
+ }
486
+ function buildAnchoredTextMap(body) {
487
+ const result = /* @__PURE__ */ new Map();
488
+ const openRanges = /* @__PURE__ */ new Map();
489
+ forEachParagraph(body, (para, paragraphIndex) => {
490
+ for (const item of para.content) {
491
+ if (item.type === "commentRangeStart") {
492
+ openRanges.set(item.id, { paragraphIndex, parts: [] });
493
+ } else if (item.type === "commentRangeEnd") {
494
+ const open = openRanges.get(item.id);
495
+ if (open) {
496
+ result.set(item.id, { text: open.parts.join(""), paragraphIndex: open.paragraphIndex });
497
+ openRanges.delete(item.id);
498
+ }
499
+ } else if (item.type === "run") {
500
+ const text = getRunText(item);
501
+ for (const open of openRanges.values()) {
502
+ open.parts.push(text);
503
+ }
504
+ } else if (item.type === "hyperlink") {
505
+ const text = item.children.filter((c) => c.type === "run").map(getRunText).join("");
506
+ for (const open of openRanges.values()) {
507
+ open.parts.push(text);
508
+ }
509
+ } else if (isTrackedChange(item)) {
510
+ const text = getTrackedChangeText(item.content);
511
+ for (const open of openRanges.values()) {
512
+ open.parts.push(text);
513
+ }
514
+ }
515
+ }
516
+ });
517
+ return result;
518
+ }
519
+
520
+ // src/comments.ts
521
+ function addComment(body, options) {
522
+ const { paragraphIndex, author = "AI", text, search } = options;
523
+ const para = getParagraphAtIndex(body, paragraphIndex);
524
+ const existingIds = (body.comments ?? []).map((c) => c.id);
525
+ const newId = existingIds.length > 0 ? Math.max(...existingIds) + 1 : 1;
526
+ const comment = {
527
+ id: newId,
528
+ author,
529
+ date: (/* @__PURE__ */ new Date()).toISOString(),
530
+ content: [
531
+ {
532
+ type: "paragraph",
533
+ content: [{ type: "run", content: [{ type: "text", text }] }],
534
+ formatting: {}
535
+ }
536
+ ]
537
+ };
538
+ if (!body.comments) {
539
+ body.comments = [];
540
+ }
541
+ body.comments.push(comment);
542
+ if (search) {
543
+ const result = findTextInParagraph(para, search, paragraphIndex);
544
+ para.content.splice(result.endRunIndex + 1, 0, { type: "commentRangeEnd", id: newId });
545
+ para.content.splice(result.startRunIndex, 0, { type: "commentRangeStart", id: newId });
546
+ } else {
547
+ para.content.unshift({ type: "commentRangeStart", id: newId });
548
+ para.content.push({ type: "commentRangeEnd", id: newId });
549
+ }
550
+ return newId;
551
+ }
552
+ function replyTo(body, commentId, options) {
553
+ const comments = body.comments ?? [];
554
+ const parent = comments.find((c) => c.id === commentId);
555
+ if (!parent) {
556
+ throw new CommentNotFoundError(commentId);
557
+ }
558
+ const existingIds = comments.map((c) => c.id);
559
+ const newId = Math.max(...existingIds) + 1;
560
+ const reply = {
561
+ id: newId,
562
+ author: options.author ?? "AI",
563
+ date: (/* @__PURE__ */ new Date()).toISOString(),
564
+ parentId: commentId,
565
+ content: [
566
+ {
567
+ type: "paragraph",
568
+ content: [{ type: "run", content: [{ type: "text", text: options.text }] }],
569
+ formatting: {}
570
+ }
571
+ ]
572
+ };
573
+ comments.push(reply);
574
+ return newId;
575
+ }
576
+
577
+ // src/changes.ts
578
+ function acceptChange(body, id) {
579
+ if (!processChangeById(body, id, "accept")) {
580
+ throw new ChangeNotFoundError(id);
581
+ }
582
+ }
583
+ function rejectChange(body, id) {
584
+ if (!processChangeById(body, id, "reject")) {
585
+ throw new ChangeNotFoundError(id);
586
+ }
587
+ }
588
+ function acceptAll(body) {
589
+ return processAllChanges(body, "accept");
590
+ }
591
+ function rejectAll(body) {
592
+ return processAllChanges(body, "reject");
593
+ }
594
+ function processAllChanges(body, mode) {
595
+ let count = 0;
596
+ forEachParagraph(body, (para) => {
597
+ for (let i = para.content.length - 1; i >= 0; i--) {
598
+ const item = para.content[i];
599
+ if (isTrackedChange(item)) {
600
+ applyChangeAtIndex(para, i, item, mode);
601
+ count++;
602
+ }
603
+ }
604
+ });
605
+ return count;
606
+ }
607
+ function processChangeById(body, id, mode) {
608
+ let found = false;
609
+ forEachParagraph(body, (para) => {
610
+ for (let i = para.content.length - 1; i >= 0; i--) {
611
+ const item = para.content[i];
612
+ if (isTrackedChange(item) && item.info.id === id) {
613
+ applyChangeAtIndex(para, i, item, mode);
614
+ found = true;
615
+ }
616
+ }
617
+ if (found) return false;
618
+ });
619
+ return found;
620
+ }
621
+ function applyChangeAtIndex(para, index, item, mode) {
622
+ const keepContent = item.type === "insertion" && mode === "accept" || item.type === "deletion" && mode === "reject" || item.type === "moveTo" && mode === "accept" || item.type === "moveFrom" && mode === "reject";
623
+ if (keepContent) {
624
+ const runs = item.content;
625
+ para.content.splice(index, 1, ...runs);
626
+ } else {
627
+ para.content.splice(index, 1);
628
+ }
629
+ }
630
+ function proposeReplacement(body, options) {
631
+ const { paragraphIndex, search, author = "AI", replaceWith } = options;
632
+ const para = getParagraphAtIndex(body, paragraphIndex);
633
+ const { startIndex, endIndex } = isolateMatchedText(para, search, paragraphIndex);
634
+ const now = (/* @__PURE__ */ new Date()).toISOString();
635
+ const baseId = nextRevisionId(body);
636
+ const matchedContent = para.content.slice(startIndex, endIndex + 1);
637
+ const deletion = {
638
+ type: "deletion",
639
+ info: { id: baseId, author, date: now },
640
+ content: matchedContent
641
+ };
642
+ const insertion = {
643
+ type: "insertion",
644
+ info: { id: baseId + 1, author, date: now },
645
+ content: [{ type: "run", content: [{ type: "text", text: replaceWith }] }]
646
+ };
647
+ para.content.splice(startIndex, endIndex - startIndex + 1, deletion, insertion);
648
+ }
649
+ function proposeInsertion(body, options) {
650
+ const { paragraphIndex, author = "AI", insertText, position = "after", search } = options;
651
+ const para = getParagraphAtIndex(body, paragraphIndex);
652
+ const now = (/* @__PURE__ */ new Date()).toISOString();
653
+ const id = nextRevisionId(body);
654
+ const insertion = {
655
+ type: "insertion",
656
+ info: { id, author, date: now },
657
+ content: [{ type: "run", content: [{ type: "text", text: insertText }] }]
658
+ };
659
+ if (search) {
660
+ const { startIndex, endIndex } = isolateMatchedText(para, search, paragraphIndex);
661
+ const insertAt = position === "after" ? endIndex + 1 : startIndex;
662
+ para.content.splice(insertAt, 0, insertion);
663
+ } else {
664
+ if (position === "before") {
665
+ para.content.unshift(insertion);
666
+ } else {
667
+ para.content.push(insertion);
668
+ }
669
+ }
670
+ }
671
+ function proposeDeletion(body, options) {
672
+ const { paragraphIndex, search, author = "AI" } = options;
673
+ const para = getParagraphAtIndex(body, paragraphIndex);
674
+ const { startIndex, endIndex } = isolateMatchedText(para, search, paragraphIndex);
675
+ const now = (/* @__PURE__ */ new Date()).toISOString();
676
+ const id = nextRevisionId(body);
677
+ const matchedContent = para.content.slice(startIndex, endIndex + 1);
678
+ const deletion = {
679
+ type: "deletion",
680
+ info: { id, author, date: now },
681
+ content: matchedContent
682
+ };
683
+ para.content.splice(startIndex, endIndex - startIndex + 1, deletion);
684
+ }
685
+ var revisionIdCache = /* @__PURE__ */ new WeakMap();
686
+ function nextRevisionId(body) {
687
+ let maxId = revisionIdCache.get(body);
688
+ if (maxId === void 0) {
689
+ maxId = 0;
690
+ forEachParagraph(body, (para) => {
691
+ for (const item of para.content) {
692
+ if (isTrackedChange(item)) {
693
+ maxId = Math.max(maxId, item.info.id);
694
+ }
695
+ }
696
+ });
697
+ }
698
+ const next = maxId + 1;
699
+ revisionIdCache.set(body, next + 1);
700
+ return next;
701
+ }
702
+
703
+ // src/batch.ts
704
+ function applyReview(body, ops, defaultAuthor = "AI") {
705
+ const errors = [];
706
+ let accepted = 0;
707
+ let rejected = 0;
708
+ let commentsAdded = 0;
709
+ let repliesAdded = 0;
710
+ let proposalsAdded = 0;
711
+ for (const id of ops.accept ?? []) {
712
+ try {
713
+ acceptChange(body, id);
714
+ accepted++;
715
+ } catch (e) {
716
+ errors.push({ operation: "accept", id, error: e.message });
717
+ }
718
+ }
719
+ for (const id of ops.reject ?? []) {
720
+ try {
721
+ rejectChange(body, id);
722
+ rejected++;
723
+ } catch (e) {
724
+ errors.push({ operation: "reject", id, error: e.message });
725
+ }
726
+ }
727
+ for (const opts of ops.comments ?? []) {
728
+ try {
729
+ addComment(body, { ...opts, author: opts.author ?? defaultAuthor });
730
+ commentsAdded++;
731
+ } catch (e) {
732
+ errors.push({ operation: "comment", search: opts.search, error: e.message });
733
+ }
734
+ }
735
+ for (const opts of ops.replies ?? []) {
736
+ try {
737
+ replyTo(body, opts.commentId, { author: opts.author ?? defaultAuthor, text: opts.text });
738
+ repliesAdded++;
739
+ } catch (e) {
740
+ errors.push({ operation: "reply", id: opts.commentId, error: e.message });
741
+ }
742
+ }
743
+ for (const opts of ops.proposals ?? []) {
744
+ try {
745
+ proposeReplacement(body, { ...opts, author: opts.author ?? defaultAuthor });
746
+ proposalsAdded++;
747
+ } catch (e) {
748
+ errors.push({ operation: "proposal", search: opts.search, error: e.message });
749
+ }
750
+ }
751
+ return { accepted, rejected, commentsAdded, repliesAdded, proposalsAdded, errors };
752
+ }
753
+
754
+ // src/DocxReviewer.ts
755
+ var DocxReviewer = class _DocxReviewer {
756
+ /**
757
+ * Create a reviewer from a parsed Document.
758
+ * @param document - Parsed Document from @eigenpal/docx-core
759
+ * @param author - Default author name for comments and changes. (default: 'AI')
760
+ * @param originalBuffer - Original DOCX buffer, needed for toBuffer()
761
+ */
762
+ constructor(document, author = "AI", originalBuffer) {
763
+ const savedBuffer = originalBuffer ?? document.originalBuffer;
764
+ const { originalBuffer: _discard, ...rest } = document;
765
+ this.doc = structuredClone(rest);
766
+ if (savedBuffer) this.doc.originalBuffer = savedBuffer;
767
+ this.author = author;
768
+ }
769
+ /**
770
+ * Create a reviewer from a DOCX file buffer.
771
+ * @param buffer - ArrayBuffer of the DOCX file
772
+ * @param author - Default author name for comments and changes. (default: 'AI')
773
+ */
774
+ static async fromBuffer(buffer, author = "AI") {
775
+ const doc = await parseDocx(buffer, { preloadFonts: false });
776
+ return new _DocxReviewer(doc, author, buffer);
777
+ }
778
+ get body() {
779
+ return this.doc.package.document;
780
+ }
781
+ resolveAuthor(author) {
782
+ return author ?? this.author;
783
+ }
784
+ // ==========================================================================
785
+ // READ
786
+ // ==========================================================================
787
+ /** Get document content as structured blocks (headings, paragraphs, tables, lists). */
788
+ getContent(options) {
789
+ return getContent(this.body, options);
790
+ }
791
+ /**
792
+ * Get document content as plain text for LLM prompts.
793
+ * Each paragraph is prefixed with its index: `[0] text`, `[1] text`, etc.
794
+ * Table cells include position: `[5] (table, row 1, col 2) cell text`.
795
+ * Avoids JSON quote-escaping issues — LLMs can copy text verbatim.
796
+ */
797
+ getContentAsText(options) {
798
+ return formatContentForLLM(getContent(this.body, options));
799
+ }
800
+ // ==========================================================================
801
+ // DISCOVER
802
+ // ==========================================================================
803
+ /** Get all tracked changes in the document. */
804
+ getChanges(filter) {
805
+ return getChanges(this.body, filter);
806
+ }
807
+ /** Get all comments with their replies. */
808
+ getComments(filter) {
809
+ return getComments(this.body, filter);
810
+ }
811
+ addComment(indexOrOptions, text) {
812
+ const opts = typeof indexOrOptions === "number" ? { paragraphIndex: indexOrOptions, text, author: this.author } : { ...indexOrOptions, author: this.resolveAuthor(indexOrOptions.author) };
813
+ return addComment(this.body, opts);
814
+ }
815
+ replyTo(commentId, textOrOptions) {
816
+ const opts = typeof textOrOptions === "string" ? { text: textOrOptions, author: this.author } : { ...textOrOptions, author: this.resolveAuthor(textOrOptions.author) };
817
+ return replyTo(this.body, commentId, opts);
818
+ }
819
+ replace(indexOrOptions, search, replaceWith) {
820
+ const opts = typeof indexOrOptions === "number" ? {
821
+ paragraphIndex: indexOrOptions,
822
+ search,
823
+ replaceWith,
824
+ author: this.author
825
+ } : { ...indexOrOptions, author: this.resolveAuthor(indexOrOptions.author) };
826
+ proposeReplacement(this.body, opts);
827
+ }
828
+ /** @deprecated Use replace() instead. */
829
+ proposeReplacement(options) {
830
+ this.replace(options);
831
+ }
832
+ /** Insert text as a tracked change. */
833
+ proposeInsertion(options) {
834
+ proposeInsertion(this.body, {
835
+ ...options,
836
+ author: this.resolveAuthor(options.author)
837
+ });
838
+ }
839
+ /** Delete text as a tracked change. */
840
+ proposeDeletion(options) {
841
+ proposeDeletion(this.body, {
842
+ ...options,
843
+ author: this.resolveAuthor(options.author)
844
+ });
845
+ }
846
+ // ==========================================================================
847
+ // RESOLVE
848
+ // ==========================================================================
849
+ /** Accept a tracked change by its revision ID. */
850
+ acceptChange(id) {
851
+ acceptChange(this.body, id);
852
+ }
853
+ /** Reject a tracked change by its revision ID. */
854
+ rejectChange(id) {
855
+ rejectChange(this.body, id);
856
+ }
857
+ /** Accept all tracked changes. Returns count accepted. */
858
+ acceptAll() {
859
+ return acceptAll(this.body);
860
+ }
861
+ /** Reject all tracked changes. Returns count rejected. */
862
+ rejectAll() {
863
+ return rejectAll(this.body);
864
+ }
865
+ // ==========================================================================
866
+ // BATCH
867
+ // ==========================================================================
868
+ /**
869
+ * Apply multiple review operations in one call.
870
+ * Uses the reviewer's default author. Individual failures are collected, not thrown.
871
+ */
872
+ applyReview(ops) {
873
+ return applyReview(this.body, ops, this.author);
874
+ }
875
+ // ==========================================================================
876
+ // EXPORT
877
+ // ==========================================================================
878
+ /** Get the modified Document model. */
879
+ toDocument() {
880
+ return this.doc;
881
+ }
882
+ /** Serialize back to a DOCX buffer. Requires the original buffer. */
883
+ async toBuffer() {
884
+ if (!this.doc.originalBuffer) {
885
+ throw new Error(
886
+ "Cannot create buffer: no original DOCX buffer was provided. Use DocxReviewer.fromBuffer() or pass originalBuffer to the constructor."
887
+ );
888
+ }
889
+ const { repackDocx } = await import('@eigenpal/docx-core/headless');
890
+ return repackDocx(this.doc);
891
+ }
892
+ };
893
+
894
+ export { ChangeNotFoundError, CommentNotFoundError, DocxReviewer, TextNotFoundError };
895
+ //# sourceMappingURL=index.js.map
896
+ //# sourceMappingURL=index.js.map