@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.
Files changed (41) hide show
  1. package/dist/{chunk-CIBMJ7QA.js → chunk-5C7XT6LW.js} +6 -2
  2. package/dist/{chunk-CIBMJ7QA.js.map → chunk-5C7XT6LW.js.map} +1 -1
  3. package/dist/{chunk-YUV5CJMG.js → chunk-CGMJ665Z.js} +488 -436
  4. package/dist/chunk-CGMJ665Z.js.map +1 -0
  5. package/dist/{chunk-ZQNAWKEI.js → chunk-M6LVAHZB.js} +2 -2
  6. package/dist/chunk-M6LVAHZB.js.map +1 -0
  7. package/dist/{chunk-TPA2QVQD.js → chunk-NZKLNPBZ.js} +6 -6
  8. package/dist/{chunk-UTOSZI5Q.js → chunk-P4CGTP5W.js} +4 -4
  9. package/dist/chunk-P4CGTP5W.js.map +1 -0
  10. package/dist/{chunk-IHZIO72A.js → chunk-PVQV2RS4.js} +2 -2
  11. package/dist/{chunk-VBZC5WT6.js → chunk-SMIG3LSE.js} +2 -2
  12. package/dist/{chunk-YK2UUG65.js → chunk-X2B5OVWA.js} +2 -2
  13. package/dist/{chunk-25SAKHVN.js → chunk-YRRBEJPB.js} +2 -2
  14. package/dist/{chunk-BFJJ5HYD.js → chunk-YZYUY2VH.js} +153 -11
  15. package/dist/chunk-YZYUY2VH.js.map +1 -0
  16. package/dist/index.d.ts +158 -21
  17. package/dist/index.js +701 -12
  18. package/dist/index.js.map +1 -1
  19. package/dist/schemas/api.js +4 -4
  20. package/dist/schemas/component-defaults.d.ts +48 -32
  21. package/dist/schemas/component-defaults.js +1 -1
  22. package/dist/schemas/component-registry.js +2 -2
  23. package/dist/schemas/components.d.ts +178 -33
  24. package/dist/schemas/components.js +13 -3
  25. package/dist/schemas/document.js +5 -5
  26. package/dist/schemas/export.js +3 -3
  27. package/dist/schemas/generator.js +3 -3
  28. package/dist/schemas/theme.d.ts +24 -16
  29. package/dist/schemas/theme.js +2 -2
  30. package/dist/validation/unified/index.d.ts +51 -18
  31. package/dist/validation/unified/index.js +6 -6
  32. package/package.json +3 -2
  33. package/dist/chunk-BFJJ5HYD.js.map +0 -1
  34. package/dist/chunk-UTOSZI5Q.js.map +0 -1
  35. package/dist/chunk-YUV5CJMG.js.map +0 -1
  36. package/dist/chunk-ZQNAWKEI.js.map +0 -1
  37. /package/dist/{chunk-TPA2QVQD.js.map → chunk-NZKLNPBZ.js.map} +0 -0
  38. /package/dist/{chunk-IHZIO72A.js.map → chunk-PVQV2RS4.js.map} +0 -0
  39. /package/dist/{chunk-VBZC5WT6.js.map → chunk-SMIG3LSE.js.map} +0 -0
  40. /package/dist/{chunk-YK2UUG65.js.map → chunk-X2B5OVWA.js.map} +0 -0
  41. /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-25SAKHVN.js";
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-TPA2QVQD.js";
35
+ } from "./chunk-NZKLNPBZ.js";
36
36
  import {
37
37
  ThemeConfigSchema,
38
38
  isValidThemeConfig
39
- } from "./chunk-YK2UUG65.js";
39
+ } from "./chunk-X2B5OVWA.js";
40
40
  import {
41
41
  GenerateDocumentRequestSchema,
42
42
  GenerateDocumentResponseSchema,
43
43
  ValidateDocumentRequestSchema,
44
44
  ValidateDocumentResponseSchema
45
- } from "./chunk-IHZIO72A.js";
45
+ } from "./chunk-PVQV2RS4.js";
46
46
  import {
47
47
  JSON_SCHEMA_URLS,
48
48
  JsonComponentDefinitionSchema
49
- } from "./chunk-VBZC5WT6.js";
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-UTOSZI5Q.js";
71
+ } from "./chunk-P4CGTP5W.js";
72
72
  import {
73
73
  ComponentDefinitionSchema,
74
74
  StandardComponentDefinitionSchema
75
- } from "./chunk-ZQNAWKEI.js";
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-CIBMJ7QA.js";
89
+ } from "./chunk-5C7XT6LW.js";
90
90
  import {
91
91
  HighchartsPropsSchema,
92
92
  ReportPropsSchema,
93
93
  TextBoxPropsSchema,
94
- TocPropsSchema
95
- } from "./chunk-BFJJ5HYD.js";
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-YUV5CJMG.js";
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,