@json-to-office/shared-docx 0.13.0 → 0.14.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/dist/{chunk-ZQNAWKEI.js → chunk-52PV5JQK.js} +2 -2
- package/dist/chunk-52PV5JQK.js.map +1 -0
- package/dist/{chunk-7LRSVYGN.js → chunk-5BKZFYJL.js} +61 -51
- package/dist/chunk-5BKZFYJL.js.map +1 -0
- package/dist/{chunk-CIBMJ7QA.js → chunk-6IXK54YE.js} +2 -2
- package/dist/{chunk-YUV5CJMG.js → chunk-CGMJ665Z.js} +488 -436
- package/dist/chunk-CGMJ665Z.js.map +1 -0
- package/dist/{chunk-BFJJ5HYD.js → chunk-CY6IVMEZ.js} +7 -2
- package/dist/chunk-CY6IVMEZ.js.map +1 -0
- package/dist/{chunk-VBZC5WT6.js → chunk-FA6FKO3L.js} +2 -2
- package/dist/{chunk-ANU2V2BM.js → chunk-HJIS6RRQ.js} +6 -6
- package/dist/{chunk-25SAKHVN.js → chunk-PQGT7BU5.js} +2 -2
- package/dist/{chunk-IHZIO72A.js → chunk-QUA2GDH7.js} +2 -2
- package/dist/{chunk-YK2UUG65.js → chunk-X2B5OVWA.js} +2 -2
- package/dist/index.d.ts +144 -19
- package/dist/index.js +688 -10
- package/dist/index.js.map +1 -1
- package/dist/schemas/api.js +4 -4
- package/dist/schemas/component-defaults.d.ts +48 -32
- package/dist/schemas/component-defaults.js +1 -1
- package/dist/schemas/component-registry.js +2 -2
- package/dist/schemas/components.d.ts +90 -33
- package/dist/schemas/components.js +7 -3
- package/dist/schemas/document.js +5 -5
- package/dist/schemas/export.js +3 -3
- package/dist/schemas/generator.js +3 -3
- package/dist/schemas/theme.d.ts +24 -16
- package/dist/schemas/theme.js +2 -2
- package/dist/validation/unified/index.d.ts +59 -19
- package/dist/validation/unified/index.js +6 -6
- package/package.json +2 -2
- package/dist/chunk-7LRSVYGN.js.map +0 -1
- package/dist/chunk-BFJJ5HYD.js.map +0 -1
- package/dist/chunk-YUV5CJMG.js.map +0 -1
- package/dist/chunk-ZQNAWKEI.js.map +0 -1
- /package/dist/{chunk-CIBMJ7QA.js.map → chunk-6IXK54YE.js.map} +0 -0
- /package/dist/{chunk-VBZC5WT6.js.map → chunk-FA6FKO3L.js.map} +0 -0
- /package/dist/{chunk-ANU2V2BM.js.map → chunk-HJIS6RRQ.js.map} +0 -0
- /package/dist/{chunk-25SAKHVN.js.map → chunk-PQGT7BU5.js.map} +0 -0
- /package/dist/{chunk-IHZIO72A.js.map → chunk-QUA2GDH7.js.map} +0 -0
- /package/dist/{chunk-YK2UUG65.js.map → chunk-X2B5OVWA.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
generateUnifiedDocumentSchema
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-PQGT7BU5.js";
|
|
4
4
|
import {
|
|
5
5
|
DOC_LINKS,
|
|
6
6
|
ERROR_TEMPLATES,
|
|
@@ -32,21 +32,21 @@ import {
|
|
|
32
32
|
validateTheme,
|
|
33
33
|
validateThemeJson,
|
|
34
34
|
validateThemeWithEnhancement
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-HJIS6RRQ.js";
|
|
36
36
|
import {
|
|
37
37
|
ThemeConfigSchema,
|
|
38
38
|
isValidThemeConfig
|
|
39
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-X2B5OVWA.js";
|
|
40
40
|
import {
|
|
41
41
|
GenerateDocumentRequestSchema,
|
|
42
42
|
GenerateDocumentResponseSchema,
|
|
43
43
|
ValidateDocumentRequestSchema,
|
|
44
44
|
ValidateDocumentResponseSchema
|
|
45
|
-
} from "./chunk-
|
|
45
|
+
} from "./chunk-QUA2GDH7.js";
|
|
46
46
|
import {
|
|
47
47
|
JSON_SCHEMA_URLS,
|
|
48
48
|
JsonComponentDefinitionSchema
|
|
49
|
-
} from "./chunk-
|
|
49
|
+
} from "./chunk-FA6FKO3L.js";
|
|
50
50
|
import {
|
|
51
51
|
comprehensiveValidateDocument,
|
|
52
52
|
createDocumentValidator,
|
|
@@ -68,11 +68,11 @@ import {
|
|
|
68
68
|
validateJsonComponent,
|
|
69
69
|
validateJsonDocument,
|
|
70
70
|
validateWithEnhancement
|
|
71
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-5BKZFYJL.js";
|
|
72
72
|
import {
|
|
73
73
|
ComponentDefinitionSchema,
|
|
74
74
|
StandardComponentDefinitionSchema
|
|
75
|
-
} from "./chunk-
|
|
75
|
+
} from "./chunk-52PV5JQK.js";
|
|
76
76
|
import {
|
|
77
77
|
CustomComponentDefinitionSchema,
|
|
78
78
|
TextSpaceAfterComponentSchema,
|
|
@@ -86,13 +86,13 @@ import {
|
|
|
86
86
|
createComponentSchema,
|
|
87
87
|
exportSchemaToFile,
|
|
88
88
|
fixSchemaReferences
|
|
89
|
-
} from "./chunk-
|
|
89
|
+
} from "./chunk-6IXK54YE.js";
|
|
90
90
|
import {
|
|
91
91
|
HighchartsPropsSchema,
|
|
92
92
|
ReportPropsSchema,
|
|
93
93
|
TextBoxPropsSchema,
|
|
94
94
|
TocPropsSchema
|
|
95
|
-
} from "./chunk-
|
|
95
|
+
} from "./chunk-CY6IVMEZ.js";
|
|
96
96
|
import {
|
|
97
97
|
AlignmentSchema,
|
|
98
98
|
BaseComponentPropsSchema,
|
|
@@ -108,13 +108,686 @@ import {
|
|
|
108
108
|
MarginsSchema,
|
|
109
109
|
NumberingSchema,
|
|
110
110
|
ParagraphPropsSchema,
|
|
111
|
+
RevisionSchema,
|
|
112
|
+
RevisionSegmentSchema,
|
|
111
113
|
SectionPropsSchema,
|
|
112
114
|
SpacingSchema,
|
|
113
115
|
StatisticPropsSchema,
|
|
114
116
|
TablePropsSchema
|
|
115
|
-
} from "./chunk-
|
|
117
|
+
} from "./chunk-CGMJ665Z.js";
|
|
116
118
|
import "./chunk-H56RLQOF.js";
|
|
117
119
|
|
|
120
|
+
// src/diff/word-diff.ts
|
|
121
|
+
var MAX_LCS_CELLS = 1e6;
|
|
122
|
+
function tokenizeWords(text) {
|
|
123
|
+
if (!text) return [];
|
|
124
|
+
return text.split(/(\s+)/).filter((t) => t.length > 0);
|
|
125
|
+
}
|
|
126
|
+
function mergeSegments(segments) {
|
|
127
|
+
const merged = [];
|
|
128
|
+
for (const seg of segments) {
|
|
129
|
+
if (!seg.text) continue;
|
|
130
|
+
const last = merged[merged.length - 1];
|
|
131
|
+
if (last && last.type === seg.type) {
|
|
132
|
+
last.text += seg.text;
|
|
133
|
+
} else {
|
|
134
|
+
merged.push({ ...seg });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return merged;
|
|
138
|
+
}
|
|
139
|
+
function normalizeSegments(segments) {
|
|
140
|
+
const folded = [];
|
|
141
|
+
for (let k2 = 0; k2 < segments.length; k2++) {
|
|
142
|
+
const seg = segments[k2];
|
|
143
|
+
if (seg.type === "equal" && /^\s+$/.test(seg.text)) {
|
|
144
|
+
const prev = folded[folded.length - 1];
|
|
145
|
+
const next = segments[k2 + 1];
|
|
146
|
+
if (prev && prev.type !== "equal" && next && next.type !== "equal") {
|
|
147
|
+
folded.push(
|
|
148
|
+
{ type: "delete", text: seg.text },
|
|
149
|
+
{ type: "insert", text: seg.text }
|
|
150
|
+
);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
folded.push({ ...seg });
|
|
155
|
+
}
|
|
156
|
+
const out = [];
|
|
157
|
+
let k = 0;
|
|
158
|
+
while (k < folded.length) {
|
|
159
|
+
if (folded[k].type === "equal") {
|
|
160
|
+
out.push(folded[k]);
|
|
161
|
+
k++;
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
let deleted = "";
|
|
165
|
+
let inserted = "";
|
|
166
|
+
while (k < folded.length && folded[k].type !== "equal") {
|
|
167
|
+
if (folded[k].type === "delete") deleted += folded[k].text;
|
|
168
|
+
else inserted += folded[k].text;
|
|
169
|
+
k++;
|
|
170
|
+
}
|
|
171
|
+
if (deleted) out.push({ type: "delete", text: deleted });
|
|
172
|
+
if (inserted) out.push({ type: "insert", text: inserted });
|
|
173
|
+
}
|
|
174
|
+
return mergeSegments(out);
|
|
175
|
+
}
|
|
176
|
+
function diffWords(oldText, newText) {
|
|
177
|
+
if (oldText === newText) {
|
|
178
|
+
return oldText ? [{ type: "equal", text: oldText }] : [];
|
|
179
|
+
}
|
|
180
|
+
const oldTokens = tokenizeWords(oldText);
|
|
181
|
+
const newTokens = tokenizeWords(newText);
|
|
182
|
+
if (oldTokens.length === 0) {
|
|
183
|
+
return mergeSegments([{ type: "insert", text: newText }]);
|
|
184
|
+
}
|
|
185
|
+
if (newTokens.length === 0) {
|
|
186
|
+
return mergeSegments([{ type: "delete", text: oldText }]);
|
|
187
|
+
}
|
|
188
|
+
if (oldTokens.length * newTokens.length > MAX_LCS_CELLS) {
|
|
189
|
+
return mergeSegments([
|
|
190
|
+
{ type: "delete", text: oldText },
|
|
191
|
+
{ type: "insert", text: newText }
|
|
192
|
+
]);
|
|
193
|
+
}
|
|
194
|
+
const n = oldTokens.length;
|
|
195
|
+
const m = newTokens.length;
|
|
196
|
+
const lcs = Array.from(
|
|
197
|
+
{ length: n + 1 },
|
|
198
|
+
() => new Int32Array(m + 1)
|
|
199
|
+
);
|
|
200
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
201
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
202
|
+
lcs[i2][j2] = oldTokens[i2] === newTokens[j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const segments = [];
|
|
206
|
+
let i = 0;
|
|
207
|
+
let j = 0;
|
|
208
|
+
while (i < n && j < m) {
|
|
209
|
+
if (oldTokens[i] === newTokens[j]) {
|
|
210
|
+
segments.push({ type: "equal", text: oldTokens[i] });
|
|
211
|
+
i++;
|
|
212
|
+
j++;
|
|
213
|
+
} else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
|
|
214
|
+
segments.push({ type: "delete", text: oldTokens[i] });
|
|
215
|
+
i++;
|
|
216
|
+
} else {
|
|
217
|
+
segments.push({ type: "insert", text: newTokens[j] });
|
|
218
|
+
j++;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
while (i < n) {
|
|
222
|
+
segments.push({ type: "delete", text: oldTokens[i] });
|
|
223
|
+
i++;
|
|
224
|
+
}
|
|
225
|
+
while (j < m) {
|
|
226
|
+
segments.push({ type: "insert", text: newTokens[j] });
|
|
227
|
+
j++;
|
|
228
|
+
}
|
|
229
|
+
return normalizeSegments(segments);
|
|
230
|
+
}
|
|
231
|
+
function stripMarkdown(text) {
|
|
232
|
+
return text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "$1").replace(
|
|
233
|
+
/(\*\*\*|___)([\s\S]*?)\1|(\*\*|__)([\s\S]*?)\3|(\*|_)([\s\S]*?)\5/g,
|
|
234
|
+
(_match, _d1, bi, _d2, b, _d3, i) => bi ?? b ?? i ?? ""
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// src/diff/document-diff.ts
|
|
239
|
+
var TEXT_COMPONENTS = /* @__PURE__ */ new Set(["paragraph", "heading"]);
|
|
240
|
+
function deepEqual(a, b) {
|
|
241
|
+
return JSON.stringify(a) === JSON.stringify(b);
|
|
242
|
+
}
|
|
243
|
+
function makeRevision(ctx, segments) {
|
|
244
|
+
return {
|
|
245
|
+
...ctx.author && { author: ctx.author },
|
|
246
|
+
...ctx.date && { date: ctx.date },
|
|
247
|
+
segments
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
function rawText(node) {
|
|
251
|
+
const text = node.props?.text;
|
|
252
|
+
return typeof text === "string" ? text.normalize("NFC") : "";
|
|
253
|
+
}
|
|
254
|
+
function plainText(node) {
|
|
255
|
+
return stripMarkdown(rawText(node));
|
|
256
|
+
}
|
|
257
|
+
var PLACEHOLDER_PATTERN = /\{[^}]+\}/;
|
|
258
|
+
function isEnabled(node) {
|
|
259
|
+
return node.enabled !== false;
|
|
260
|
+
}
|
|
261
|
+
function notePlaceholdersInChanges(segments, path, component, ctx) {
|
|
262
|
+
if (segments.some((s) => s.type !== "equal" && PLACEHOLDER_PATTERN.test(s.text))) {
|
|
263
|
+
ctx.summary.untracked.push({
|
|
264
|
+
path,
|
|
265
|
+
kind: "modified",
|
|
266
|
+
component,
|
|
267
|
+
detail: "placeholder (e.g. {DATE}) inside inserted/deleted text renders literally in the redline"
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function propsWithout(props, ...keys) {
|
|
272
|
+
const copy = { ...props || {} };
|
|
273
|
+
for (const key of keys) delete copy[key];
|
|
274
|
+
return copy;
|
|
275
|
+
}
|
|
276
|
+
var MAX_ALIGN_CELLS = 4e6;
|
|
277
|
+
function alignByLcs(oldItems, newItems, key) {
|
|
278
|
+
const oldKeys = oldItems.map(key);
|
|
279
|
+
const newKeys = newItems.map(key);
|
|
280
|
+
let start = 0;
|
|
281
|
+
while (start < oldItems.length && start < newItems.length && oldKeys[start] === newKeys[start]) {
|
|
282
|
+
start++;
|
|
283
|
+
}
|
|
284
|
+
let oldEnd = oldItems.length;
|
|
285
|
+
let newEnd = newItems.length;
|
|
286
|
+
while (oldEnd > start && newEnd > start && oldKeys[oldEnd - 1] === newKeys[newEnd - 1]) {
|
|
287
|
+
oldEnd--;
|
|
288
|
+
newEnd--;
|
|
289
|
+
}
|
|
290
|
+
const prefix = [];
|
|
291
|
+
for (let k = 0; k < start; k++) {
|
|
292
|
+
prefix.push({ op: "equal", oldItem: oldItems[k], newItem: newItems[k] });
|
|
293
|
+
}
|
|
294
|
+
const suffix = [];
|
|
295
|
+
for (let k = 0; k < oldItems.length - oldEnd; k++) {
|
|
296
|
+
suffix.push({
|
|
297
|
+
op: "equal",
|
|
298
|
+
oldItem: oldItems[oldEnd + k],
|
|
299
|
+
newItem: newItems[newEnd + k]
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
const n = oldEnd - start;
|
|
303
|
+
const m = newEnd - start;
|
|
304
|
+
const middle = [];
|
|
305
|
+
if (n * m > MAX_ALIGN_CELLS) {
|
|
306
|
+
for (let k = 0; k < n; k++) {
|
|
307
|
+
middle.push({ op: "delete", oldItem: oldItems[start + k] });
|
|
308
|
+
}
|
|
309
|
+
for (let k = 0; k < m; k++) {
|
|
310
|
+
middle.push({ op: "insert", newItem: newItems[start + k] });
|
|
311
|
+
}
|
|
312
|
+
return [...prefix, ...middle, ...suffix];
|
|
313
|
+
}
|
|
314
|
+
const lcs = Array.from(
|
|
315
|
+
{ length: n + 1 },
|
|
316
|
+
() => new Int32Array(m + 1)
|
|
317
|
+
);
|
|
318
|
+
for (let i2 = n - 1; i2 >= 0; i2--) {
|
|
319
|
+
for (let j2 = m - 1; j2 >= 0; j2--) {
|
|
320
|
+
lcs[i2][j2] = oldKeys[start + i2] === newKeys[start + j2] ? lcs[i2 + 1][j2 + 1] + 1 : Math.max(lcs[i2 + 1][j2], lcs[i2][j2 + 1]);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
let i = 0;
|
|
324
|
+
let j = 0;
|
|
325
|
+
while (i < n && j < m) {
|
|
326
|
+
if (oldKeys[start + i] === newKeys[start + j]) {
|
|
327
|
+
middle.push({
|
|
328
|
+
op: "equal",
|
|
329
|
+
oldItem: oldItems[start + i],
|
|
330
|
+
newItem: newItems[start + j]
|
|
331
|
+
});
|
|
332
|
+
i++;
|
|
333
|
+
j++;
|
|
334
|
+
} else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
|
|
335
|
+
middle.push({ op: "delete", oldItem: oldItems[start + i] });
|
|
336
|
+
i++;
|
|
337
|
+
} else {
|
|
338
|
+
middle.push({ op: "insert", newItem: newItems[start + j] });
|
|
339
|
+
j++;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
while (i < n) middle.push({ op: "delete", oldItem: oldItems[start + i++] });
|
|
343
|
+
while (j < m) middle.push({ op: "insert", newItem: newItems[start + j++] });
|
|
344
|
+
return [...prefix, ...middle, ...suffix];
|
|
345
|
+
}
|
|
346
|
+
function pairGap(deleted, inserted, pairable) {
|
|
347
|
+
const usedOld = new Array(deleted.length).fill(false);
|
|
348
|
+
const pairing = new Array(inserted.length).fill(-1);
|
|
349
|
+
let searchFrom = 0;
|
|
350
|
+
for (let j = 0; j < inserted.length; j++) {
|
|
351
|
+
for (let i = searchFrom; i < deleted.length; i++) {
|
|
352
|
+
if (!usedOld[i] && pairable(deleted[i], inserted[j])) {
|
|
353
|
+
usedOld[i] = true;
|
|
354
|
+
pairing[j] = i;
|
|
355
|
+
searchFrom = i + 1;
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const plan = [];
|
|
361
|
+
const pairs = [];
|
|
362
|
+
let emittedOld = 0;
|
|
363
|
+
for (let j = 0; j < inserted.length; j++) {
|
|
364
|
+
const i = pairing[j];
|
|
365
|
+
if (i >= 0) {
|
|
366
|
+
while (emittedOld < i) {
|
|
367
|
+
if (!usedOld[emittedOld]) {
|
|
368
|
+
plan.push({ kind: "deleted", oldItem: deleted[emittedOld] });
|
|
369
|
+
}
|
|
370
|
+
emittedOld++;
|
|
371
|
+
}
|
|
372
|
+
emittedOld = i + 1;
|
|
373
|
+
plan.push({ kind: "paired", oldItem: deleted[i], newItem: inserted[j] });
|
|
374
|
+
pairs.push({ oldItem: deleted[i], newItem: inserted[j] });
|
|
375
|
+
} else {
|
|
376
|
+
plan.push({ kind: "inserted", newItem: inserted[j] });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
for (let i = emittedOld; i < deleted.length; i++) {
|
|
380
|
+
if (!usedOld[i]) plan.push({ kind: "deleted", oldItem: deleted[i] });
|
|
381
|
+
}
|
|
382
|
+
return { pairs, plan };
|
|
383
|
+
}
|
|
384
|
+
function diffTextComponent(oldNode, newNode, path, ctx) {
|
|
385
|
+
const oldText = plainText(oldNode);
|
|
386
|
+
const newText = plainText(newNode);
|
|
387
|
+
const propsChanged = !deepEqual(
|
|
388
|
+
propsWithout(oldNode.props, "text", "revision"),
|
|
389
|
+
propsWithout(newNode.props, "text", "revision")
|
|
390
|
+
);
|
|
391
|
+
if (propsChanged) {
|
|
392
|
+
ctx.summary.untracked.push({
|
|
393
|
+
path,
|
|
394
|
+
kind: "modified",
|
|
395
|
+
component: newNode.name,
|
|
396
|
+
detail: "formatting/props changed (not expressible as a tracked change); new version rendered"
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
if (oldText === newText) {
|
|
400
|
+
if (rawText(oldNode) !== rawText(newNode)) {
|
|
401
|
+
ctx.summary.untracked.push({
|
|
402
|
+
path,
|
|
403
|
+
kind: "modified",
|
|
404
|
+
component: newNode.name,
|
|
405
|
+
detail: "inline formatting or link target changed (markdown-only); new version rendered without a tracked change"
|
|
406
|
+
});
|
|
407
|
+
} else if (!propsChanged) {
|
|
408
|
+
ctx.summary.unchangedBlocks++;
|
|
409
|
+
}
|
|
410
|
+
return newNode;
|
|
411
|
+
}
|
|
412
|
+
ctx.summary.tracked.modified++;
|
|
413
|
+
const segments = diffWords(oldText, newText);
|
|
414
|
+
notePlaceholdersInChanges(segments, path, newNode.name, ctx);
|
|
415
|
+
if (rawText(oldNode) !== oldText || rawText(newNode) !== newText) {
|
|
416
|
+
ctx.summary.untracked.push({
|
|
417
|
+
path,
|
|
418
|
+
kind: "modified",
|
|
419
|
+
component: newNode.name,
|
|
420
|
+
detail: "inline formatting or links flattened to plain text in the redline (revision segments render literally)"
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
return {
|
|
424
|
+
...newNode,
|
|
425
|
+
props: {
|
|
426
|
+
...newNode.props,
|
|
427
|
+
text: newText,
|
|
428
|
+
revision: makeRevision(ctx, segments)
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function insertedTextComponent(node, path, ctx) {
|
|
433
|
+
ctx.summary.tracked.inserted++;
|
|
434
|
+
const text = plainText(node);
|
|
435
|
+
const segments = [{ type: "insert", text }];
|
|
436
|
+
notePlaceholdersInChanges(segments, path, node.name, ctx);
|
|
437
|
+
return {
|
|
438
|
+
...node,
|
|
439
|
+
props: {
|
|
440
|
+
...node.props,
|
|
441
|
+
text,
|
|
442
|
+
revision: makeRevision(ctx, segments)
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
function deletedTextComponent(node, path, ctx) {
|
|
447
|
+
ctx.summary.tracked.deleted++;
|
|
448
|
+
const text = plainText(node);
|
|
449
|
+
const segments = [{ type: "delete", text }];
|
|
450
|
+
notePlaceholdersInChanges(segments, path, node.name, ctx);
|
|
451
|
+
return {
|
|
452
|
+
...node,
|
|
453
|
+
props: {
|
|
454
|
+
...node.props,
|
|
455
|
+
text: "",
|
|
456
|
+
revision: makeRevision(ctx, segments)
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
function normalizeListItems(node) {
|
|
461
|
+
const items = node.props?.items || [];
|
|
462
|
+
return items.map((item) => {
|
|
463
|
+
const raw = typeof item === "string" ? item : item.text;
|
|
464
|
+
const level = typeof item === "string" ? 0 : item.level || 0;
|
|
465
|
+
return { raw, text: stripMarkdown(raw.normalize("NFC")), level };
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
function diffListComponent(oldNode, newNode, path, ctx) {
|
|
469
|
+
const propsChanged = !deepEqual(
|
|
470
|
+
propsWithout(oldNode.props, "items"),
|
|
471
|
+
propsWithout(newNode.props, "items")
|
|
472
|
+
);
|
|
473
|
+
if (propsChanged) {
|
|
474
|
+
ctx.summary.untracked.push({
|
|
475
|
+
path,
|
|
476
|
+
kind: "modified",
|
|
477
|
+
component: "list",
|
|
478
|
+
detail: "list configuration changed (format/levels/spacing); new version rendered"
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
const oldItems = normalizeListItems(oldNode);
|
|
482
|
+
const newItems = normalizeListItems(newNode);
|
|
483
|
+
const stripped = (items) => items.map((i) => ({ text: i.text, level: i.level }));
|
|
484
|
+
if (deepEqual(stripped(oldItems), stripped(newItems))) {
|
|
485
|
+
if (!deepEqual(
|
|
486
|
+
oldItems.map((i) => i.raw),
|
|
487
|
+
newItems.map((i) => i.raw)
|
|
488
|
+
)) {
|
|
489
|
+
ctx.summary.untracked.push({
|
|
490
|
+
path,
|
|
491
|
+
kind: "modified",
|
|
492
|
+
component: "list",
|
|
493
|
+
detail: "inline formatting or link target changed in list items (markdown-only); new version rendered without a tracked change"
|
|
494
|
+
});
|
|
495
|
+
} else if (!propsChanged) {
|
|
496
|
+
ctx.summary.unchangedBlocks++;
|
|
497
|
+
}
|
|
498
|
+
return newNode;
|
|
499
|
+
}
|
|
500
|
+
const ops = alignByLcs(
|
|
501
|
+
oldItems,
|
|
502
|
+
newItems,
|
|
503
|
+
(item) => `${item.level}:${item.text}`
|
|
504
|
+
);
|
|
505
|
+
const outItems = [];
|
|
506
|
+
let changed = false;
|
|
507
|
+
let k = 0;
|
|
508
|
+
while (k < ops.length) {
|
|
509
|
+
const op = ops[k];
|
|
510
|
+
if (op.op === "equal") {
|
|
511
|
+
outItems.push({ text: op.newItem.raw, level: op.newItem.level });
|
|
512
|
+
k++;
|
|
513
|
+
continue;
|
|
514
|
+
}
|
|
515
|
+
const deleted = [];
|
|
516
|
+
const inserted = [];
|
|
517
|
+
while (k < ops.length && ops[k].op !== "equal") {
|
|
518
|
+
const gapOp = ops[k];
|
|
519
|
+
if (gapOp.op === "delete") deleted.push(gapOp.oldItem);
|
|
520
|
+
else if (gapOp.op === "insert") inserted.push(gapOp.newItem);
|
|
521
|
+
k++;
|
|
522
|
+
}
|
|
523
|
+
const { plan } = pairGap(
|
|
524
|
+
deleted,
|
|
525
|
+
inserted,
|
|
526
|
+
(oldItem, newItem) => oldItem.level === newItem.level
|
|
527
|
+
);
|
|
528
|
+
for (const step of plan) {
|
|
529
|
+
changed = true;
|
|
530
|
+
if (step.kind === "paired") {
|
|
531
|
+
const segments = diffWords(step.oldItem.text, step.newItem.text);
|
|
532
|
+
notePlaceholdersInChanges(segments, path, "list", ctx);
|
|
533
|
+
outItems.push({
|
|
534
|
+
text: step.newItem.text,
|
|
535
|
+
level: step.newItem.level,
|
|
536
|
+
revision: makeRevision(ctx, segments)
|
|
537
|
+
});
|
|
538
|
+
} else if (step.kind === "inserted") {
|
|
539
|
+
outItems.push({
|
|
540
|
+
text: step.newItem.text,
|
|
541
|
+
level: step.newItem.level,
|
|
542
|
+
revision: makeRevision(ctx, [
|
|
543
|
+
{ type: "insert", text: step.newItem.text }
|
|
544
|
+
])
|
|
545
|
+
});
|
|
546
|
+
} else {
|
|
547
|
+
outItems.push({
|
|
548
|
+
text: "",
|
|
549
|
+
level: step.oldItem.level,
|
|
550
|
+
revision: makeRevision(ctx, [
|
|
551
|
+
{ type: "delete", text: step.oldItem.text }
|
|
552
|
+
])
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
if (changed) ctx.summary.tracked.modified++;
|
|
558
|
+
return {
|
|
559
|
+
...newNode,
|
|
560
|
+
props: { ...newNode.props, items: outItems }
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function listWithAllItems(node, type, ctx) {
|
|
564
|
+
if (type === "insert") ctx.summary.tracked.inserted++;
|
|
565
|
+
else ctx.summary.tracked.deleted++;
|
|
566
|
+
const items = normalizeListItems(node).map((item) => ({
|
|
567
|
+
text: type === "insert" ? item.text : "",
|
|
568
|
+
level: item.level,
|
|
569
|
+
revision: makeRevision(ctx, [{ type, text: item.text }])
|
|
570
|
+
}));
|
|
571
|
+
return { ...node, props: { ...node.props, items } };
|
|
572
|
+
}
|
|
573
|
+
function isContainer(node) {
|
|
574
|
+
return Array.isArray(node.children);
|
|
575
|
+
}
|
|
576
|
+
function nodesPairable(oldNode, newNode) {
|
|
577
|
+
return oldNode.name === newNode.name;
|
|
578
|
+
}
|
|
579
|
+
function diffPairedNode(oldNode, newNode, path, ctx) {
|
|
580
|
+
const oldEnabled = isEnabled(oldNode);
|
|
581
|
+
const newEnabled = isEnabled(newNode);
|
|
582
|
+
if (!oldEnabled && !newEnabled) {
|
|
583
|
+
ctx.summary.unchangedBlocks++;
|
|
584
|
+
return newNode;
|
|
585
|
+
}
|
|
586
|
+
if (!oldEnabled && newEnabled) {
|
|
587
|
+
return emitInserted(newNode, path, ctx);
|
|
588
|
+
}
|
|
589
|
+
if (oldEnabled && !newEnabled) {
|
|
590
|
+
const deletedNode = emitDeleted(oldNode, path, ctx);
|
|
591
|
+
return deletedNode ?? newNode;
|
|
592
|
+
}
|
|
593
|
+
if (TEXT_COMPONENTS.has(newNode.name)) {
|
|
594
|
+
return diffTextComponent(oldNode, newNode, path, ctx);
|
|
595
|
+
}
|
|
596
|
+
if (newNode.name === "list") {
|
|
597
|
+
return diffListComponent(oldNode, newNode, path, ctx);
|
|
598
|
+
}
|
|
599
|
+
if (isContainer(newNode) || isContainer(oldNode)) {
|
|
600
|
+
const propsChanged = !deepEqual(oldNode.props, newNode.props);
|
|
601
|
+
if (propsChanged) {
|
|
602
|
+
ctx.summary.untracked.push({
|
|
603
|
+
path,
|
|
604
|
+
kind: "modified",
|
|
605
|
+
component: newNode.name,
|
|
606
|
+
detail: "container props changed; new version rendered"
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
...newNode,
|
|
611
|
+
children: diffChildren(
|
|
612
|
+
oldNode.children || [],
|
|
613
|
+
newNode.children || [],
|
|
614
|
+
`${path}/children`,
|
|
615
|
+
ctx
|
|
616
|
+
)
|
|
617
|
+
};
|
|
618
|
+
}
|
|
619
|
+
ctx.summary.untracked.push({
|
|
620
|
+
path,
|
|
621
|
+
kind: "modified",
|
|
622
|
+
component: newNode.name,
|
|
623
|
+
detail: `"${newNode.name}" changed (no native tracked-change support); new version rendered`
|
|
624
|
+
});
|
|
625
|
+
return newNode;
|
|
626
|
+
}
|
|
627
|
+
function emitInserted(node, path, ctx) {
|
|
628
|
+
if (!isEnabled(node)) {
|
|
629
|
+
return node;
|
|
630
|
+
}
|
|
631
|
+
if (TEXT_COMPONENTS.has(node.name)) {
|
|
632
|
+
return insertedTextComponent(node, path, ctx);
|
|
633
|
+
}
|
|
634
|
+
if (node.name === "list") {
|
|
635
|
+
return listWithAllItems(node, "insert", ctx);
|
|
636
|
+
}
|
|
637
|
+
if (isContainer(node)) {
|
|
638
|
+
if (typeof node.props?.title === "string") {
|
|
639
|
+
ctx.summary.untracked.push({
|
|
640
|
+
path,
|
|
641
|
+
kind: "inserted",
|
|
642
|
+
component: node.name,
|
|
643
|
+
detail: `"${node.name}" title rendered without insertion mark (titles are props, not text blocks)`
|
|
644
|
+
});
|
|
645
|
+
}
|
|
646
|
+
return {
|
|
647
|
+
...node,
|
|
648
|
+
children: diffChildren([], node.children || [], `${path}/children`, ctx)
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
ctx.summary.untracked.push({
|
|
652
|
+
path,
|
|
653
|
+
kind: "inserted",
|
|
654
|
+
component: node.name,
|
|
655
|
+
detail: `"${node.name}" added (rendered, but not marked as a tracked insertion)`
|
|
656
|
+
});
|
|
657
|
+
return node;
|
|
658
|
+
}
|
|
659
|
+
function emitDeleted(node, path, ctx) {
|
|
660
|
+
if (!isEnabled(node)) {
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
if (TEXT_COMPONENTS.has(node.name)) {
|
|
664
|
+
return deletedTextComponent(node, path, ctx);
|
|
665
|
+
}
|
|
666
|
+
if (node.name === "list") {
|
|
667
|
+
return listWithAllItems(node, "delete", ctx);
|
|
668
|
+
}
|
|
669
|
+
if (isContainer(node)) {
|
|
670
|
+
if (typeof node.props?.title === "string") {
|
|
671
|
+
ctx.summary.untracked.push({
|
|
672
|
+
path,
|
|
673
|
+
kind: "deleted",
|
|
674
|
+
component: node.name,
|
|
675
|
+
detail: `"${node.name}" title still rendered without deletion mark (titles are props, not text blocks)`
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
const children = diffChildren(
|
|
679
|
+
node.children || [],
|
|
680
|
+
[],
|
|
681
|
+
`${path}/children`,
|
|
682
|
+
ctx
|
|
683
|
+
);
|
|
684
|
+
return { ...node, children };
|
|
685
|
+
}
|
|
686
|
+
ctx.summary.untracked.push({
|
|
687
|
+
path,
|
|
688
|
+
kind: "deleted",
|
|
689
|
+
component: node.name,
|
|
690
|
+
detail: `"${node.name}" removed (dropped from the redline; Word cannot mark it as a tracked deletion)`
|
|
691
|
+
});
|
|
692
|
+
return null;
|
|
693
|
+
}
|
|
694
|
+
function diffChildren(oldChildren, newChildren, path, ctx) {
|
|
695
|
+
const ops = alignByLcs(
|
|
696
|
+
oldChildren,
|
|
697
|
+
newChildren,
|
|
698
|
+
(node) => JSON.stringify(node)
|
|
699
|
+
);
|
|
700
|
+
const out = [];
|
|
701
|
+
let k = 0;
|
|
702
|
+
let newIndex = 0;
|
|
703
|
+
while (k < ops.length) {
|
|
704
|
+
const op = ops[k];
|
|
705
|
+
if (op.op === "equal") {
|
|
706
|
+
ctx.summary.unchangedBlocks++;
|
|
707
|
+
out.push(op.newItem);
|
|
708
|
+
k++;
|
|
709
|
+
newIndex++;
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
const deleted = [];
|
|
713
|
+
const inserted = [];
|
|
714
|
+
while (k < ops.length && ops[k].op !== "equal") {
|
|
715
|
+
const gapOp = ops[k];
|
|
716
|
+
if (gapOp.op === "delete") deleted.push(gapOp.oldItem);
|
|
717
|
+
else if (gapOp.op === "insert") inserted.push(gapOp.newItem);
|
|
718
|
+
k++;
|
|
719
|
+
}
|
|
720
|
+
const { plan } = pairGap(deleted, inserted, nodesPairable);
|
|
721
|
+
for (const step of plan) {
|
|
722
|
+
if (step.kind === "paired") {
|
|
723
|
+
out.push(
|
|
724
|
+
diffPairedNode(step.oldItem, step.newItem, `${path}/${newIndex}`, ctx)
|
|
725
|
+
);
|
|
726
|
+
newIndex++;
|
|
727
|
+
} else if (step.kind === "inserted") {
|
|
728
|
+
out.push(emitInserted(step.newItem, `${path}/${newIndex}`, ctx));
|
|
729
|
+
newIndex++;
|
|
730
|
+
} else {
|
|
731
|
+
const deletedNode = emitDeleted(
|
|
732
|
+
step.oldItem,
|
|
733
|
+
`${path}/${newIndex}`,
|
|
734
|
+
ctx
|
|
735
|
+
);
|
|
736
|
+
if (deletedNode) out.push(deletedNode);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return out;
|
|
741
|
+
}
|
|
742
|
+
function diffDocuments(oldDoc, newDoc, options = {}) {
|
|
743
|
+
if (!oldDoc || oldDoc.name !== "docx") {
|
|
744
|
+
throw new Error('Old document: top-level component must be "docx"');
|
|
745
|
+
}
|
|
746
|
+
if (!newDoc || newDoc.name !== "docx") {
|
|
747
|
+
throw new Error('New document: top-level component must be "docx"');
|
|
748
|
+
}
|
|
749
|
+
const summary = {
|
|
750
|
+
tracked: { modified: 0, inserted: 0, deleted: 0 },
|
|
751
|
+
untracked: [],
|
|
752
|
+
unchangedBlocks: 0,
|
|
753
|
+
notes: []
|
|
754
|
+
};
|
|
755
|
+
const ctx = {
|
|
756
|
+
author: options.author,
|
|
757
|
+
date: options.date,
|
|
758
|
+
summary
|
|
759
|
+
};
|
|
760
|
+
const rootPropsChanged = !deepEqual(
|
|
761
|
+
propsWithout(oldDoc.props, "trackRevisions"),
|
|
762
|
+
propsWithout(newDoc.props, "trackRevisions")
|
|
763
|
+
);
|
|
764
|
+
if (rootPropsChanged) {
|
|
765
|
+
summary.untracked.push({
|
|
766
|
+
path: "/props",
|
|
767
|
+
kind: "modified",
|
|
768
|
+
component: "docx",
|
|
769
|
+
detail: "document props changed (theme/metadata/defaults); new version used"
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
const children = diffChildren(
|
|
773
|
+
oldDoc.children || [],
|
|
774
|
+
newDoc.children || [],
|
|
775
|
+
"/children",
|
|
776
|
+
ctx
|
|
777
|
+
);
|
|
778
|
+
if (summary.tracked.deleted > 0) {
|
|
779
|
+
summary.notes.push(
|
|
780
|
+
`${summary.tracked.deleted} fully deleted block(s): accepting all changes leaves an empty paragraph behind (OOXML paragraph-mark deletion is not supported by the renderer)`
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
const document = {
|
|
784
|
+
...newDoc,
|
|
785
|
+
props: { ...newDoc.props, trackRevisions: true },
|
|
786
|
+
children
|
|
787
|
+
};
|
|
788
|
+
return { document, summary };
|
|
789
|
+
}
|
|
790
|
+
|
|
118
791
|
// src/index.ts
|
|
119
792
|
import {
|
|
120
793
|
fixSchemaReferences as fixSchemaReferences2,
|
|
@@ -980,6 +1653,8 @@ export {
|
|
|
980
1653
|
NumberingSchema,
|
|
981
1654
|
ParagraphPropsSchema,
|
|
982
1655
|
ReportPropsSchema,
|
|
1656
|
+
RevisionSchema,
|
|
1657
|
+
RevisionSegmentSchema,
|
|
983
1658
|
SHARED_DOCX_VERSION,
|
|
984
1659
|
STANDARD_COMPONENTS,
|
|
985
1660
|
STANDARD_COMPONENTS_SET,
|
|
@@ -1014,6 +1689,8 @@ export {
|
|
|
1014
1689
|
createValidatedComponent,
|
|
1015
1690
|
createValidator,
|
|
1016
1691
|
deepValidateDocument,
|
|
1692
|
+
diffDocuments,
|
|
1693
|
+
diffWords,
|
|
1017
1694
|
documentValidator,
|
|
1018
1695
|
exportSchemaToFile as exportDocxSchemaToFile,
|
|
1019
1696
|
exportSchemaToFile2 as exportSchemaToFile,
|
|
@@ -1082,6 +1759,7 @@ export {
|
|
|
1082
1759
|
strictComponentValidator,
|
|
1083
1760
|
strictDocumentValidator,
|
|
1084
1761
|
strictThemeValidator,
|
|
1762
|
+
stripMarkdown,
|
|
1085
1763
|
themeValidator,
|
|
1086
1764
|
transformAndValidate,
|
|
1087
1765
|
transformValueError as transformDocxValueError,
|