@usejunior/docx-mcp 0.1.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/README.md +4 -4
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/cli/commands/edit.d.ts +24 -0
  4. package/dist/cli/commands/edit.d.ts.map +1 -0
  5. package/dist/cli/commands/edit.js +145 -0
  6. package/dist/cli/commands/edit.js.map +1 -0
  7. package/dist/cli/flag_parser.d.ts +7 -0
  8. package/dist/cli/flag_parser.d.ts.map +1 -0
  9. package/dist/cli/flag_parser.js +235 -0
  10. package/dist/cli/flag_parser.js.map +1 -0
  11. package/dist/cli/help.d.ts +3 -0
  12. package/dist/cli/help.d.ts.map +1 -0
  13. package/dist/cli/help.js +62 -0
  14. package/dist/cli/help.js.map +1 -0
  15. package/dist/cli/index.d.ts +2 -2
  16. package/dist/cli/index.d.ts.map +1 -1
  17. package/dist/cli/index.js +29 -32
  18. package/dist/cli/index.js.map +1 -1
  19. package/dist/cli/parse_utils.d.ts +7 -0
  20. package/dist/cli/parse_utils.d.ts.map +1 -0
  21. package/dist/cli/parse_utils.js +18 -0
  22. package/dist/cli/parse_utils.js.map +1 -0
  23. package/dist/cli/tool_runner.d.ts +6 -0
  24. package/dist/cli/tool_runner.d.ts.map +1 -0
  25. package/dist/cli/tool_runner.js +17 -0
  26. package/dist/cli/tool_runner.js.map +1 -0
  27. package/dist/conformance/harness.js +7 -7
  28. package/dist/conformance/harness.js.map +1 -1
  29. package/dist/server.d.ts +5 -5
  30. package/dist/server.js +8 -8
  31. package/dist/server.js.map +1 -1
  32. package/dist/session/manager.d.ts +20 -6
  33. package/dist/session/manager.d.ts.map +1 -1
  34. package/dist/session/manager.js +28 -6
  35. package/dist/session/manager.js.map +1 -1
  36. package/dist/tool_catalog.d.ts +29 -22
  37. package/dist/tool_catalog.d.ts.map +1 -1
  38. package/dist/tool_catalog.js +23 -17
  39. package/dist/tool_catalog.js.map +1 -1
  40. package/dist/tools/clear_formatting.d.ts +14 -0
  41. package/dist/tools/clear_formatting.d.ts.map +1 -0
  42. package/dist/tools/clear_formatting.js +94 -0
  43. package/dist/tools/clear_formatting.js.map +1 -0
  44. package/dist/tools/get_session_status.js +3 -3
  45. package/dist/tools/get_session_status.js.map +1 -1
  46. package/dist/tools/grep.d.ts.map +1 -1
  47. package/dist/tools/grep.js +8 -1
  48. package/dist/tools/grep.js.map +1 -1
  49. package/dist/tools/insert_paragraph.d.ts +1 -0
  50. package/dist/tools/insert_paragraph.d.ts.map +1 -1
  51. package/dist/tools/insert_paragraph.js +61 -397
  52. package/dist/tools/insert_paragraph.js.map +1 -1
  53. package/dist/tools/open_document.d.ts.map +1 -1
  54. package/dist/tools/open_document.js +9 -22
  55. package/dist/tools/open_document.js.map +1 -1
  56. package/dist/tools/pagination.d.ts +9 -0
  57. package/dist/tools/pagination.d.ts.map +1 -0
  58. package/dist/tools/pagination.js +16 -0
  59. package/dist/tools/pagination.js.map +1 -0
  60. package/dist/tools/read_file.d.ts.map +1 -1
  61. package/dist/tools/read_file.js +96 -17
  62. package/dist/tools/read_file.js.map +1 -1
  63. package/dist/tools/replace_text.d.ts +18 -0
  64. package/dist/tools/replace_text.d.ts.map +1 -1
  65. package/dist/tools/replace_text.js +129 -517
  66. package/dist/tools/replace_text.js.map +1 -1
  67. package/dist/tools/{download.d.ts → save.d.ts} +4 -4
  68. package/dist/tools/save.d.ts.map +1 -0
  69. package/dist/tools/{download.js → save.js} +21 -20
  70. package/dist/tools/save.js.map +1 -0
  71. package/dist/tools/session_resolution.d.ts.map +1 -1
  72. package/dist/tools/session_resolution.js +68 -18
  73. package/dist/tools/session_resolution.js.map +1 -1
  74. package/dist/tools/tag_parser.d.ts +28 -0
  75. package/dist/tools/tag_parser.d.ts.map +1 -0
  76. package/dist/tools/tag_parser.js +327 -0
  77. package/dist/tools/tag_parser.js.map +1 -0
  78. package/package.json +2 -2
  79. package/dist/tools/download.d.ts.map +0 -1
  80. package/dist/tools/download.js.map +0 -1
  81. package/dist/tools/duplicate_document.d.ts +0 -8
  82. package/dist/tools/duplicate_document.d.ts.map +0 -1
  83. package/dist/tools/duplicate_document.js +0 -90
  84. package/dist/tools/duplicate_document.js.map +0 -1
@@ -2,340 +2,26 @@ import { errorMessage } from "../error_utils.js";
2
2
  import { err, ok } from './types.js';
3
3
  import { ERROR_PREVIEW_CHARS, RESULT_PREVIEW_CHARS, previewText } from './preview.js';
4
4
  import { mergeSessionResolutionMetadata, resolveSessionForTool } from './session_resolution.js';
5
- import { OOXML, W, findInlineDefinitionSpan, findUniqueSubstringMatch, getParagraphRuns, hasDefinitionTags, hasHighlightTags, hasFormattingTags, hasHyperlinkTags, stripDefinitionTags, stripHighlightTags, stripFormattingTags, stripHyperlinkTags, } from '@usejunior/docx-core';
6
- const OPEN_DEF = '<definition>';
7
- const CLOSE_DEF = '</definition>';
8
- const OPEN_HEADER = '<header>';
9
- const CLOSE_HEADER = '</header>';
10
- const OPEN_RUN_IN_HEADER = '<RunInHeader>';
11
- const CLOSE_RUN_IN_HEADER = '</RunInHeader>';
12
- const OPEN_HL = '<highlighting>';
13
- const CLOSE_HL = '</highlighting>';
14
- const OPEN_B = '<b>';
15
- const CLOSE_B = '</b>';
16
- const OPEN_I = '<i>';
17
- const CLOSE_I = '</i>';
18
- const OPEN_U = '<u>';
19
- const CLOSE_U = '</u>';
20
- function isTruthyEnv(value) {
21
- if (!value)
22
- return false;
23
- const v = value.trim().toLowerCase();
24
- return v === '1' || v === 'true' || v === 'yes' || v === 'on';
25
- }
26
- function useLegacyDefinitionTags() {
27
- return isTruthyEnv(process.env.SAFE_DOCX_ENABLE_LEGACY_DEFINITION_TAGS);
28
- }
29
- function hasInlineStyleTags(text) {
30
- return (text.includes(OPEN_B) ||
31
- text.includes(CLOSE_B) ||
32
- text.includes(OPEN_I) ||
33
- text.includes(CLOSE_I) ||
34
- text.includes(OPEN_U) ||
35
- text.includes(CLOSE_U));
36
- }
37
- function hasHeaderTags(text) {
38
- return (text.includes(OPEN_HEADER) ||
39
- text.includes(CLOSE_HEADER) ||
40
- text.includes(OPEN_RUN_IN_HEADER) ||
41
- text.includes(CLOSE_RUN_IN_HEADER));
42
- }
43
- function statesEqual(a, b) {
44
- return (a.definition === b.definition &&
45
- a.highlighting === b.highlighting &&
46
- a.bold === b.bold &&
47
- a.italic === b.italic &&
48
- a.underline === b.underline &&
49
- a.header === b.header);
50
- }
51
- function pushSegment(out, text, state) {
52
- if (!text)
53
- return;
54
- const prev = out[out.length - 1];
55
- if (prev && statesEqual(prev, state)) {
56
- prev.text += text;
57
- return;
58
- }
59
- out.push({ text, ...state });
60
- }
61
- function splitTaggedText(text, opts) {
62
- const counters = {
63
- definition: 0,
64
- header: 0,
65
- highlighting: 0,
66
- bold: 0,
67
- italic: 0,
68
- underline: 0,
69
- };
70
- const out = [];
71
- let i = 0;
72
- let buf = '';
73
- const state = () => ({
74
- definition: counters.definition > 0,
75
- header: counters.header > 0,
76
- highlighting: counters.highlighting > 0,
77
- bold: counters.bold > 0,
78
- italic: counters.italic > 0,
79
- underline: counters.underline > 0,
80
- });
81
- const flush = () => {
82
- pushSegment(out, buf, state());
83
- buf = '';
84
- };
85
- const open = (key) => {
86
- flush();
87
- counters[key] += 1;
88
- };
89
- const close = (key, errCode) => {
90
- flush();
91
- if (counters[key] === 0)
92
- throw new Error(errCode);
93
- counters[key] -= 1;
94
- };
95
- while (i < text.length) {
96
- if (opts.allowDefinitionTags && text.startsWith(OPEN_DEF, i)) {
97
- open('definition');
98
- i += OPEN_DEF.length;
99
- continue;
100
- }
101
- if (opts.allowDefinitionTags && text.startsWith(CLOSE_DEF, i)) {
102
- close('definition', 'UNBALANCED_DEFINITION_TAGS');
103
- i += CLOSE_DEF.length;
104
- continue;
105
- }
106
- if (text.startsWith(OPEN_HEADER, i)) {
107
- open('header');
108
- i += OPEN_HEADER.length;
109
- continue;
110
- }
111
- if (text.startsWith(CLOSE_HEADER, i)) {
112
- close('header', 'UNBALANCED_HEADER_TAGS');
113
- i += CLOSE_HEADER.length;
114
- continue;
115
- }
116
- if (text.startsWith(OPEN_RUN_IN_HEADER, i)) {
117
- open('header');
118
- i += OPEN_RUN_IN_HEADER.length;
119
- continue;
120
- }
121
- if (text.startsWith(CLOSE_RUN_IN_HEADER, i)) {
122
- close('header', 'UNBALANCED_HEADER_TAGS');
123
- i += CLOSE_RUN_IN_HEADER.length;
124
- continue;
125
- }
126
- if (text.startsWith(OPEN_HL, i)) {
127
- open('highlighting');
128
- i += OPEN_HL.length;
129
- continue;
130
- }
131
- if (text.startsWith(CLOSE_HL, i)) {
132
- close('highlighting', 'UNBALANCED_HIGHLIGHT_TAGS');
133
- i += CLOSE_HL.length;
134
- continue;
135
- }
136
- if (text.startsWith(OPEN_B, i)) {
137
- open('bold');
138
- i += OPEN_B.length;
139
- continue;
140
- }
141
- if (text.startsWith(CLOSE_B, i)) {
142
- close('bold', 'UNBALANCED_BOLD_TAGS');
143
- i += CLOSE_B.length;
144
- continue;
145
- }
146
- if (text.startsWith(OPEN_I, i)) {
147
- open('italic');
148
- i += OPEN_I.length;
149
- continue;
150
- }
151
- if (text.startsWith(CLOSE_I, i)) {
152
- close('italic', 'UNBALANCED_ITALIC_TAGS');
153
- i += CLOSE_I.length;
154
- continue;
155
- }
156
- if (text.startsWith(OPEN_U, i)) {
157
- open('underline');
158
- i += OPEN_U.length;
159
- continue;
160
- }
161
- if (text.startsWith(CLOSE_U, i)) {
162
- close('underline', 'UNBALANCED_UNDERLINE_TAGS');
163
- i += CLOSE_U.length;
164
- continue;
165
- }
166
- buf += text[i];
167
- i += 1;
168
- }
169
- flush();
170
- if (counters.definition > 0)
171
- throw new Error('UNBALANCED_DEFINITION_TAGS');
172
- if (counters.header > 0)
173
- throw new Error('UNBALANCED_HEADER_TAGS');
174
- if (counters.highlighting > 0)
175
- throw new Error('UNBALANCED_HIGHLIGHT_TAGS');
176
- if (counters.bold > 0)
177
- throw new Error('UNBALANCED_BOLD_TAGS');
178
- if (counters.italic > 0)
179
- throw new Error('UNBALANCED_ITALIC_TAGS');
180
- if (counters.underline > 0)
181
- throw new Error('UNBALANCED_UNDERLINE_TAGS');
182
- return out.length > 0 ? out : [{ text: '', ...state() }];
183
- }
184
- const QUOTE_CHARS = new Set([
185
- '"',
186
- "'",
187
- '\u201c',
188
- '\u201d',
189
- '\u2018',
190
- '\u2019',
191
- '\u00ab',
192
- '\u00bb',
193
- '\u2039',
194
- '\u203a',
195
- ]);
196
- function isQuoteChar(ch) {
197
- return QUOTE_CHARS.has(ch);
198
- }
199
- function stripOuterQuotes(term) {
200
- let t = term;
201
- while (t && isQuoteChar(t[0]))
202
- t = t.slice(1);
203
- while (t && isQuoteChar(t[t.length - 1]))
204
- t = t.slice(0, -1);
205
- return t;
206
- }
207
- function absorbSurroundingQuotes(segs) {
208
- // Python parity: <definition> absorbs quotes. If the LLM returns surrounding quotes
209
- // like: "<definition>Term</definition>" we should not double-quote.
210
- for (let i = 0; i < segs.length; i++) {
211
- const seg = segs[i];
212
- if (!seg.definition)
213
- continue;
214
- const prev = i > 0 ? segs[i - 1] : null;
215
- if (prev && !prev.definition && prev.text && isQuoteChar(prev.text[prev.text.length - 1])) {
216
- prev.text = prev.text.slice(0, -1);
217
- }
218
- const next = i + 1 < segs.length ? segs[i + 1] : null;
219
- if (next && !next.definition && next.text && isQuoteChar(next.text[0])) {
220
- next.text = next.text.slice(1);
221
- }
222
- }
223
- }
224
- function segmentAddRunProps(seg) {
225
- const add = {};
226
- if (seg.bold)
227
- add.bold = true;
228
- if (seg.italic)
229
- add.italic = true;
230
- if (seg.underline)
231
- add.underline = true;
232
- if (seg.highlighting)
233
- add.highlight = true;
234
- return Object.keys(add).length > 0 ? add : undefined;
235
- }
236
- function chooseRunByOverlap(runs, start, end) {
237
- let pos = 0;
238
- let bestRun = null;
239
- let best = -1;
240
- for (const run of runs) {
241
- const runStart = pos;
242
- const runEnd = pos + run.text.length;
243
- const overlap = Math.max(0, Math.min(end, runEnd) - Math.max(start, runStart));
244
- if (overlap > best) {
245
- best = overlap;
246
- bestRun = run.r;
247
- }
248
- pos = runEnd;
249
- }
250
- return bestRun;
251
- }
252
- function getWAttr(el, localName) {
253
- return el.getAttributeNS(OOXML.W_NS, localName) ?? el.getAttribute(`w:${localName}`) ?? el.getAttribute(localName);
254
- }
255
- function runHasHighlight(run) {
256
- const rPr = run.getElementsByTagNameNS(OOXML.W_NS, W.rPr).item(0);
257
- if (!rPr)
258
- return false;
259
- const h = rPr.getElementsByTagNameNS(OOXML.W_NS, W.highlight).item(0);
260
- if (!h)
261
- return false;
262
- const v = getWAttr(h, 'val');
263
- return !!v && v !== 'none';
264
- }
265
- export function stripSearchTags(text) {
266
- let s = text;
267
- if (hasDefinitionTags(s))
268
- s = stripDefinitionTags(s);
269
- if (hasHighlightTags(s))
270
- s = stripHighlightTags(s);
271
- if (hasFormattingTags(s))
272
- s = stripFormattingTags(s);
273
- if (hasHyperlinkTags(s))
274
- s = stripHyperlinkTags(s);
275
- return s;
276
- }
277
- function isLikelyFieldPlaceholder(text) {
278
- return /(\[[^\]]+\])|(\{\{[^}]+\}\})|(_{3,})/.test(text);
279
- }
280
- function chooseContextTemplateRun(runs, matchStart, matchEnd) {
281
- const overlapIndices = [];
282
- let pos = 0;
283
- for (let i = 0; i < runs.length; i++) {
284
- const end = pos + runs[i].text.length;
285
- if (Math.min(matchEnd, end) > Math.max(matchStart, pos))
286
- overlapIndices.push(i);
287
- pos = end;
288
- }
289
- if (overlapIndices.length === 0)
290
- return { templateRun: null, allOverlappedRunsHighlighted: false };
291
- let allHl = true;
292
- for (const idx of overlapIndices) {
293
- if (!runHasHighlight(runs[idx].r)) {
294
- allHl = false;
295
- break;
296
- }
297
- }
298
- // Prefer a non-highlight run inside overlap.
299
- for (const idx of overlapIndices) {
300
- const r = runs[idx];
301
- if (!runHasHighlight(r.r))
302
- return { templateRun: r.r, allOverlappedRunsHighlighted: allHl };
303
- }
304
- // Then search nearest non-highlight run around overlap.
305
- const left = overlapIndices[0];
306
- const right = overlapIndices[overlapIndices.length - 1];
307
- for (let d = 1; d < runs.length; d++) {
308
- const li = left - d;
309
- if (li >= 0 && !runHasHighlight(runs[li].r))
310
- return { templateRun: runs[li].r, allOverlappedRunsHighlighted: allHl };
311
- const ri = right + d;
312
- if (ri < runs.length && !runHasHighlight(runs[ri].r))
313
- return { templateRun: runs[ri].r, allOverlappedRunsHighlighted: allHl };
314
- }
315
- // Fallback to predominant run in overlap.
316
- return {
317
- templateRun: chooseRunByOverlap(runs, matchStart, matchEnd),
318
- allOverlappedRunsHighlighted: allHl,
319
- };
320
- }
5
+ import { OOXML, W, findUniqueSubstringMatch, applyDocumentQuoteStyle, getParagraphRuns, hasHighlightTags, hasHyperlinkTags, stripHyperlinkTags, stripAllInlineTags, } from '@usejunior/docx-core';
6
+ import { splitTaggedText, segmentAddRunProps, hasAnyMarkupTags, } from './tag_parser.js';
321
7
  function mergeAddRunProps(a, b) {
322
- const out = {};
323
- if (a?.bold)
324
- out.bold = true;
325
- if (a?.italic)
326
- out.italic = true;
327
- if (a?.underline)
328
- out.underline = a.underline;
329
- if (a?.highlight)
330
- out.highlight = a.highlight;
331
- if (b?.bold)
332
- out.bold = true;
333
- if (b?.italic)
334
- out.italic = true;
335
- if (b?.underline)
336
- out.underline = b.underline;
337
- if (b?.highlight)
338
- out.highlight = b.highlight;
8
+ const out = { ...a };
9
+ if (b) {
10
+ if (b.bold !== undefined)
11
+ out.bold = b.bold;
12
+ if (b.italic !== undefined)
13
+ out.italic = b.italic;
14
+ if (b.underline !== undefined)
15
+ out.underline = b.underline;
16
+ if (b.highlight !== undefined)
17
+ out.highlight = b.highlight;
18
+ if (b.fontSize !== undefined)
19
+ out.fontSize = b.fontSize;
20
+ if (b.fontName !== undefined)
21
+ out.fontName = b.fontName;
22
+ if (b.color !== undefined)
23
+ out.color = b.color;
24
+ }
339
25
  return Object.keys(out).length > 0 ? out : undefined;
340
26
  }
341
27
  function headerFormattingToAddRunProps(formatting) {
@@ -351,60 +37,6 @@ function headerFormattingToAddRunProps(formatting) {
351
37
  add.underline = true;
352
38
  return Object.keys(add).length > 0 ? add : null;
353
39
  }
354
- function extractDefinitionAddRunProps(run) {
355
- if (!run)
356
- return null;
357
- const rPr = run.getElementsByTagNameNS(OOXML.W_NS, W.rPr).item(0);
358
- if (!rPr)
359
- return null;
360
- const add = {};
361
- const bEl = rPr.getElementsByTagNameNS(OOXML.W_NS, W.b).item(0);
362
- if (bEl) {
363
- const v = getWAttr(bEl, 'val');
364
- if (v !== '0' && v !== 'false')
365
- add.bold = true;
366
- }
367
- const iEl = rPr.getElementsByTagNameNS(OOXML.W_NS, W.i).item(0);
368
- if (iEl) {
369
- const v = getWAttr(iEl, 'val');
370
- if (v !== '0' && v !== 'false')
371
- add.italic = true;
372
- }
373
- const uEl = rPr.getElementsByTagNameNS(OOXML.W_NS, W.u).item(0);
374
- if (uEl) {
375
- const v = getWAttr(uEl, 'val');
376
- if (!v || v !== 'none')
377
- add.underline = v ?? true;
378
- }
379
- return Object.keys(add).length > 0 ? add : null;
380
- }
381
- function findDefinitionRoleModelAddRunProps(session, anchorParagraphId) {
382
- const { nodes } = session.doc.buildDocumentView({ includeSemanticTags: false });
383
- const anchorIdx = nodes.findIndex((n) => n.id === anchorParagraphId);
384
- if (anchorIdx < 0)
385
- return null;
386
- for (let delta = 0; delta < nodes.length; delta++) {
387
- const candidates = [anchorIdx - delta, anchorIdx + delta];
388
- for (const idx of candidates) {
389
- if (idx < 0 || idx >= nodes.length)
390
- continue;
391
- const nid = nodes[idx].id;
392
- const pEl = session.doc.getParagraphElementById(nid);
393
- if (!pEl)
394
- continue;
395
- const text = session.doc.getParagraphTextById(nid) ?? '';
396
- const span = findInlineDefinitionSpan(text);
397
- if (!span)
398
- continue;
399
- const runs = getParagraphRuns(pEl);
400
- const r = chooseRunByOverlap(runs, span.term_start, span.term_end);
401
- const add = extractDefinitionAddRunProps(r);
402
- if (add)
403
- return add;
404
- }
405
- }
406
- return null;
407
- }
408
40
  function findHeaderRoleModelAddRunProps(session, anchorParagraphId) {
409
41
  const { nodes } = session.doc.buildDocumentView({ includeSemanticTags: false });
410
42
  const anchorIdx = nodes.findIndex((n) => n.id === anchorParagraphId);
@@ -423,9 +55,13 @@ function findHeaderRoleModelAddRunProps(session, anchorParagraphId) {
423
55
  }
424
56
  return null;
425
57
  }
426
- function buildDistributedPartsAcrossRuns(runs, matchStart, matchEnd, replacementText, clearHighlight) {
427
- if (replacementText.length === 0)
428
- return [];
58
+ export function stripSearchTags(text) {
59
+ return stripAllInlineTags(text);
60
+ }
61
+ function runHasHighlight(r) {
62
+ return r.getElementsByTagNameNS(OOXML.W_NS, W.highlight).length > 0;
63
+ }
64
+ function chooseContextTemplateRun(runs, matchStart, matchEnd) {
429
65
  const overlaps = [];
430
66
  let pos = 0;
431
67
  for (const run of runs) {
@@ -436,41 +72,49 @@ function buildDistributedPartsAcrossRuns(runs, matchStart, matchEnd, replacement
436
72
  overlaps.push({ run: run.r, overlap });
437
73
  pos = runEnd;
438
74
  }
439
- if (overlaps.length <= 1)
440
- return null;
441
- const totalOverlap = overlaps.reduce((sum, x) => sum + x.overlap, 0);
442
- if (totalOverlap <= 0)
443
- return null;
444
- const allocations = overlaps.map((x) => {
445
- const exact = (replacementText.length * x.overlap) / totalOverlap;
446
- const base = Math.floor(exact);
447
- return { ...x, base, frac: exact - base, alloc: base };
448
- });
449
- let assigned = allocations.reduce((sum, x) => sum + x.alloc, 0);
450
- let remaining = replacementText.length - assigned;
451
- allocations.sort((a, b) => b.frac - a.frac);
452
- for (let i = 0; i < allocations.length && remaining > 0; i++) {
453
- allocations[i].alloc += 1;
454
- remaining -= 1;
455
- }
456
- allocations.sort((a, b) => runs.findIndex((r) => r.r === a.run) - runs.findIndex((r) => r.r === b.run));
457
- const parts = [];
458
- let cursor = 0;
459
- for (const a of allocations) {
460
- if (a.alloc <= 0)
461
- continue;
462
- const text = replacementText.slice(cursor, cursor + a.alloc);
463
- cursor += a.alloc;
464
- parts.push({
465
- text,
466
- templateRun: a.run,
467
- clearHighlight,
468
- });
75
+ if (overlaps.length === 0)
76
+ return { templateRun: null, allOverlappedRunsHighlighted: false };
77
+ let allHl = true;
78
+ for (const o of overlaps) {
79
+ if (!runHasHighlight(o.run)) {
80
+ allHl = false;
81
+ break;
82
+ }
469
83
  }
470
- if (cursor < replacementText.length && parts.length > 0) {
471
- parts[parts.length - 1].text += replacementText.slice(cursor);
84
+ let best = -1;
85
+ let template = null;
86
+ for (const o of overlaps) {
87
+ if (o.overlap > best) {
88
+ best = o.overlap;
89
+ template = o.run;
90
+ }
472
91
  }
473
- return parts.length > 0 ? parts : null;
92
+ return { templateRun: template, allOverlappedRunsHighlighted: allHl };
93
+ }
94
+ /**
95
+ * Count shared leading characters between two strings.
96
+ */
97
+ export function commonPrefixLength(a, b) {
98
+ const len = Math.min(a.length, b.length);
99
+ let i = 0;
100
+ while (i < len && a[i] === b[i])
101
+ i++;
102
+ return i;
103
+ }
104
+ /**
105
+ * Count shared trailing characters between two strings,
106
+ * non-overlapping with a known prefix of length `prefixLen`.
107
+ */
108
+ export function commonSuffixLength(a, b, prefixLen) {
109
+ const maxSuffix = Math.min(a.length - prefixLen, b.length - prefixLen);
110
+ let i = 0;
111
+ while (i < maxSuffix && a[a.length - 1 - i] === b[b.length - 1 - i])
112
+ i++;
113
+ return i;
114
+ }
115
+ function isLikelyFieldPlaceholder(text) {
116
+ const t = text.trim();
117
+ return (t.startsWith('[') && t.endsWith(']')) || (t.startsWith('«') && t.endsWith('»'));
474
118
  }
475
119
  export async function replaceText(manager, params) {
476
120
  try {
@@ -478,151 +122,119 @@ export async function replaceText(manager, params) {
478
122
  if (!resolved.ok)
479
123
  return resolved.response;
480
124
  const { session, metadata } = resolved;
481
- // Optional run normalization before search to handle fragmented runs
482
125
  if (params.normalize_first) {
483
126
  session.doc.mergeRunsOnly();
484
127
  }
485
128
  const { target_paragraph_id: pid } = params;
486
- const legacyDefinitionTags = useLegacyDefinitionTags();
487
129
  const oldStr = stripSearchTags(params.old_string);
488
130
  let newStr = params.new_string;
489
- if (!legacyDefinitionTags && hasDefinitionTags(newStr)) {
490
- // Default mode: definition tags are normalized into plain quoted text.
491
- newStr = stripDefinitionTags(newStr);
492
- }
493
- // Strip <a> tags from new_string — hyperlinks are read-only and cannot be created via replace_text.
494
131
  if (hasHyperlinkTags(newStr))
495
132
  newStr = stripHyperlinkTags(newStr);
496
- // Find the paragraph and do a unique find/replace.
497
133
  const beforeTextRaw = session.doc.getParagraphTextById(pid);
498
134
  if (beforeTextRaw === null) {
499
- return err('ANCHOR_NOT_FOUND', `Paragraph ID ${pid} not found in document`, 'Use grep or read_file to find valid paragraph IDs');
135
+ return err('ANCHOR_NOT_FOUND', `Paragraph ID ${pid} not found in document`);
500
136
  }
501
137
  const paraText = beforeTextRaw;
502
- const textMatch = findUniqueSubstringMatch(paraText, oldStr);
138
+ const findMode = params.clean_match ? 'clean' : 'default';
139
+ const textMatch = findUniqueSubstringMatch(paraText, oldStr, { mode: findMode });
503
140
  if (textMatch.status === 'not_found') {
504
- return err('TEXT_NOT_FOUND', `Text '${previewText(oldStr, ERROR_PREVIEW_CHARS)}' not found in paragraph ${pid}`, 'Verify old_string and paragraph context. Matching is tolerant to quote/whitespace variants but still requires a unique match.');
141
+ return err('TEXT_NOT_FOUND', `Text '${previewText(oldStr, ERROR_PREVIEW_CHARS)}' not found in paragraph ${pid}`);
505
142
  }
506
143
  if (textMatch.status === 'multiple') {
507
- return err('MULTIPLE_MATCHES', `Found ${textMatch.matchCount} matches for '${previewText(oldStr, ERROR_PREVIEW_CHARS)}' in paragraph using ${textMatch.mode} matching. Need unique match.`, 'Provide more context in old_string to make the match unique.');
144
+ return err('MULTIPLE_MATCHES', `Found ${textMatch.matchCount} matches for '${previewText(oldStr, ERROR_PREVIEW_CHARS)}' in paragraph. Need unique match.`);
508
145
  }
509
146
  const pEl = session.doc.getParagraphElementById(pid);
510
147
  if (!pEl) {
511
- return err('ANCHOR_NOT_FOUND', `Paragraph ID ${pid} not found in document`, 'Use grep or read_file to find valid paragraph IDs');
148
+ return err('ANCHOR_NOT_FOUND', `Paragraph ID ${pid} not found in document`);
512
149
  }
513
150
  const matchedOldStr = textMatch.matchedText;
514
151
  const matchStart = textMatch.start;
515
152
  const matchEnd = textMatch.end;
516
153
  const paraRuns = getParagraphRuns(pEl);
517
154
  const { templateRun: contextTemplateRun, allOverlappedRunsHighlighted } = chooseContextTemplateRun(paraRuns, matchStart, matchEnd);
518
- const shouldClearHighlight = allOverlappedRunsHighlighted && !hasHighlightTags(newStr) && isLikelyFieldPlaceholder(oldStr);
519
- // Apply edit.
155
+ const explicitAddProps = {};
156
+ if (params.bold !== undefined)
157
+ explicitAddProps.bold = params.bold;
158
+ if (params.italic !== undefined)
159
+ explicitAddProps.italic = params.italic;
160
+ if (params.underline !== undefined)
161
+ explicitAddProps.underline = params.underline;
162
+ if (params.highlight !== undefined)
163
+ explicitAddProps.highlight = params.highlight;
164
+ if (params.font_size !== undefined)
165
+ explicitAddProps.fontSize = params.font_size * 2;
166
+ if (params.font_name !== undefined)
167
+ explicitAddProps.fontName = params.font_name;
168
+ if (params.color !== undefined)
169
+ explicitAddProps.color = params.color;
170
+ const shouldClearHighlight = params.clear_highlight || (allOverlappedRunsHighlighted && !hasHighlightTags(newStr) && isLikelyFieldPlaceholder(oldStr));
520
171
  let replaceText = newStr;
521
- const hasMarkup = hasHighlightTags(newStr) ||
522
- hasHeaderTags(newStr) ||
523
- hasInlineStyleTags(newStr) ||
524
- (legacyDefinitionTags && hasDefinitionTags(newStr));
172
+ const hasMarkup = hasAnyMarkupTags(newStr);
525
173
  if (hasMarkup) {
526
- const segs = splitTaggedText(newStr, { allowDefinitionTags: legacyDefinitionTags });
527
- if (legacyDefinitionTags)
528
- absorbSurroundingQuotes(segs);
529
- const defAddProps = legacyDefinitionTags && segs.some((s) => s.definition)
530
- ? findDefinitionRoleModelAddRunProps(session, pid)
531
- : null;
532
- const headerAddProps = segs.some((s) => s.header)
533
- ? findHeaderRoleModelAddRunProps(session, pid)
534
- : null;
174
+ let segs;
175
+ try {
176
+ segs = splitTaggedText(newStr);
177
+ }
178
+ catch (e) {
179
+ return err(errorMessage(e), `Tag parse error in new_string: ${errorMessage(e)}`);
180
+ }
181
+ const headerAddProps = segs.some((s) => s.header) ? findHeaderRoleModelAddRunProps(session, pid) : null;
535
182
  const parts = [];
536
183
  for (const s of segs) {
537
184
  if (!s.text)
538
185
  continue;
539
- const segAddProps = mergeAddRunProps(segmentAddRunProps(s), s.header ? headerAddProps : null);
186
+ const segAddProps = mergeAddRunProps(mergeAddRunProps(segmentAddRunProps(s), explicitAddProps), s.header ? headerAddProps : null);
540
187
  const clearHighlight = shouldClearHighlight && !s.highlighting;
541
- if (legacyDefinitionTags && s.definition) {
542
- const term = stripOuterQuotes(s.text);
543
- if (!term)
544
- continue;
545
- parts.push({
546
- text: '"',
547
- templateRun: contextTemplateRun ?? undefined,
548
- addRunProps: segAddProps,
549
- clearHighlight,
550
- });
551
- parts.push({
552
- text: term,
553
- templateRun: contextTemplateRun ?? undefined,
554
- addRunProps: mergeAddRunProps(defAddProps, segAddProps),
555
- clearHighlight,
556
- });
557
- parts.push({
558
- text: '"',
559
- templateRun: contextTemplateRun ?? undefined,
560
- addRunProps: segAddProps,
561
- clearHighlight,
562
- });
563
- }
564
- else {
565
- parts.push({
566
- text: s.text,
567
- templateRun: contextTemplateRun ?? undefined,
568
- addRunProps: segAddProps,
569
- clearHighlight,
570
- });
571
- }
188
+ parts.push({ text: s.text, templateRun: contextTemplateRun ?? undefined, addRunProps: segAddProps, clearHighlight });
572
189
  }
573
190
  replaceText = parts;
191
+ session.doc.replaceText({ targetParagraphId: pid, findText: matchedOldStr, replaceText });
574
192
  }
575
193
  else {
576
- const defAddProps = findDefinitionRoleModelAddRunProps(session, pid);
577
- const explicitDefSpan = defAddProps ? findInlineDefinitionSpan(newStr) : null;
578
- if (explicitDefSpan && explicitDefSpan.term_start >= 0 && explicitDefSpan.term_end > explicitDefSpan.term_start) {
579
- const before = newStr.slice(0, explicitDefSpan.term_start);
580
- const term = newStr.slice(explicitDefSpan.term_start, explicitDefSpan.term_end);
581
- const after = newStr.slice(explicitDefSpan.term_end);
582
- replaceText = [
583
- ...(before ? [{ text: before, templateRun: contextTemplateRun ?? undefined, clearHighlight: shouldClearHighlight }] : []),
584
- {
585
- text: term,
586
- templateRun: contextTemplateRun ?? undefined,
587
- addRunProps: mergeAddRunProps(defAddProps, null),
588
- clearHighlight: shouldClearHighlight,
589
- },
590
- ...(after ? [{ text: after, templateRun: contextTemplateRun ?? undefined, clearHighlight: shouldClearHighlight }] : []),
591
- ];
194
+ // Fix 2: Transfer document quote style to new_string for non-exact matches.
195
+ if (textMatch.mode !== 'exact' && textMatch.mode !== 'clean') {
196
+ newStr = applyDocumentQuoteStyle(matchedOldStr, newStr);
592
197
  }
593
- else {
594
- const distributed = buildDistributedPartsAcrossRuns(paraRuns, matchStart, matchEnd, newStr, shouldClearHighlight);
595
- if (distributed && distributed.length > 0) {
596
- replaceText = distributed;
198
+ // Fix 1: Range trimming — compute common prefix/suffix between matched old text
199
+ // and new text, then only replace the changed middle. This preserves formatting
200
+ // on unchanged prefix/suffix characters and naturally avoids field intersections.
201
+ const prefixLen = commonPrefixLength(matchedOldStr, newStr);
202
+ const suffixLen = commonSuffixLength(matchedOldStr, newStr, prefixLen);
203
+ const trimmedNewStr = newStr.slice(prefixLen, newStr.length - suffixLen);
204
+ const trimmedStart = matchStart + prefixLen;
205
+ const trimmedEnd = matchEnd - suffixLen;
206
+ if (trimmedStart < trimmedEnd || trimmedNewStr.length > 0) {
207
+ // There IS a changed middle — trim and replace.
208
+ let trimmedReplace;
209
+ if (shouldClearHighlight || Object.keys(explicitAddProps).length > 0) {
210
+ const { templateRun } = chooseContextTemplateRun(paraRuns, trimmedStart, trimmedEnd);
211
+ trimmedReplace = [{ text: trimmedNewStr, templateRun: templateRun ?? undefined,
212
+ addRunProps: explicitAddProps, clearHighlight: shouldClearHighlight }];
597
213
  }
598
- else if (shouldClearHighlight || contextTemplateRun) {
599
- replaceText = [
600
- {
601
- text: newStr,
602
- templateRun: contextTemplateRun ?? undefined,
603
- clearHighlight: shouldClearHighlight,
604
- },
605
- ];
214
+ else {
215
+ trimmedReplace = trimmedNewStr;
606
216
  }
217
+ session.doc.replaceTextAtRange({ targetParagraphId: pid, start: trimmedStart, end: trimmedEnd, replaceText: trimmedReplace });
218
+ // Range trimming splits the original run at prefix/suffix boundaries, producing
219
+ // adjacent runs with identical formatting. Merge them back to keep output clean.
220
+ session.doc.mergeRunsOnly();
607
221
  }
222
+ // else: text is identical after normalization — no-op
608
223
  }
609
- session.doc.replaceText({ targetParagraphId: pid, findText: matchedOldStr, replaceText });
610
224
  manager.markEdited(session);
611
- const beforeText = paraText.trim();
612
- const afterText = (session.doc.getParagraphTextById(pid) ?? '').trim();
613
225
  return ok(mergeSessionResolutionMetadata({
614
226
  success: true,
615
227
  session_id: session.sessionId,
616
228
  edit_count: session.editCount,
617
229
  target_paragraph_id: pid,
618
230
  replacements_made: 1,
619
- before_text: previewText(beforeText, RESULT_PREVIEW_CHARS),
620
- after_text: previewText(afterText, RESULT_PREVIEW_CHARS),
231
+ before_text: previewText(paraText.trim(), RESULT_PREVIEW_CHARS),
232
+ after_text: previewText((session.doc.getParagraphTextById(pid) ?? '').trim(), RESULT_PREVIEW_CHARS),
621
233
  }, metadata));
622
234
  }
623
235
  catch (e) {
624
236
  const msg = errorMessage(e);
625
- return err('EDIT_ERROR', `Failed to edit document: ${msg}`, 'Use grep to find valid paragraph IDs and verify old_string exists.');
237
+ return err('EDIT_ERROR', `Failed to edit document: ${msg}`);
626
238
  }
627
239
  }
628
240
  //# sourceMappingURL=replace_text.js.map