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