@ansonlai/docx-redline-js 0.8.3 → 0.9.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 (71) hide show
  1. package/ARCHITECTURE.md +221 -12
  2. package/CHANGELOG.md +803 -586
  3. package/README.md +1018 -901
  4. package/core/current-properties.js +136 -0
  5. package/core/paragraph-text.js +211 -209
  6. package/core/types.js +9 -3
  7. package/core/word-revision-semantics.js +256 -0
  8. package/dist/docx-redline-js.esm.js +4551 -967
  9. package/dist/docx-redline-js.esm.js.map +4 -4
  10. package/dist/docx-redline-js.esm.min.js +112 -106
  11. package/dist/docx-redline-js.esm.min.js.map +4 -4
  12. package/dist/docx-redline.bundle.cjs +4690 -1106
  13. package/dist/docx-redline.bundle.cjs.map +4 -4
  14. package/dist/docx-redline.bundle.js +4690 -1106
  15. package/dist/docx-redline.bundle.js.map +4 -4
  16. package/docs/AGENT_FAST_START.md +2 -0
  17. package/docs/AGENT_KNOWLEDGE_BASE.md +23 -4
  18. package/docs/SKILL_AUTHORING.md +165 -150
  19. package/docs/TESTING.md +1142 -1059
  20. package/docs/schemas/document-operations.schema.json +221 -168
  21. package/document/docx-document.js +1022 -1010
  22. package/engine/oxml-engine.js +567 -560
  23. package/engine/paragraph-mark-revisions.js +292 -0
  24. package/engine/reconstruction-mapper.js +312 -311
  25. package/engine/reconstruction-mode.js +183 -183
  26. package/engine/reconstruction-writer.js +67 -10
  27. package/engine/rpr-helpers.js +42 -0
  28. package/engine/run-builders.js +0 -73
  29. package/engine/surgical-diff-application.js +604 -605
  30. package/engine/table-mode.js +63 -63
  31. package/index.d.ts +51 -1
  32. package/index.js +47 -3
  33. package/node/cli-help.js +1 -0
  34. package/node/cli.js +992 -974
  35. package/orchestration/list-structural-fallback.js +67 -9
  36. package/package.json +1 -1
  37. package/pipeline/diff-engine.js +130 -1
  38. package/pipeline/ingestion-export.js +227 -243
  39. package/pipeline/ingestion-paragraph.js +93 -98
  40. package/pipeline/list-generation.js +415 -419
  41. package/pipeline/pipeline.js +206 -204
  42. package/pipeline/serialization.js +111 -94
  43. package/scripts/export-validation-fixtures.mjs +19 -6
  44. package/scripts/generate-word-revision-semantics-matrix.mjs +134 -0
  45. package/scripts/lib/word-coverage-catalogue.mjs +7 -2
  46. package/scripts/render-multilevel-bullet-visual.ps1 +2 -2
  47. package/scripts/word-com-corpus-visual-suite.ps1 +2 -2
  48. package/scripts/word-com-differential.ps1 +443 -335
  49. package/scripts/word-com-visual-suite.ps1 +2 -2
  50. package/scripts/word-revision-semantics-matrix.ps1 +108 -0
  51. package/services/batch-operation-orchestrator.js +26 -0
  52. package/services/capture-engine.js +228 -226
  53. package/services/document-operation-applier.js +215 -0
  54. package/services/document-operation-contract.js +628 -537
  55. package/services/document-operation-mutations.js +789 -46
  56. package/services/error-recovery.js +17 -0
  57. package/services/list-conversion-diagnostics.js +296 -0
  58. package/services/list-format-change.js +586 -0
  59. package/services/list-format-mutation.js +416 -0
  60. package/services/list-format.js +305 -0
  61. package/services/numbering-allocation.js +642 -0
  62. package/services/numbering-helpers.js +354 -524
  63. package/services/operation-batch-compiler.js +612 -436
  64. package/services/operation-heuristics.js +197 -173
  65. package/services/operation-preflight.js +1 -0
  66. package/services/receipt-collector.js +288 -288
  67. package/services/revision-comment-management.js +634 -594
  68. package/services/revision-fidelity-verifier.js +498 -0
  69. package/services/standalone-docx-plumbing.js +627 -631
  70. package/services/standalone-operation-runner.d.ts +606 -427
  71. package/services/table-reconciliation.js +103 -49
@@ -7,8 +7,9 @@
7
7
  import { computeWordLevelDiffOps } from '../pipeline/diff-engine.js';
8
8
  import { splitRunsAtDiffBoundaries, applyPatches } from '../pipeline/patching.js';
9
9
  import { serializeToOoxml } from '../pipeline/serialization.js';
10
- import { createRevisionMetadata, escapeXml, RunKind } from '../core/types.js';
11
- import { preprocessMarkdown } from '../pipeline/markdown-processor.js';
10
+ import { createRevisionMetadata, escapeXml, RunKind } from '../core/types.js';
11
+ import { preprocessMarkdown } from '../pipeline/markdown-processor.js';
12
+ import { paragraphMarkPPrXml } from '../engine/paragraph-mark-revisions.js';
12
13
 
13
14
  /**
14
15
  * Generates a new w:tbl OOXML structure from Markdown table data.
@@ -18,14 +19,16 @@ import { preprocessMarkdown } from '../pipeline/markdown-processor.js';
18
19
  * @param {Object} options - { generateRedlines, author }
19
20
  * @returns {string} Complete w:tbl OOXML
20
21
  */
21
- export function generateTableOoxml(tableData, options = {}) {
22
- const {
23
- generateRedlines = false,
24
- author = 'AI',
25
- revisionIdAllocator = null,
26
- trackAsBlock = false
27
- } = options;
28
- const tableInsertMeta = generateRedlines && trackAsBlock ? createRevisionMetadata(author, revisionIdAllocator) : null;
22
+ export function generateTableOoxml(tableData, options = {}) {
23
+ const {
24
+ generateRedlines = false,
25
+ author = 'AI',
26
+ revisionIdAllocator = null,
27
+ trackAsBlock = false
28
+ } = options;
29
+ // trackAsBlock: Word-native inserted table = tracked-inserted rows (trPr/ins), inserted cell runs and
30
+ // tracked-inserted cell paragraph marks. (w:ins around w:tbl is not a table insertion to Word.)
31
+ const trackRows = generateRedlines && trackAsBlock;
29
32
 
30
33
  // Determine number of columns
31
34
  const numCols = tableData.headers?.length || (tableData.rows?.[0]?.length || 1);
@@ -65,7 +68,7 @@ export function generateTableOoxml(tableData, options = {}) {
65
68
 
66
69
  // Build run model for the cell
67
70
  const runModel = [{
68
- kind: generateRedlines && !trackAsBlock ? RunKind.INSERTION : RunKind.TEXT,
71
+ kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
69
72
  text: cleanText,
70
73
  rPrXml: isHeaderRow ? '<w:rPr><w:b/></w:rPr>' : '',
71
74
  author,
@@ -73,11 +76,12 @@ export function generateTableOoxml(tableData, options = {}) {
73
76
  endOffset: cleanText.length
74
77
  }];
75
78
 
76
- const runsOoxml = serializeToOoxml(runModel, null, formatHints, {
77
- author,
78
- generateRedlines,
79
- revisionIdAllocator
80
- });
79
+ const cellPPr = trackRows ? paragraphMarkPPrXml('ins', { author, revisionIdAllocator }) : null;
80
+ const runsOoxml = serializeToOoxml(runModel, cellPPr, formatHints, {
81
+ author,
82
+ generateRedlines,
83
+ revisionIdAllocator
84
+ });
81
85
 
82
86
  // Cell properties
83
87
  const tcPr = '<w:tcPr><w:tcW w:w="0" w:type="auto"/></w:tcPr>';
@@ -85,24 +89,20 @@ export function generateTableOoxml(tableData, options = {}) {
85
89
  cellsXml += `<w:tc>${tcPr}${runsOoxml}</w:tc>`;
86
90
  }
87
91
 
88
- // Keep each logical record intact across page boundaries. When a
89
- // Markdown header is present, also mark it as a repeating Word table
90
- // header so continuation pages retain their column labels.
91
- const trPr = isHeaderRow
92
- ? '<w:trPr><w:tblHeader/><w:cantSplit/></w:trPr>'
93
- : '<w:trPr><w:cantSplit/></w:trPr>';
94
- rowsXml += `<w:tr>${trPr}${cellsXml}</w:tr>`;
92
+ // Keep each logical record intact across page boundaries. When a
93
+ // Markdown header is present, also mark it as a repeating Word table
94
+ // header so continuation pages retain their column labels.
95
+ const trPr = isHeaderRow
96
+ ? '<w:trPr><w:tblHeader/><w:cantSplit/></w:trPr>'
97
+ : '<w:trPr><w:cantSplit/></w:trPr>';
98
+ const rowMeta = trackRows ? createRevisionMetadata(author, revisionIdAllocator) : null;
99
+ const trackedTrPr = rowMeta
100
+ ? trPr.replace('</w:trPr>', `<w:ins w:id="${rowMeta.id}" w:author="${escapeXml(rowMeta.author)}" w:date="${rowMeta.date}"/></w:trPr>`)
101
+ : trPr;
102
+ rowsXml += `<w:tr>${trackedTrPr}${cellsXml}</w:tr>`;
95
103
  }
96
104
 
97
- // Build the table
98
- let tableXml = `<w:tbl>${tblPr}${tblGrid}${rowsXml}</w:tbl>`;
99
-
100
- // Wrap entire table in w:ins if generating redlines
101
- if (tableInsertMeta) {
102
- tableXml = `<w:ins w:id="${tableInsertMeta.id}" w:author="${escapeXml(tableInsertMeta.author)}" w:date="${tableInsertMeta.date}">${tableXml}</w:ins>`;
103
- }
104
-
105
- return tableXml;
105
+ return `<w:tbl>${tblPr}${tblGrid}${rowsXml}</w:tbl>`;
106
106
  }
107
107
 
108
108
  /**
@@ -151,8 +151,9 @@ export function diffTablesWithVirtualGrid(oldGrid, newTableData) {
151
151
  if (col > oldCell.gridCol) continue;
152
152
 
153
153
  if (newCellText !== undefined) {
154
- const oldText = oldCell.getText();
155
- if (oldText !== newCellText) {
154
+ // Markdown cannot represent a cell's trailing empty paragraphs, so they do not count as a change.
155
+ const oldText = trimTrailingNewlines(oldCell.getText());
156
+ if (oldText !== trimTrailingNewlines(newCellText)) {
156
157
  operations.push({
157
158
  type: 'cell_modify',
158
159
  gridRow: row,
@@ -177,7 +178,7 @@ export function diffTablesWithVirtualGrid(oldGrid, newTableData) {
177
178
  * @returns {string} Reconciled w:tbl OOXML
178
179
  */
179
180
  export function serializeVirtualGridToOoxml(grid, operations, options) {
180
- const { generateRedlines, author, revisionIdAllocator = null } = options;
181
+ const { generateRedlines, author, revisionIdAllocator = null } = options;
181
182
  const opIndex = buildTableOperationIndex(operations);
182
183
 
183
184
  let rowsXml = '';
@@ -219,7 +220,7 @@ export function serializeVirtualGridToOoxml(grid, operations, options) {
219
220
  let trPr = grid.trPrList[row] || '<w:trPr/>';
220
221
 
221
222
  if (rowDeleteOp && generateRedlines) {
222
- const metadata = createRevisionMetadata(author, revisionIdAllocator);
223
+ const metadata = createRevisionMetadata(author, revisionIdAllocator);
223
224
  const delMark = `<w:del w:id="${metadata.id}" w:author="${escapeXml(metadata.author)}" w:date="${metadata.date}"/>`;
224
225
  if (trPr.includes('</w:trPr>')) {
225
226
  trPr = trPr.replace('</w:trPr>', `${delMark}</w:trPr>`);
@@ -235,7 +236,7 @@ export function serializeVirtualGridToOoxml(grid, operations, options) {
235
236
  const insertOps = opIndex.rowInsertOperations;
236
237
  for (const op of insertOps) {
237
238
  let cellsXml = '';
238
- const rowInsertMeta = generateRedlines ? createRevisionMetadata(author, revisionIdAllocator) : null;
239
+ const rowInsertMeta = generateRedlines ? createRevisionMetadata(author, revisionIdAllocator) : null;
239
240
 
240
241
  for (const cellText of op.cells) {
241
242
  const { cleanText, formatHints } = preprocessMarkdown(cellText);
@@ -249,11 +250,11 @@ export function serializeVirtualGridToOoxml(grid, operations, options) {
249
250
  endOffset: cleanText.length
250
251
  }];
251
252
 
252
- const runsOoxml = serializeToOoxml(runModel, null, formatHints, {
253
- author,
254
- generateRedlines,
255
- revisionIdAllocator
256
- });
253
+ const runsOoxml = serializeToOoxml(runModel, null, formatHints, {
254
+ author,
255
+ generateRedlines,
256
+ revisionIdAllocator
257
+ });
257
258
  // serializeToOoxml already returns one or more w:p blocks.
258
259
  cellsXml += `<w:tc><w:tcPr><w:tcW w:w="0" w:type="auto"/></w:tcPr>${runsOoxml}</w:tc>`;
259
260
  }
@@ -309,8 +310,61 @@ function buildTableOperationIndex(operations) {
309
310
  };
310
311
  }
311
312
 
313
+ function trimTrailingNewlines(text) {
314
+ return typeof text === 'string' ? text.replace(/\n+$/, '') : text;
315
+ }
316
+
317
+ /**
318
+ * Reconciles a multi-paragraph cell paragraph by paragraph so unchanged paragraphs (including
319
+ * empty ones the Markdown cell text cannot express) are preserved exactly.
320
+ */
321
+ function reconcileMultiBlockCellContent(cell, newText, options) {
322
+ const { generateRedlines, author, revisionIdAllocator = null } = options;
323
+ const lines = String(newText ?? '').split('\n');
324
+ // Trailing blank lines carry no content; never create inserted paragraphs from them.
325
+ while (lines.length > cell.blocks.length && lines[lines.length - 1] === '') lines.pop();
326
+ let out = '';
327
+
328
+ cell.blocks.forEach((block, index) => {
329
+ const line = lines[index];
330
+ if (line === undefined || line === block.acceptedText) {
331
+ if (line === undefined && block.acceptedText !== '') {
332
+ // Markdown dropped this paragraph's text: track the text deletion.
333
+ const diffOps = computeWordLevelDiffOps(block.acceptedText, '');
334
+ const model = applyPatches(splitRunsAtDiffBoundaries(block.runModel, diffOps), diffOps, {
335
+ generateRedlines, author, formatHints: []
336
+ });
337
+ out += serializeToOoxml(model, block.pPr, [], { author, generateRedlines, revisionIdAllocator });
338
+ } else {
339
+ out += serializeToOoxml(block.runModel, block.pPr, [], {});
340
+ }
341
+ return;
342
+ }
343
+ const { cleanText, formatHints } = preprocessMarkdown(line);
344
+ const diffOps = computeWordLevelDiffOps(block.acceptedText, cleanText);
345
+ const model = applyPatches(splitRunsAtDiffBoundaries(block.runModel, diffOps), diffOps, {
346
+ generateRedlines, author, formatHints
347
+ });
348
+ out += serializeToOoxml(model, block.pPr, formatHints, { author, generateRedlines, revisionIdAllocator });
349
+ });
350
+
351
+ for (let i = cell.blocks.length; i < lines.length; i++) {
352
+ const { cleanText, formatHints } = preprocessMarkdown(lines[i]);
353
+ const runModel = [{
354
+ kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
355
+ text: cleanText, rPrXml: '', author, startOffset: 0, endOffset: cleanText.length
356
+ }];
357
+ const pPr = generateRedlines ? paragraphMarkPPrXml('ins', { author, revisionIdAllocator }) : null;
358
+ out += serializeToOoxml(runModel, pPr, formatHints, { author, generateRedlines, revisionIdAllocator });
359
+ }
360
+ return out;
361
+ }
362
+
312
363
  function reconcileCellContent(cell, newText, options) {
313
- const { generateRedlines, author, revisionIdAllocator = null } = options;
364
+ if (cell.blocks.length > 1) {
365
+ return reconcileMultiBlockCellContent(cell, newText, options);
366
+ }
367
+ const { generateRedlines, author, revisionIdAllocator = null } = options;
314
368
  const { cleanText, formatHints } = preprocessMarkdown(newText);
315
369
 
316
370
  // For now, satisfy with single block or join blocks
@@ -327,11 +381,11 @@ function reconcileCellContent(cell, newText, options) {
327
381
  formatHints
328
382
  });
329
383
 
330
- const runsOoxml = serializeToOoxml(patchedModel, baseBlock.pPr, formatHints, {
331
- author,
332
- generateRedlines,
333
- revisionIdAllocator
334
- });
384
+ const runsOoxml = serializeToOoxml(patchedModel, baseBlock.pPr, formatHints, {
385
+ author,
386
+ generateRedlines,
387
+ revisionIdAllocator
388
+ });
335
389
  return runsOoxml;
336
390
  }
337
391
 
@@ -343,7 +397,7 @@ function serializeCellBlocks(blocks) {
343
397
  }).join('');
344
398
  }
345
399
 
346
- function buildTcXml(cell, content, _options) {
400
+ function buildTcXml(cell, content, _options) {
347
401
  let tcPr = cell.tcPrXml;
348
402
 
349
403
  // Ensure gridSpan is preserved