@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.
- package/README.md +4 -4
- package/dist/.tsbuildinfo +1 -1
- package/dist/cli/commands/edit.d.ts +24 -0
- package/dist/cli/commands/edit.d.ts.map +1 -0
- package/dist/cli/commands/edit.js +145 -0
- package/dist/cli/commands/edit.js.map +1 -0
- package/dist/cli/flag_parser.d.ts +7 -0
- package/dist/cli/flag_parser.d.ts.map +1 -0
- package/dist/cli/flag_parser.js +235 -0
- package/dist/cli/flag_parser.js.map +1 -0
- package/dist/cli/help.d.ts +3 -0
- package/dist/cli/help.d.ts.map +1 -0
- package/dist/cli/help.js +62 -0
- package/dist/cli/help.js.map +1 -0
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +29 -32
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/parse_utils.d.ts +7 -0
- package/dist/cli/parse_utils.d.ts.map +1 -0
- package/dist/cli/parse_utils.js +18 -0
- package/dist/cli/parse_utils.js.map +1 -0
- package/dist/cli/tool_runner.d.ts +6 -0
- package/dist/cli/tool_runner.d.ts.map +1 -0
- package/dist/cli/tool_runner.js +17 -0
- package/dist/cli/tool_runner.js.map +1 -0
- 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 +20 -6
- package/dist/session/manager.d.ts.map +1 -1
- package/dist/session/manager.js +28 -6
- package/dist/session/manager.js.map +1 -1
- package/dist/tool_catalog.d.ts +29 -22
- package/dist/tool_catalog.d.ts.map +1 -1
- package/dist/tool_catalog.js +23 -17
- 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/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +8 -1
- package/dist/tools/grep.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.d.ts.map +1 -1
- package/dist/tools/open_document.js +9 -22
- package/dist/tools/open_document.js.map +1 -1
- package/dist/tools/pagination.d.ts +9 -0
- package/dist/tools/pagination.d.ts.map +1 -0
- package/dist/tools/pagination.js +16 -0
- package/dist/tools/pagination.js.map +1 -0
- package/dist/tools/read_file.d.ts.map +1 -1
- package/dist/tools/read_file.js +96 -17
- package/dist/tools/read_file.js.map +1 -1
- package/dist/tools/replace_text.d.ts +18 -0
- package/dist/tools/replace_text.d.ts.map +1 -1
- package/dist/tools/replace_text.js +129 -517
- 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/session_resolution.d.ts.map +1 -1
- package/dist/tools/session_resolution.js +68 -18
- package/dist/tools/session_resolution.js.map +1 -1
- 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, 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 (
|
|
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,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
|
|
440
|
-
return null;
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
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
|
-
|
|
471
|
-
|
|
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
|
|
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
|
|
135
|
+
return err('ANCHOR_NOT_FOUND', `Paragraph ID ${pid} not found in document`);
|
|
500
136
|
}
|
|
501
137
|
const paraText = beforeTextRaw;
|
|
502
|
-
const
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
519
|
-
|
|
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 =
|
|
522
|
-
hasHeaderTags(newStr) ||
|
|
523
|
-
hasInlineStyleTags(newStr) ||
|
|
524
|
-
(legacyDefinitionTags && hasDefinitionTags(newStr));
|
|
172
|
+
const hasMarkup = hasAnyMarkupTags(newStr);
|
|
525
173
|
if (hasMarkup) {
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
:
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
];
|
|
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
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
|
599
|
-
|
|
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(
|
|
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
|