@ansonlai/docx-redline-js 0.7.0 → 0.7.2
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/AGENTS.md +22 -27
- package/CHANGELOG.md +21 -1
- package/README.md +43 -22
- package/dist/docx-redline-js.esm.js +3 -4
- package/dist/docx-redline-js.esm.js.map +2 -2
- package/dist/docx-redline-js.esm.min.js +51 -51
- package/dist/docx-redline-js.esm.min.js.map +2 -2
- package/docs/AGENT_FAST_START.md +31 -33
- package/docs/AGENT_KNOWLEDGE_BASE.md +67 -55
- package/docs/SKILL_AUTHORING.md +82 -100
- package/docs/releases/0.7.0.md +56 -36
- package/docs/schemas/document-operations.schema.json +1 -1
- package/docs/validation-reports/2026-09-14-agent-surface-simplification.md +75 -0
- package/node/cli-help.js +8 -6
- package/node/cli.js +57 -9
- package/package.json +4 -1
- package/services/document-inspection.js +2 -3
- package/services/document-operation-applier.js +12 -2
- package/services/document-operation-contract.js +49 -2
- package/services/document-operation-mutations.js +36 -36
- package/services/localized-replacement-compiler.js +73 -7
- package/services/operation-batch-compiler.js +9 -0
- package/services/standalone-operation-runner.d.ts +25 -4
|
@@ -223,7 +223,37 @@ function describeTargetTextMatch(actualText, requestedText) {
|
|
|
223
223
|
requestedExcerpt: escapeInvisibleText(requested)
|
|
224
224
|
};
|
|
225
225
|
}
|
|
226
|
-
|
|
226
|
+
|
|
227
|
+
function resolvedTargetDiagnostics(xmlDoc, paragraph, metadata, targetText, options) {
|
|
228
|
+
const revisionView = options?.targetDescriptor?.revisionView === 'rejected'
|
|
229
|
+
? 'rejected'
|
|
230
|
+
: 'accepted';
|
|
231
|
+
const viewMetadata = metadata?.revisionView === revisionView ? metadata : null;
|
|
232
|
+
const index = viewMetadata?.index ?? getDocumentParagraphNodes(xmlDoc).indexOf(paragraph) + 1;
|
|
233
|
+
const paragraphId = viewMetadata?.paragraphId ?? getParagraphId(paragraph);
|
|
234
|
+
const inTable = viewMetadata?.inTable ?? !!findContainingWordElement(paragraph, 'tbl');
|
|
235
|
+
const text = viewMetadata?.text ?? extractCanonicalParagraphText(paragraph, { revisionView });
|
|
236
|
+
const fingerprint = viewMetadata?.fingerprint ?? createParagraphFingerprint(paragraph, {
|
|
237
|
+
text,
|
|
238
|
+
index,
|
|
239
|
+
paragraphId,
|
|
240
|
+
inTable,
|
|
241
|
+
revisionView
|
|
242
|
+
});
|
|
243
|
+
return {
|
|
244
|
+
index,
|
|
245
|
+
paragraphId,
|
|
246
|
+
text,
|
|
247
|
+
fingerprint,
|
|
248
|
+
revisionView,
|
|
249
|
+
inTable,
|
|
250
|
+
targetTextMatch: describeTargetTextMatch(
|
|
251
|
+
text,
|
|
252
|
+
options?.targetDescriptor?.text ?? options?.targetDescriptor?.exactText ?? targetText
|
|
253
|
+
)
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
227
257
|
function resolveTargetParagraph(xmlDoc, targetText, targetRef, opType, runtimeContext = null, options = {}) {
|
|
228
258
|
const onInfo = typeof options?.onInfo === 'function' ? options.onInfo : () => { };
|
|
229
259
|
const onWarn = typeof options?.onWarn === 'function' ? options.onWarn : () => { };
|
|
@@ -239,17 +269,7 @@ function resolveTargetParagraph(xmlDoc, targetText, targetRef, opType, runtimeCo
|
|
|
239
269
|
const metadata = paragraphMetadataIndex?.byParagraph?.get(paragraph) || null;
|
|
240
270
|
Object.assign(options._resolutionCapture, {
|
|
241
271
|
resolvedBy: resolved.resolvedBy,
|
|
242
|
-
resolvedTarget:
|
|
243
|
-
index: metadata?.index ?? getDocumentParagraphNodes(xmlDoc).indexOf(paragraph) + 1,
|
|
244
|
-
paragraphId: metadata?.paragraphId ?? getParagraphId(paragraph),
|
|
245
|
-
text: metadata?.text ?? getParagraphText(paragraph),
|
|
246
|
-
fingerprint: metadata?.fingerprint ?? createParagraphFingerprint(paragraph),
|
|
247
|
-
inTable: metadata?.inTable ?? !!findContainingWordElement(paragraph, 'tbl'),
|
|
248
|
-
targetTextMatch: describeTargetTextMatch(
|
|
249
|
-
metadata?.text ?? getParagraphText(paragraph),
|
|
250
|
-
options?.targetDescriptor?.exactText ?? targetText
|
|
251
|
-
)
|
|
252
|
-
}
|
|
272
|
+
resolvedTarget: resolvedTargetDiagnostics(xmlDoc, paragraph, metadata, targetText, options)
|
|
253
273
|
});
|
|
254
274
|
}
|
|
255
275
|
return resolved;
|
|
@@ -263,17 +283,7 @@ function resolveTargetParagraph(xmlDoc, targetText, targetRef, opType, runtimeCo
|
|
|
263
283
|
const metadata = paragraphMetadataIndex?.byParagraph?.get(paragraph) || null;
|
|
264
284
|
Object.assign(options._resolutionCapture, {
|
|
265
285
|
resolvedBy: resolved.resolvedBy,
|
|
266
|
-
resolvedTarget:
|
|
267
|
-
index: metadata?.index ?? Array.from(xmlDoc.getElementsByTagNameNS(NS_W, 'p')).indexOf(paragraph) + 1,
|
|
268
|
-
paragraphId: metadata?.paragraphId ?? getParagraphId(paragraph),
|
|
269
|
-
text: metadata?.text ?? getParagraphText(paragraph),
|
|
270
|
-
fingerprint: metadata?.fingerprint ?? createParagraphFingerprint(paragraph),
|
|
271
|
-
inTable: metadata?.inTable ?? !!findContainingWordElement(paragraph, 'tbl'),
|
|
272
|
-
targetTextMatch: describeTargetTextMatch(
|
|
273
|
-
metadata?.text ?? getParagraphText(paragraph),
|
|
274
|
-
options?.targetDescriptor?.exactText ?? targetText
|
|
275
|
-
)
|
|
276
|
-
}
|
|
286
|
+
resolvedTarget: resolvedTargetDiagnostics(xmlDoc, paragraph, metadata, targetText, options)
|
|
277
287
|
});
|
|
278
288
|
}
|
|
279
289
|
return resolved;
|
|
@@ -301,19 +311,9 @@ function resolveTargetParagraph(xmlDoc, targetText, targetRef, opType, runtimeCo
|
|
|
301
311
|
if (options?._resolutionCapture && resolved?.paragraph) {
|
|
302
312
|
const paragraph = resolved.paragraph;
|
|
303
313
|
const metadata = paragraphMetadataIndex?.byParagraph?.get(paragraph) || null;
|
|
304
|
-
Object.assign(options._resolutionCapture, {
|
|
305
|
-
resolvedBy: resolved.resolvedBy,
|
|
306
|
-
resolvedTarget:
|
|
307
|
-
index: metadata?.index ?? Array.from(xmlDoc.getElementsByTagNameNS(NS_W, 'p')).indexOf(paragraph) + 1,
|
|
308
|
-
paragraphId: metadata?.paragraphId ?? getParagraphId(paragraph),
|
|
309
|
-
text: metadata?.text ?? getParagraphText(paragraph),
|
|
310
|
-
fingerprint: metadata?.fingerprint ?? createParagraphFingerprint(paragraph),
|
|
311
|
-
inTable: metadata?.inTable ?? !!findContainingWordElement(paragraph, 'tbl'),
|
|
312
|
-
targetTextMatch: describeTargetTextMatch(
|
|
313
|
-
metadata?.text ?? getParagraphText(paragraph),
|
|
314
|
-
options?.targetDescriptor?.exactText ?? targetText
|
|
315
|
-
)
|
|
316
|
-
}
|
|
314
|
+
Object.assign(options._resolutionCapture, {
|
|
315
|
+
resolvedBy: resolved.resolvedBy,
|
|
316
|
+
resolvedTarget: resolvedTargetDiagnostics(xmlDoc, paragraph, metadata, targetText, options)
|
|
317
317
|
});
|
|
318
318
|
}
|
|
319
319
|
return resolved;
|
|
@@ -41,6 +41,65 @@ function occurrenceOffsets(sourceText, find) {
|
|
|
41
41
|
return offsets;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function equivalentSpace(left, right) {
|
|
45
|
+
return (left === ' ' || left === '\u00a0')
|
|
46
|
+
&& (right === ' ' || right === '\u00a0');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function spaceEquivalentOffsets(sourceText, find) {
|
|
50
|
+
const offsets = [];
|
|
51
|
+
for (let start = 0; start <= sourceText.length - find.length; start += 1) {
|
|
52
|
+
let matches = true;
|
|
53
|
+
for (let offset = 0; offset < find.length; offset += 1) {
|
|
54
|
+
const sourceCharacter = sourceText[start + offset];
|
|
55
|
+
const findCharacter = find[offset];
|
|
56
|
+
if (sourceCharacter !== findCharacter && !equivalentSpace(sourceCharacter, findCharacter)) {
|
|
57
|
+
matches = false;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (matches) offsets.push(start);
|
|
62
|
+
}
|
|
63
|
+
return offsets;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Resolve exact offsets first, then the narrow ASCII-space/NBSP equivalent fallback. */
|
|
67
|
+
export function localizedReplacementOffsets(sourceText, find) {
|
|
68
|
+
const exact = occurrenceOffsets(sourceText, find);
|
|
69
|
+
if (exact.length > 0) return { offsets: exact, matchMode: 'exact' };
|
|
70
|
+
return { offsets: spaceEquivalentOffsets(sourceText, find), matchMode: 'space_equivalent' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function preserveEquivalentBoundarySpaces(sourceSlice, find, replace) {
|
|
74
|
+
const effective = replace.split('');
|
|
75
|
+
let prefix = 0;
|
|
76
|
+
while (
|
|
77
|
+
prefix < find.length
|
|
78
|
+
&& prefix < replace.length
|
|
79
|
+
&& (find[prefix] === replace[prefix] || equivalentSpace(find[prefix], replace[prefix]))
|
|
80
|
+
) {
|
|
81
|
+
if (equivalentSpace(sourceSlice[prefix], replace[prefix])) effective[prefix] = sourceSlice[prefix];
|
|
82
|
+
prefix += 1;
|
|
83
|
+
}
|
|
84
|
+
let sourceOffset = find.length - 1;
|
|
85
|
+
let replacementOffset = replace.length - 1;
|
|
86
|
+
while (
|
|
87
|
+
sourceOffset >= prefix
|
|
88
|
+
&& replacementOffset >= prefix
|
|
89
|
+
&& (
|
|
90
|
+
find[sourceOffset] === replace[replacementOffset]
|
|
91
|
+
|| equivalentSpace(find[sourceOffset], replace[replacementOffset])
|
|
92
|
+
)
|
|
93
|
+
) {
|
|
94
|
+
if (equivalentSpace(sourceSlice[sourceOffset], replace[replacementOffset])) {
|
|
95
|
+
effective[replacementOffset] = sourceSlice[sourceOffset];
|
|
96
|
+
}
|
|
97
|
+
sourceOffset -= 1;
|
|
98
|
+
replacementOffset -= 1;
|
|
99
|
+
}
|
|
100
|
+
return effective.join('');
|
|
101
|
+
}
|
|
102
|
+
|
|
44
103
|
function offsetCandidate(sourceText, start, length) {
|
|
45
104
|
const excerptStart = Math.max(0, start - 40);
|
|
46
105
|
const excerptEnd = Math.min(sourceText.length, start + length + 40);
|
|
@@ -168,15 +227,15 @@ export function compileExactReplacements(
|
|
|
168
227
|
const replacement = validation.replacements[index];
|
|
169
228
|
const itemField = `${field}[${index}]`;
|
|
170
229
|
const occurrence = replacement.occurrence ?? null;
|
|
171
|
-
const offsets =
|
|
230
|
+
const { offsets, matchMode } = localizedReplacementOffsets(sourceText, replacement.find);
|
|
172
231
|
if (offsets.length === 0 || (occurrence != null && occurrence > offsets.length)) {
|
|
173
232
|
return {
|
|
174
233
|
ok: false,
|
|
175
234
|
error: patchError(
|
|
176
235
|
'PATCH_SOURCE_NOT_FOUND',
|
|
177
236
|
occurrence == null
|
|
178
|
-
? `
|
|
179
|
-
: `Occurrence ${occurrence} of
|
|
237
|
+
? `Patch source was not found: "${replacement.find}".`
|
|
238
|
+
: `Occurrence ${occurrence} of patch source was not found: "${replacement.find}".`,
|
|
180
239
|
itemField,
|
|
181
240
|
'change_patch',
|
|
182
241
|
{ matchCount: offsets.length }
|
|
@@ -188,7 +247,7 @@ export function compileExactReplacements(
|
|
|
188
247
|
ok: false,
|
|
189
248
|
error: patchError(
|
|
190
249
|
'AMBIGUOUS_PATCH_SOURCE',
|
|
191
|
-
`
|
|
250
|
+
`Patch source matched ${offsets.length} locations; provide occurrence.`,
|
|
192
251
|
itemField,
|
|
193
252
|
'choose_occurrence',
|
|
194
253
|
{ candidates: offsets.map(start => offsetCandidate(sourceText, start, replacement.find.length)) }
|
|
@@ -197,12 +256,17 @@ export function compileExactReplacements(
|
|
|
197
256
|
}
|
|
198
257
|
|
|
199
258
|
const start = offsets[(occurrence || 1) - 1];
|
|
259
|
+
const sourceSlice = sourceText.slice(start, start + replacement.find.length);
|
|
200
260
|
resolved.push({
|
|
201
261
|
requestIndex: index,
|
|
202
262
|
start,
|
|
203
263
|
end: start + replacement.find.length,
|
|
204
264
|
find: replacement.find,
|
|
205
265
|
replace: replacement.replace,
|
|
266
|
+
effectiveReplace: matchMode === 'space_equivalent'
|
|
267
|
+
? preserveEquivalentBoundarySpaces(sourceSlice, replacement.find, replacement.replace)
|
|
268
|
+
: replacement.replace,
|
|
269
|
+
matchMode,
|
|
206
270
|
occurrence: occurrence || 1
|
|
207
271
|
});
|
|
208
272
|
}
|
|
@@ -212,7 +276,7 @@ export function compileExactReplacements(
|
|
|
212
276
|
for (const replacement of resolved) {
|
|
213
277
|
const previous = unique.at(-1);
|
|
214
278
|
if (previous && replacement.start === previous.start && replacement.end === previous.end) {
|
|
215
|
-
if (replacement.
|
|
279
|
+
if (replacement.effectiveReplace !== previous.effectiveReplace) {
|
|
216
280
|
return {
|
|
217
281
|
ok: false,
|
|
218
282
|
error: patchError(
|
|
@@ -245,7 +309,7 @@ export function compileExactReplacements(
|
|
|
245
309
|
let desiredText = '';
|
|
246
310
|
for (const replacement of unique) {
|
|
247
311
|
desiredText += sourceText.slice(cursor, replacement.start);
|
|
248
|
-
desiredText += replacement.
|
|
312
|
+
desiredText += replacement.effectiveReplace;
|
|
249
313
|
cursor = replacement.end;
|
|
250
314
|
}
|
|
251
315
|
desiredText += sourceText.slice(cursor);
|
|
@@ -259,9 +323,10 @@ export function compileExactReplacements(
|
|
|
259
323
|
end: replacement.end,
|
|
260
324
|
find: replacement.find,
|
|
261
325
|
replace: replacement.replace,
|
|
326
|
+
matchMode: replacement.matchMode,
|
|
262
327
|
occurrence: replacement.occurrence,
|
|
263
328
|
removedLength: replacement.end - replacement.start,
|
|
264
|
-
insertedLength: replacement.
|
|
329
|
+
insertedLength: replacement.effectiveReplace.length
|
|
265
330
|
}))
|
|
266
331
|
};
|
|
267
332
|
}
|
|
@@ -280,6 +345,7 @@ export function buildLocalizedReplacementChange(compilation, outputText, target
|
|
|
280
345
|
return {
|
|
281
346
|
find: replacement.find,
|
|
282
347
|
replace: replacement.replace,
|
|
348
|
+
...(replacement.matchMode === 'space_equivalent' ? { matchMode: replacement.matchMode } : {}),
|
|
283
349
|
occurrence: replacement.occurrence,
|
|
284
350
|
beforeExcerpt: boundedExcerpt(sourceText, replacement.start, replacement.end),
|
|
285
351
|
afterExcerpt: boundedExcerpt(desiredText, afterStart, afterEnd)
|
|
@@ -296,6 +296,15 @@ export function compileOperationBatch(xmlDoc, operations = [], options = {}) {
|
|
|
296
296
|
canonicalValidation.error
|
|
297
297
|
);
|
|
298
298
|
}
|
|
299
|
+
} else if (operation.operationKind === 'restore' && sourceOperation.modified === undefined) {
|
|
300
|
+
compiled.modified = start.metadata.text;
|
|
301
|
+
const canonicalValidation = validateDocumentOperation(compiled);
|
|
302
|
+
if (!canonicalValidation.valid) {
|
|
303
|
+
throw Object.assign(
|
|
304
|
+
new Error(canonicalValidation.error.message),
|
|
305
|
+
canonicalValidation.error
|
|
306
|
+
);
|
|
307
|
+
}
|
|
299
308
|
}
|
|
300
309
|
|
|
301
310
|
const targetEndDescriptor = operation.targetEndDescriptor
|
|
@@ -75,13 +75,32 @@ export type RedlineDocumentOperation =
|
|
|
75
75
|
replacements?: never;
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
interface RestoreDocumentOperationCommon extends DocumentOperationBase {
|
|
79
79
|
type: 'restore';
|
|
80
|
-
modified: string | string[];
|
|
81
|
-
targetEnd?: ParagraphTargetDescriptor;
|
|
82
|
-
targetEndRef?: number | string | null;
|
|
83
80
|
}
|
|
84
81
|
|
|
82
|
+
export type RestoreDocumentOperation =
|
|
83
|
+
| (RestoreDocumentOperationCommon & {
|
|
84
|
+
modified: string | string[];
|
|
85
|
+
replacements?: never;
|
|
86
|
+
targetEnd?: ParagraphTargetDescriptor;
|
|
87
|
+
targetEndRef?: number | string | null;
|
|
88
|
+
})
|
|
89
|
+
| (RestoreDocumentOperationCommon & {
|
|
90
|
+
modified?: never;
|
|
91
|
+
/** Exact, case-sensitive replacements in one strong rejected-view paragraph. */
|
|
92
|
+
replacements: LocalizedReplacement[];
|
|
93
|
+
targetEnd?: never;
|
|
94
|
+
targetEndRef?: never;
|
|
95
|
+
})
|
|
96
|
+
| (RestoreDocumentOperationCommon & {
|
|
97
|
+
/** Omit modified to restore one strong rejected-view paragraph verbatim. */
|
|
98
|
+
modified?: never;
|
|
99
|
+
replacements?: never;
|
|
100
|
+
targetEnd?: never;
|
|
101
|
+
targetEndRef?: never;
|
|
102
|
+
});
|
|
103
|
+
|
|
85
104
|
export interface DeleteDocumentOperation extends DocumentOperationBase {
|
|
86
105
|
type: 'delete';
|
|
87
106
|
modified?: '';
|
|
@@ -155,6 +174,7 @@ export interface ResolvedDocumentTarget {
|
|
|
155
174
|
paragraphId: string | null;
|
|
156
175
|
text: string;
|
|
157
176
|
fingerprint?: string;
|
|
177
|
+
revisionView?: 'accepted' | 'rejected';
|
|
158
178
|
inTable?: boolean;
|
|
159
179
|
targetTextMatch?: {
|
|
160
180
|
mode: 'exact' | 'space_equivalent' | 'normalized';
|
|
@@ -232,6 +252,7 @@ export interface LocalizedReplacementChange {
|
|
|
232
252
|
find: string;
|
|
233
253
|
replace: string;
|
|
234
254
|
occurrence?: number;
|
|
255
|
+
matchMode?: 'space_equivalent';
|
|
235
256
|
beforeExcerpt: string;
|
|
236
257
|
afterExcerpt: string;
|
|
237
258
|
}>;
|