@usejunior/docx-mcp 0.1.1 → 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.
- package/README.md +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/conformance/harness.js +7 -7
- package/dist/conformance/harness.js.map +1 -1
- package/dist/server.d.ts +5 -5
- package/dist/server.js +8 -8
- package/dist/server.js.map +1 -1
- package/dist/session/manager.d.ts +6 -6
- package/dist/session/manager.d.ts.map +1 -1
- package/dist/session/manager.js +6 -6
- package/dist/session/manager.js.map +1 -1
- package/dist/tool_catalog.d.ts +26 -20
- package/dist/tool_catalog.d.ts.map +1 -1
- package/dist/tool_catalog.js +18 -13
- package/dist/tool_catalog.js.map +1 -1
- package/dist/tools/clear_formatting.d.ts +14 -0
- package/dist/tools/clear_formatting.d.ts.map +1 -0
- package/dist/tools/clear_formatting.js +94 -0
- package/dist/tools/clear_formatting.js.map +1 -0
- package/dist/tools/get_session_status.js +3 -3
- package/dist/tools/get_session_status.js.map +1 -1
- package/dist/tools/insert_paragraph.d.ts +1 -0
- package/dist/tools/insert_paragraph.d.ts.map +1 -1
- package/dist/tools/insert_paragraph.js +61 -397
- package/dist/tools/insert_paragraph.js.map +1 -1
- package/dist/tools/open_document.js +5 -5
- package/dist/tools/open_document.js.map +1 -1
- package/dist/tools/replace_text.d.ts +9 -0
- package/dist/tools/replace_text.d.ts.map +1 -1
- package/dist/tools/replace_text.js +128 -516
- package/dist/tools/replace_text.js.map +1 -1
- package/dist/tools/{download.d.ts → save.d.ts} +4 -4
- package/dist/tools/save.d.ts.map +1 -0
- package/dist/tools/{download.js → save.js} +21 -20
- package/dist/tools/save.js.map +1 -0
- package/dist/tools/tag_parser.d.ts +28 -0
- package/dist/tools/tag_parser.d.ts.map +1 -0
- package/dist/tools/tag_parser.js +327 -0
- package/dist/tools/tag_parser.js.map +1 -0
- package/package.json +2 -2
- package/dist/tools/download.d.ts.map +0 -1
- package/dist/tools/download.js.map +0 -1
- package/dist/tools/duplicate_document.d.ts +0 -8
- package/dist/tools/duplicate_document.d.ts.map +0 -1
- package/dist/tools/duplicate_document.js +0 -90
- 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,
|
|
6
|
-
|
|
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 (
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
|
427
|
-
|
|
428
|
-
|
|
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
|
|
442
|
-
if (
|
|
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
|
|
459
|
-
for (
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
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
|
|
157
|
+
return err('ANCHOR_NOT_FOUND', `Paragraph ID ${pid} not found in document`);
|
|
500
158
|
}
|
|
501
159
|
const paraText = beforeTextRaw;
|
|
502
|
-
const
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
519
|
-
|
|
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 =
|
|
522
|
-
hasHeaderTags(newStr) ||
|
|
523
|
-
hasInlineStyleTags(newStr) ||
|
|
524
|
-
(legacyDefinitionTags && hasDefinitionTags(newStr));
|
|
194
|
+
const hasMarkup = hasAnyMarkupTags(newStr);
|
|
525
195
|
if (hasMarkup) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
:
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
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
|
|
577
|
-
|
|
578
|
-
|
|
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
|
-
|
|
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(
|
|
620
|
-
after_text: previewText(
|
|
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}
|
|
237
|
+
return err('EDIT_ERROR', `Failed to edit document: ${msg}`);
|
|
626
238
|
}
|
|
627
239
|
}
|
|
628
240
|
//# sourceMappingURL=replace_text.js.map
|