@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.
- package/ARCHITECTURE.md +221 -12
- package/CHANGELOG.md +803 -586
- package/README.md +1018 -901
- package/core/current-properties.js +136 -0
- package/core/paragraph-text.js +211 -209
- package/core/types.js +9 -3
- package/core/word-revision-semantics.js +256 -0
- package/dist/docx-redline-js.esm.js +4551 -967
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +112 -106
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/dist/docx-redline.bundle.cjs +4690 -1106
- package/dist/docx-redline.bundle.cjs.map +4 -4
- package/dist/docx-redline.bundle.js +4690 -1106
- package/dist/docx-redline.bundle.js.map +4 -4
- package/docs/AGENT_FAST_START.md +2 -0
- package/docs/AGENT_KNOWLEDGE_BASE.md +23 -4
- package/docs/SKILL_AUTHORING.md +165 -150
- package/docs/TESTING.md +1142 -1059
- package/docs/schemas/document-operations.schema.json +221 -168
- package/document/docx-document.js +1022 -1010
- package/engine/oxml-engine.js +567 -560
- package/engine/paragraph-mark-revisions.js +292 -0
- package/engine/reconstruction-mapper.js +312 -311
- package/engine/reconstruction-mode.js +183 -183
- package/engine/reconstruction-writer.js +67 -10
- package/engine/rpr-helpers.js +42 -0
- package/engine/run-builders.js +0 -73
- package/engine/surgical-diff-application.js +604 -605
- package/engine/table-mode.js +63 -63
- package/index.d.ts +51 -1
- package/index.js +47 -3
- package/node/cli-help.js +1 -0
- package/node/cli.js +992 -974
- package/orchestration/list-structural-fallback.js +67 -9
- package/package.json +1 -1
- package/pipeline/diff-engine.js +130 -1
- package/pipeline/ingestion-export.js +227 -243
- package/pipeline/ingestion-paragraph.js +93 -98
- package/pipeline/list-generation.js +415 -419
- package/pipeline/pipeline.js +206 -204
- package/pipeline/serialization.js +111 -94
- package/scripts/export-validation-fixtures.mjs +19 -6
- package/scripts/generate-word-revision-semantics-matrix.mjs +134 -0
- package/scripts/lib/word-coverage-catalogue.mjs +7 -2
- package/scripts/render-multilevel-bullet-visual.ps1 +2 -2
- package/scripts/word-com-corpus-visual-suite.ps1 +2 -2
- package/scripts/word-com-differential.ps1 +443 -335
- package/scripts/word-com-visual-suite.ps1 +2 -2
- package/scripts/word-revision-semantics-matrix.ps1 +108 -0
- package/services/batch-operation-orchestrator.js +26 -0
- package/services/capture-engine.js +228 -226
- package/services/document-operation-applier.js +215 -0
- package/services/document-operation-contract.js +628 -537
- package/services/document-operation-mutations.js +789 -46
- package/services/error-recovery.js +17 -0
- package/services/list-conversion-diagnostics.js +296 -0
- package/services/list-format-change.js +586 -0
- package/services/list-format-mutation.js +416 -0
- package/services/list-format.js +305 -0
- package/services/numbering-allocation.js +642 -0
- package/services/numbering-helpers.js +354 -524
- package/services/operation-batch-compiler.js +612 -436
- package/services/operation-heuristics.js +197 -173
- package/services/operation-preflight.js +1 -0
- package/services/receipt-collector.js +288 -288
- package/services/revision-comment-management.js +634 -594
- package/services/revision-fidelity-verifier.js +498 -0
- package/services/standalone-docx-plumbing.js +627 -631
- package/services/standalone-operation-runner.d.ts +606 -427
- 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
|
-
|
|
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
|
|
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
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
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
|
-
|
|
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
|