@json-to-office/shared-docx 0.13.1 → 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.
Files changed (40) hide show
  1. package/dist/{chunk-ZQNAWKEI.js → chunk-52PV5JQK.js} +2 -2
  2. package/dist/chunk-52PV5JQK.js.map +1 -0
  3. package/dist/{chunk-UTOSZI5Q.js → chunk-5BKZFYJL.js} +3 -3
  4. package/dist/{chunk-CIBMJ7QA.js → chunk-6IXK54YE.js} +2 -2
  5. package/dist/{chunk-YUV5CJMG.js → chunk-CGMJ665Z.js} +488 -436
  6. package/dist/chunk-CGMJ665Z.js.map +1 -0
  7. package/dist/{chunk-BFJJ5HYD.js → chunk-CY6IVMEZ.js} +7 -2
  8. package/dist/chunk-CY6IVMEZ.js.map +1 -0
  9. package/dist/{chunk-VBZC5WT6.js → chunk-FA6FKO3L.js} +2 -2
  10. package/dist/{chunk-TPA2QVQD.js → chunk-HJIS6RRQ.js} +6 -6
  11. package/dist/{chunk-25SAKHVN.js → chunk-PQGT7BU5.js} +2 -2
  12. package/dist/{chunk-IHZIO72A.js → chunk-QUA2GDH7.js} +2 -2
  13. package/dist/{chunk-YK2UUG65.js → chunk-X2B5OVWA.js} +2 -2
  14. package/dist/index.d.ts +144 -19
  15. package/dist/index.js +688 -10
  16. package/dist/index.js.map +1 -1
  17. package/dist/schemas/api.js +4 -4
  18. package/dist/schemas/component-defaults.d.ts +48 -32
  19. package/dist/schemas/component-defaults.js +1 -1
  20. package/dist/schemas/component-registry.js +2 -2
  21. package/dist/schemas/components.d.ts +90 -33
  22. package/dist/schemas/components.js +7 -3
  23. package/dist/schemas/document.js +5 -5
  24. package/dist/schemas/export.js +3 -3
  25. package/dist/schemas/generator.js +3 -3
  26. package/dist/schemas/theme.d.ts +24 -16
  27. package/dist/schemas/theme.js +2 -2
  28. package/dist/validation/unified/index.d.ts +51 -18
  29. package/dist/validation/unified/index.js +6 -6
  30. package/package.json +2 -2
  31. package/dist/chunk-BFJJ5HYD.js.map +0 -1
  32. package/dist/chunk-YUV5CJMG.js.map +0 -1
  33. package/dist/chunk-ZQNAWKEI.js.map +0 -1
  34. /package/dist/{chunk-UTOSZI5Q.js.map → chunk-5BKZFYJL.js.map} +0 -0
  35. /package/dist/{chunk-CIBMJ7QA.js.map → chunk-6IXK54YE.js.map} +0 -0
  36. /package/dist/{chunk-VBZC5WT6.js.map → chunk-FA6FKO3L.js.map} +0 -0
  37. /package/dist/{chunk-TPA2QVQD.js.map → chunk-HJIS6RRQ.js.map} +0 -0
  38. /package/dist/{chunk-25SAKHVN.js.map → chunk-PQGT7BU5.js.map} +0 -0
  39. /package/dist/{chunk-IHZIO72A.js.map → chunk-QUA2GDH7.js.map} +0 -0
  40. /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-25SAKHVN.js";
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-TPA2QVQD.js";
35
+ } from "./chunk-HJIS6RRQ.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-QUA2GDH7.js";
46
46
  import {
47
47
  JSON_SCHEMA_URLS,
48
48
  JsonComponentDefinitionSchema
49
- } from "./chunk-VBZC5WT6.js";
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-UTOSZI5Q.js";
71
+ } from "./chunk-5BKZFYJL.js";
72
72
  import {
73
73
  ComponentDefinitionSchema,
74
74
  StandardComponentDefinitionSchema
75
- } from "./chunk-ZQNAWKEI.js";
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-CIBMJ7QA.js";
89
+ } from "./chunk-6IXK54YE.js";
90
90
  import {
91
91
  HighchartsPropsSchema,
92
92
  ReportPropsSchema,
93
93
  TextBoxPropsSchema,
94
94
  TocPropsSchema
95
- } from "./chunk-BFJJ5HYD.js";
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-YUV5CJMG.js";
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,