@usejunior/docx-mcp 0.1.2 → 0.2.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 (46) hide show
  1. package/README.md +4 -4
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/conformance/harness.js +7 -7
  4. package/dist/conformance/harness.js.map +1 -1
  5. package/dist/server.d.ts +5 -5
  6. package/dist/server.js +8 -8
  7. package/dist/server.js.map +1 -1
  8. package/dist/session/manager.d.ts +6 -6
  9. package/dist/session/manager.d.ts.map +1 -1
  10. package/dist/session/manager.js +6 -6
  11. package/dist/session/manager.js.map +1 -1
  12. package/dist/tool_catalog.d.ts +26 -20
  13. package/dist/tool_catalog.d.ts.map +1 -1
  14. package/dist/tool_catalog.js +18 -13
  15. package/dist/tool_catalog.js.map +1 -1
  16. package/dist/tools/clear_formatting.d.ts +14 -0
  17. package/dist/tools/clear_formatting.d.ts.map +1 -0
  18. package/dist/tools/clear_formatting.js +94 -0
  19. package/dist/tools/clear_formatting.js.map +1 -0
  20. package/dist/tools/get_session_status.js +3 -3
  21. package/dist/tools/get_session_status.js.map +1 -1
  22. package/dist/tools/insert_paragraph.d.ts +1 -0
  23. package/dist/tools/insert_paragraph.d.ts.map +1 -1
  24. package/dist/tools/insert_paragraph.js +61 -397
  25. package/dist/tools/insert_paragraph.js.map +1 -1
  26. package/dist/tools/open_document.js +5 -5
  27. package/dist/tools/open_document.js.map +1 -1
  28. package/dist/tools/replace_text.d.ts +9 -0
  29. package/dist/tools/replace_text.d.ts.map +1 -1
  30. package/dist/tools/replace_text.js +128 -516
  31. package/dist/tools/replace_text.js.map +1 -1
  32. package/dist/tools/{download.d.ts → save.d.ts} +4 -4
  33. package/dist/tools/save.d.ts.map +1 -0
  34. package/dist/tools/{download.js → save.js} +21 -20
  35. package/dist/tools/save.js.map +1 -0
  36. package/dist/tools/tag_parser.d.ts +28 -0
  37. package/dist/tools/tag_parser.d.ts.map +1 -0
  38. package/dist/tools/tag_parser.js +327 -0
  39. package/dist/tools/tag_parser.js.map +1 -0
  40. package/package.json +2 -2
  41. package/dist/tools/download.d.ts.map +0 -1
  42. package/dist/tools/download.js.map +0 -1
  43. package/dist/tools/duplicate_document.d.ts +0 -8
  44. package/dist/tools/duplicate_document.d.ts.map +0 -1
  45. package/dist/tools/duplicate_document.js +0 -90
  46. 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, 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,71 @@ function buildDistributedPartsAcrossRuns(runs, matchStart, matchEnd, replacement
436
72
  overlaps.push({ run: run.r, overlap });
437
73
  pos = runEnd;
438
74
  }
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
+ }
83
+ }
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
+ }
91
+ }
92
+ return { templateRun: template, allOverlappedRunsHighlighted: allHl };
93
+ }
94
+ function buildDistributedPartsAcrossRuns(runs, matchStart, matchEnd, newStr, shouldClearHighlight) {
95
+ const overlaps = [];
96
+ let pos = 0;
97
+ for (const run of runs) {
98
+ const runStart = pos;
99
+ const runEnd = pos + run.text.length;
100
+ const overlap = Math.max(0, Math.min(matchEnd, runEnd) - Math.max(matchStart, runStart));
101
+ if (overlap > 0) {
102
+ overlaps.push({
103
+ r: run.r,
104
+ text: run.text,
105
+ start: Math.max(0, matchStart - runStart),
106
+ end: Math.min(run.text.length, matchEnd - runStart),
107
+ });
108
+ }
109
+ pos = runEnd;
110
+ }
439
111
  if (overlaps.length <= 1)
440
112
  return null;
441
- const totalOverlap = overlaps.reduce((sum, x) => sum + x.overlap, 0);
442
- if (totalOverlap <= 0)
113
+ const totalOldLen = overlaps.reduce((sum, o) => sum + (o.end - o.start), 0);
114
+ if (totalOldLen === 0)
443
115
  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
116
  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
- });
469
- }
470
- if (cursor < replacementText.length && parts.length > 0) {
471
- parts[parts.length - 1].text += replacementText.slice(cursor);
472
- }
473
- return parts.length > 0 ? parts : null;
117
+ let distributedLen = 0;
118
+ for (let i = 0; i < overlaps.length; i++) {
119
+ const o = overlaps[i];
120
+ const oldPartLen = o.end - o.start;
121
+ const ratio = oldPartLen / totalOldLen;
122
+ let newPartLen = Math.round(ratio * newStr.length);
123
+ if (i === overlaps.length - 1)
124
+ newPartLen = newStr.length - distributedLen;
125
+ const text = newStr.slice(distributedLen, distributedLen + newPartLen);
126
+ distributedLen += newPartLen;
127
+ if (text) {
128
+ parts.push({
129
+ text,
130
+ templateRun: o.r,
131
+ clearHighlight: shouldClearHighlight,
132
+ });
133
+ }
134
+ }
135
+ return parts;
136
+ }
137
+ function isLikelyFieldPlaceholder(text) {
138
+ const t = text.trim();
139
+ return (t.startsWith('[') && t.endsWith(']')) || (t.startsWith('«') && t.endsWith('»'));
474
140
  }
475
141
  export async function replaceText(manager, params) {
476
142
  try {
@@ -478,151 +144,97 @@ export async function replaceText(manager, params) {
478
144
  if (!resolved.ok)
479
145
  return resolved.response;
480
146
  const { session, metadata } = resolved;
481
- // Optional run normalization before search to handle fragmented runs
482
147
  if (params.normalize_first) {
483
148
  session.doc.mergeRunsOnly();
484
149
  }
485
150
  const { target_paragraph_id: pid } = params;
486
- const legacyDefinitionTags = useLegacyDefinitionTags();
487
151
  const oldStr = stripSearchTags(params.old_string);
488
152
  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
153
  if (hasHyperlinkTags(newStr))
495
154
  newStr = stripHyperlinkTags(newStr);
496
- // Find the paragraph and do a unique find/replace.
497
155
  const beforeTextRaw = session.doc.getParagraphTextById(pid);
498
156
  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');
157
+ return err('ANCHOR_NOT_FOUND', `Paragraph ID ${pid} not found in document`);
500
158
  }
501
159
  const paraText = beforeTextRaw;
502
- const textMatch = findUniqueSubstringMatch(paraText, oldStr);
160
+ const findMode = params.clean_match ? 'clean' : 'default';
161
+ const textMatch = findUniqueSubstringMatch(paraText, oldStr, { mode: findMode });
503
162
  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.');
163
+ return err('TEXT_NOT_FOUND', `Text '${previewText(oldStr, ERROR_PREVIEW_CHARS)}' not found in paragraph ${pid}`);
505
164
  }
506
165
  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.');
166
+ return err('MULTIPLE_MATCHES', `Found ${textMatch.matchCount} matches for '${previewText(oldStr, ERROR_PREVIEW_CHARS)}' in paragraph. Need unique match.`);
508
167
  }
509
168
  const pEl = session.doc.getParagraphElementById(pid);
510
169
  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');
170
+ return err('ANCHOR_NOT_FOUND', `Paragraph ID ${pid} not found in document`);
512
171
  }
513
172
  const matchedOldStr = textMatch.matchedText;
514
173
  const matchStart = textMatch.start;
515
174
  const matchEnd = textMatch.end;
516
175
  const paraRuns = getParagraphRuns(pEl);
517
176
  const { templateRun: contextTemplateRun, allOverlappedRunsHighlighted } = chooseContextTemplateRun(paraRuns, matchStart, matchEnd);
518
- const shouldClearHighlight = allOverlappedRunsHighlighted && !hasHighlightTags(newStr) && isLikelyFieldPlaceholder(oldStr);
519
- // Apply edit.
177
+ const explicitAddProps = {};
178
+ if (params.bold !== undefined)
179
+ explicitAddProps.bold = params.bold;
180
+ if (params.italic !== undefined)
181
+ explicitAddProps.italic = params.italic;
182
+ if (params.underline !== undefined)
183
+ explicitAddProps.underline = params.underline;
184
+ if (params.highlight !== undefined)
185
+ explicitAddProps.highlight = params.highlight;
186
+ if (params.font_size !== undefined)
187
+ explicitAddProps.fontSize = params.font_size * 2;
188
+ if (params.font_name !== undefined)
189
+ explicitAddProps.fontName = params.font_name;
190
+ if (params.color !== undefined)
191
+ explicitAddProps.color = params.color;
192
+ const shouldClearHighlight = params.clear_highlight || (allOverlappedRunsHighlighted && !hasHighlightTags(newStr) && isLikelyFieldPlaceholder(oldStr));
520
193
  let replaceText = newStr;
521
- const hasMarkup = hasHighlightTags(newStr) ||
522
- hasHeaderTags(newStr) ||
523
- hasInlineStyleTags(newStr) ||
524
- (legacyDefinitionTags && hasDefinitionTags(newStr));
194
+ const hasMarkup = hasAnyMarkupTags(newStr);
525
195
  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;
196
+ let segs;
197
+ try {
198
+ segs = splitTaggedText(newStr);
199
+ }
200
+ catch (e) {
201
+ return err(errorMessage(e), `Tag parse error in new_string: ${errorMessage(e)}`);
202
+ }
203
+ const headerAddProps = segs.some((s) => s.header) ? findHeaderRoleModelAddRunProps(session, pid) : null;
535
204
  const parts = [];
536
205
  for (const s of segs) {
537
206
  if (!s.text)
538
207
  continue;
539
- const segAddProps = mergeAddRunProps(segmentAddRunProps(s), s.header ? headerAddProps : null);
208
+ const segAddProps = mergeAddRunProps(mergeAddRunProps(segmentAddRunProps(s), explicitAddProps), s.header ? headerAddProps : null);
540
209
  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
- }
210
+ parts.push({ text: s.text, templateRun: contextTemplateRun ?? undefined, addRunProps: segAddProps, clearHighlight });
572
211
  }
573
212
  replaceText = parts;
574
213
  }
575
214
  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
- ];
215
+ const distributed = buildDistributedPartsAcrossRuns(paraRuns, matchStart, matchEnd, newStr, shouldClearHighlight);
216
+ if (distributed && distributed.length > 0) {
217
+ replaceText = distributed.map(d => ({ ...d, addRunProps: mergeAddRunProps(d.addRunProps, explicitAddProps) }));
592
218
  }
593
- else {
594
- const distributed = buildDistributedPartsAcrossRuns(paraRuns, matchStart, matchEnd, newStr, shouldClearHighlight);
595
- if (distributed && distributed.length > 0) {
596
- replaceText = distributed;
597
- }
598
- else if (shouldClearHighlight || contextTemplateRun) {
599
- replaceText = [
600
- {
601
- text: newStr,
602
- templateRun: contextTemplateRun ?? undefined,
603
- clearHighlight: shouldClearHighlight,
604
- },
605
- ];
606
- }
219
+ else if (shouldClearHighlight || contextTemplateRun || Object.keys(explicitAddProps).length > 0) {
220
+ replaceText = [{ text: newStr, templateRun: contextTemplateRun ?? undefined, addRunProps: explicitAddProps, clearHighlight: shouldClearHighlight }];
607
221
  }
608
222
  }
609
223
  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