@ansonlai/docx-redline-js 0.1.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/AGENTS.md +176 -0
- package/ARCHITECTURE.md +121 -0
- package/LICENSE +21 -0
- package/README.md +177 -0
- package/adapters/config.js +43 -0
- package/adapters/logger.js +89 -0
- package/adapters/xml-adapter.js +74 -0
- package/core/list-targeting.js +398 -0
- package/core/ooxml-identifiers.js +15 -0
- package/core/paragraph-offset-policy.js +50 -0
- package/core/paragraph-targeting.js +501 -0
- package/core/table-targeting.js +233 -0
- package/core/types.js +204 -0
- package/core/xml-query.js +99 -0
- package/dist/docx-redline-js.esm.js +8801 -0
- package/dist/docx-redline-js.esm.js.map +7 -0
- package/dist/docx-redline-js.esm.min.js +195 -0
- package/dist/docx-redline-js.esm.min.js.map +7 -0
- package/engine/format-application.js +358 -0
- package/engine/format-extraction.js +232 -0
- package/engine/format-paragraph-targeting.js +208 -0
- package/engine/format-span-application.js +178 -0
- package/engine/formatting-removal.js +330 -0
- package/engine/oxml-engine.js +279 -0
- package/engine/reconstruction-mapper.js +270 -0
- package/engine/reconstruction-mode.js +38 -0
- package/engine/reconstruction-writer.js +276 -0
- package/engine/rpr-helpers.js +194 -0
- package/engine/run-builders.js +235 -0
- package/engine/surgical-mode.js +520 -0
- package/engine/table-cell-context.js +151 -0
- package/engine/table-mode.js +172 -0
- package/index.js +308 -0
- package/orchestration/list-markdown.js +141 -0
- package/orchestration/list-parsing.js +73 -0
- package/orchestration/list-structural-fallback.js +530 -0
- package/orchestration/redline-operation-converter.js +141 -0
- package/orchestration/route-plan.js +160 -0
- package/package.json +76 -0
- package/pipeline/content-analysis.js +107 -0
- package/pipeline/diff-engine.js +204 -0
- package/pipeline/ingestion-export.js +255 -0
- package/pipeline/ingestion-paragraph.js +351 -0
- package/pipeline/ingestion-table.js +169 -0
- package/pipeline/ingestion-xml.js +39 -0
- package/pipeline/ingestion.js +8 -0
- package/pipeline/list-generation.js +280 -0
- package/pipeline/list-markers.js +77 -0
- package/pipeline/markdown-processor.js +160 -0
- package/pipeline/patching.js +408 -0
- package/pipeline/pipeline.js +326 -0
- package/pipeline/serialization.js +395 -0
- package/services/browser-demo-prompt-context.js +345 -0
- package/services/comment-builders.js +60 -0
- package/services/comment-engine.js +248 -0
- package/services/comment-locator.js +197 -0
- package/services/comment-package.js +113 -0
- package/services/numbering-helpers.js +416 -0
- package/services/numbering-service.js +290 -0
- package/services/package-builder.js +147 -0
- package/services/standalone-docx-plumbing.js +443 -0
- package/services/standalone-operation-runner.js +1169 -0
- package/services/table-reconciliation.js +344 -0
- package/standalone.js +5 -0
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OOXML Reconciliation Pipeline - Table Reconciliation
|
|
3
|
+
*
|
|
4
|
+
* Logic for reconciling tables using a Virtual Grid to handle merged cells.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { computeWordLevelDiffOps } from '../pipeline/diff-engine.js';
|
|
8
|
+
import { splitRunsAtDiffBoundaries, applyPatches } from '../pipeline/patching.js';
|
|
9
|
+
import { serializeToOoxml } from '../pipeline/serialization.js';
|
|
10
|
+
import { NS_W, getNextRevisionId, getRevisionTimestamp, createRevisionMetadata, escapeXml, RunKind } from '../core/types.js';
|
|
11
|
+
import { preprocessMarkdown } from '../pipeline/markdown-processor.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Generates a new w:tbl OOXML structure from Markdown table data.
|
|
15
|
+
* This enables pure OOXML table creation without Word JS API.
|
|
16
|
+
*
|
|
17
|
+
* @param {Object} tableData - Parsed Markdown table { headers, rows, hasHeader }
|
|
18
|
+
* @param {Object} options - { generateRedlines, author }
|
|
19
|
+
* @returns {string} Complete w:tbl OOXML
|
|
20
|
+
*/
|
|
21
|
+
export function generateTableOoxml(tableData, options = {}) {
|
|
22
|
+
const { generateRedlines = false, author = 'AI' } = options;
|
|
23
|
+
const date = getRevisionTimestamp();
|
|
24
|
+
const revId = generateRedlines ? getNextRevisionId() : null;
|
|
25
|
+
|
|
26
|
+
// Determine number of columns
|
|
27
|
+
const numCols = tableData.headers?.length || (tableData.rows?.[0]?.length || 1);
|
|
28
|
+
|
|
29
|
+
// Build default table properties (100% width, single borders)
|
|
30
|
+
const tblPr = `
|
|
31
|
+
<w:tblPr>
|
|
32
|
+
<w:tblW w:w="5000" w:type="pct"/>
|
|
33
|
+
<w:tblBorders>
|
|
34
|
+
<w:top w:val="single" w:sz="4" w:space="0" w:color="auto"/>
|
|
35
|
+
<w:left w:val="single" w:sz="4" w:space="0" w:color="auto"/>
|
|
36
|
+
<w:bottom w:val="single" w:sz="4" w:space="0" w:color="auto"/>
|
|
37
|
+
<w:right w:val="single" w:sz="4" w:space="0" w:color="auto"/>
|
|
38
|
+
<w:insideH w:val="single" w:sz="4" w:space="0" w:color="auto"/>
|
|
39
|
+
<w:insideV w:val="single" w:sz="4" w:space="0" w:color="auto"/>
|
|
40
|
+
</w:tblBorders>
|
|
41
|
+
<w:tblLook w:val="04A0" w:firstRow="1" w:lastRow="0" w:firstColumn="1" w:lastColumn="0" w:noHBand="0" w:noVBand="1"/>
|
|
42
|
+
</w:tblPr>
|
|
43
|
+
`.trim();
|
|
44
|
+
|
|
45
|
+
// Build grid columns (equal width)
|
|
46
|
+
const gridCols = Array(numCols).fill('<w:gridCol/>').join('');
|
|
47
|
+
const tblGrid = `<w:tblGrid>${gridCols}</w:tblGrid>`;
|
|
48
|
+
|
|
49
|
+
// Build all rows
|
|
50
|
+
const allRows = tableData.hasHeader ? [tableData.headers, ...tableData.rows] : tableData.rows;
|
|
51
|
+
let rowsXml = '';
|
|
52
|
+
|
|
53
|
+
for (let r = 0; r < allRows.length; r++) {
|
|
54
|
+
const isHeaderRow = tableData.hasHeader && r === 0;
|
|
55
|
+
const row = allRows[r] || [];
|
|
56
|
+
let cellsXml = '';
|
|
57
|
+
|
|
58
|
+
for (let c = 0; c < numCols; c++) {
|
|
59
|
+
const cellText = row[c] || '';
|
|
60
|
+
const { cleanText, formatHints } = preprocessMarkdown(cellText);
|
|
61
|
+
|
|
62
|
+
// Build run model for the cell
|
|
63
|
+
const runModel = [{
|
|
64
|
+
kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
|
|
65
|
+
text: cleanText,
|
|
66
|
+
rPrXml: isHeaderRow ? '<w:rPr><w:b/></w:rPr>' : '',
|
|
67
|
+
author,
|
|
68
|
+
startOffset: 0,
|
|
69
|
+
endOffset: cleanText.length
|
|
70
|
+
}];
|
|
71
|
+
|
|
72
|
+
const runsOoxml = serializeToOoxml(runModel, null, formatHints, { author, generateRedlines });
|
|
73
|
+
|
|
74
|
+
// Cell properties
|
|
75
|
+
const tcPr = '<w:tcPr><w:tcW w:w="0" w:type="auto"/></w:tcPr>';
|
|
76
|
+
// serializeToOoxml already returns one or more w:p blocks.
|
|
77
|
+
cellsXml += `<w:tc>${tcPr}${runsOoxml}</w:tc>`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Row properties
|
|
81
|
+
const trPr = '<w:trPr/>';
|
|
82
|
+
rowsXml += `<w:tr>${trPr}${cellsXml}</w:tr>`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Build the table
|
|
86
|
+
let tableXml = `<w:tbl>${tblPr}${tblGrid}${rowsXml}</w:tbl>`;
|
|
87
|
+
|
|
88
|
+
// Wrap entire table in w:ins if generating redlines
|
|
89
|
+
if (generateRedlines && revId) {
|
|
90
|
+
tableXml = `<w:ins w:id="${revId}" w:author="${escapeXml(author)}" w:date="${date}">${tableXml}</w:ins>`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return tableXml;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Computes operations to reconcile two tables.
|
|
98
|
+
*
|
|
99
|
+
* @param {Object} oldGrid - Original Virtual Grid model
|
|
100
|
+
* @param {Object} newTableData - Parsed markdown table data
|
|
101
|
+
* @returns {Array} List of operations
|
|
102
|
+
*/
|
|
103
|
+
|
|
104
|
+
export function diffTablesWithVirtualGrid(oldGrid, newTableData) {
|
|
105
|
+
const operations = [];
|
|
106
|
+
const { headers, rows: newRowsData, hasHeader } = newTableData;
|
|
107
|
+
const allNewRows = hasHeader ? [headers, ...newRowsData] : newRowsData;
|
|
108
|
+
|
|
109
|
+
const maxRows = Math.max(oldGrid.rowCount, allNewRows.length);
|
|
110
|
+
|
|
111
|
+
for (let row = 0; row < maxRows; row++) {
|
|
112
|
+
// Row insertion
|
|
113
|
+
if (row >= oldGrid.rowCount && allNewRows[row]) {
|
|
114
|
+
operations.push({
|
|
115
|
+
type: 'row_insert',
|
|
116
|
+
gridRow: row,
|
|
117
|
+
cells: allNewRows[row]
|
|
118
|
+
});
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Row deletion
|
|
123
|
+
if (row < oldGrid.rowCount && !allNewRows[row]) {
|
|
124
|
+
operations.push({
|
|
125
|
+
type: 'row_delete',
|
|
126
|
+
gridRow: row
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const newRow = allNewRows[row];
|
|
132
|
+
|
|
133
|
+
for (let col = 0; col < oldGrid.colCount; col++) {
|
|
134
|
+
const oldCell = oldGrid.grid[row]?.[col];
|
|
135
|
+
const newCellText = newRow?.[col];
|
|
136
|
+
|
|
137
|
+
if (!oldCell) continue;
|
|
138
|
+
if (oldCell.isMergeContinuation) continue;
|
|
139
|
+
if (col > oldCell.gridCol) continue;
|
|
140
|
+
|
|
141
|
+
if (newCellText !== undefined) {
|
|
142
|
+
const oldText = oldCell.getText();
|
|
143
|
+
if (oldText !== newCellText) {
|
|
144
|
+
operations.push({
|
|
145
|
+
type: 'cell_modify',
|
|
146
|
+
gridRow: row,
|
|
147
|
+
gridCol: col,
|
|
148
|
+
originalCell: oldCell,
|
|
149
|
+
newText: newCellText
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return operations;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Serializes the virtual grid back to OOXML after applying operations.
|
|
161
|
+
*
|
|
162
|
+
* @param {Object} grid - The Virtual Grid
|
|
163
|
+
* @param {Array} operations - Diff operations
|
|
164
|
+
* @param {Object} options - Options (generateRedlines, author)
|
|
165
|
+
* @returns {string} Reconciled w:tbl OOXML
|
|
166
|
+
*/
|
|
167
|
+
export function serializeVirtualGridToOoxml(grid, operations, options) {
|
|
168
|
+
const { generateRedlines, author } = options;
|
|
169
|
+
const opIndex = buildTableOperationIndex(operations);
|
|
170
|
+
|
|
171
|
+
let rowsXml = '';
|
|
172
|
+
|
|
173
|
+
for (let row = 0; row < grid.rowCount; row++) {
|
|
174
|
+
const rowDeleteOp = opIndex.rowDeleteByRow.get(row) || null;
|
|
175
|
+
|
|
176
|
+
if (rowDeleteOp && !generateRedlines) continue;
|
|
177
|
+
|
|
178
|
+
let cellsXml = '';
|
|
179
|
+
let col = 0;
|
|
180
|
+
|
|
181
|
+
while (col < grid.colCount) {
|
|
182
|
+
const cell = grid.grid[row][col];
|
|
183
|
+
|
|
184
|
+
if (!cell) {
|
|
185
|
+
col++;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (cell.isMergeContinuation) {
|
|
190
|
+
col++;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const modOp = opIndex.cellModifyByCoordinate.get(`${row}:${col}`) || null;
|
|
195
|
+
|
|
196
|
+
let cellContent;
|
|
197
|
+
if (modOp) {
|
|
198
|
+
cellContent = reconcileCellContent(cell, modOp.newText, options);
|
|
199
|
+
} else {
|
|
200
|
+
cellContent = serializeCellBlocks(cell.blocks);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
cellsXml += buildTcXml(cell, cellContent, options);
|
|
204
|
+
col += cell.colSpan;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let trPr = grid.trPrList[row] || '<w:trPr/>';
|
|
208
|
+
|
|
209
|
+
if (rowDeleteOp && generateRedlines) {
|
|
210
|
+
const metadata = createRevisionMetadata(author);
|
|
211
|
+
const delMark = `<w:del w:id="${metadata.id}" w:author="${escapeXml(metadata.author)}" w:date="${metadata.date}"/>`;
|
|
212
|
+
if (trPr.includes('</w:trPr>')) {
|
|
213
|
+
trPr = trPr.replace('</w:trPr>', `${delMark}</w:trPr>`);
|
|
214
|
+
} else {
|
|
215
|
+
trPr = `<w:trPr>${delMark}</w:trPr>`;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
rowsXml += `<w:tr>${trPr}${cellsXml}</w:tr>`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Handle row insertions
|
|
223
|
+
const insertOps = opIndex.rowInsertOperations;
|
|
224
|
+
for (const op of insertOps) {
|
|
225
|
+
let cellsXml = '';
|
|
226
|
+
const rowInsertMeta = generateRedlines ? createRevisionMetadata(author) : null;
|
|
227
|
+
|
|
228
|
+
for (const cellText of op.cells) {
|
|
229
|
+
const { cleanText, formatHints } = preprocessMarkdown(cellText);
|
|
230
|
+
// Simple run model for new cell
|
|
231
|
+
const runModel = [{
|
|
232
|
+
kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
|
|
233
|
+
text: cleanText,
|
|
234
|
+
rPrXml: '',
|
|
235
|
+
author,
|
|
236
|
+
startOffset: 0,
|
|
237
|
+
endOffset: cleanText.length
|
|
238
|
+
}];
|
|
239
|
+
|
|
240
|
+
const runsOoxml = serializeToOoxml(runModel, null, formatHints, { author, generateRedlines });
|
|
241
|
+
// serializeToOoxml already returns one or more w:p blocks.
|
|
242
|
+
cellsXml += `<w:tc><w:tcPr><w:tcW w:w="0" w:type="auto"/></w:tcPr>${runsOoxml}</w:tc>`;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let trPr = '<w:trPr/>';
|
|
246
|
+
if (generateRedlines) {
|
|
247
|
+
trPr = `<w:trPr><w:ins w:id="${rowInsertMeta.id}" w:author="${escapeXml(rowInsertMeta.author)}" w:date="${rowInsertMeta.date}"/></w:trPr>`;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
rowsXml += `<w:tr>${trPr}${cellsXml}</w:tr>`;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return `
|
|
254
|
+
<w:tbl>
|
|
255
|
+
${grid.tblPrXml}
|
|
256
|
+
${grid.tblGridXml}
|
|
257
|
+
${rowsXml}
|
|
258
|
+
</w:tbl>
|
|
259
|
+
`;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Builds indexed table operation lookups for row/cell hot paths.
|
|
264
|
+
*
|
|
265
|
+
* @param {Array} operations - Table diff operations
|
|
266
|
+
* @returns {{
|
|
267
|
+
* rowDeleteByRow: Map<number, Object>,
|
|
268
|
+
* cellModifyByCoordinate: Map<string, Object>,
|
|
269
|
+
* rowInsertOperations: Object[]
|
|
270
|
+
* }}
|
|
271
|
+
*/
|
|
272
|
+
function buildTableOperationIndex(operations) {
|
|
273
|
+
const rowDeleteByRow = new Map();
|
|
274
|
+
const cellModifyByCoordinate = new Map();
|
|
275
|
+
const rowInsertOperations = [];
|
|
276
|
+
|
|
277
|
+
for (const operation of operations) {
|
|
278
|
+
if (operation.type === 'row_delete') {
|
|
279
|
+
rowDeleteByRow.set(operation.gridRow, operation);
|
|
280
|
+
} else if (operation.type === 'cell_modify') {
|
|
281
|
+
cellModifyByCoordinate.set(`${operation.gridRow}:${operation.gridCol}`, operation);
|
|
282
|
+
} else if (operation.type === 'row_insert') {
|
|
283
|
+
rowInsertOperations.push(operation);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
rowInsertOperations.sort((a, b) => a.gridRow - b.gridRow);
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
rowDeleteByRow,
|
|
291
|
+
cellModifyByCoordinate,
|
|
292
|
+
rowInsertOperations
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function reconcileCellContent(cell, newText, options) {
|
|
297
|
+
const { generateRedlines, author } = options;
|
|
298
|
+
const { cleanText, formatHints } = preprocessMarkdown(newText);
|
|
299
|
+
|
|
300
|
+
// For now, satisfy with single block or join blocks
|
|
301
|
+
// In a full implementation, we'd diff paragraphs within the cell
|
|
302
|
+
const oldText = cell.getText();
|
|
303
|
+
const diffOps = computeWordLevelDiffOps(oldText, cleanText);
|
|
304
|
+
|
|
305
|
+
// Use the first block as reference for formatting if available
|
|
306
|
+
const baseBlock = cell.blocks[0] || { runModel: [], pPr: null };
|
|
307
|
+
const splitModel = splitRunsAtDiffBoundaries(baseBlock.runModel, diffOps);
|
|
308
|
+
const patchedModel = applyPatches(splitModel, diffOps, {
|
|
309
|
+
generateRedlines,
|
|
310
|
+
author,
|
|
311
|
+
formatHints
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const runsOoxml = serializeToOoxml(patchedModel, baseBlock.pPr, formatHints, { author, generateRedlines });
|
|
315
|
+
return runsOoxml;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function serializeCellBlocks(blocks) {
|
|
319
|
+
return blocks.map(b => {
|
|
320
|
+
const runsOoxml = serializeToOoxml(b.runModel, b.pPr, [], {});
|
|
321
|
+
// serializeToOoxml already returns one or more w:p blocks.
|
|
322
|
+
return runsOoxml;
|
|
323
|
+
}).join('');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function buildTcXml(cell, content, options) {
|
|
327
|
+
let tcPr = cell.tcPrXml;
|
|
328
|
+
|
|
329
|
+
// Ensure gridSpan is preserved
|
|
330
|
+
if (cell.colSpan > 1 && !tcPr.includes('gridSpan')) {
|
|
331
|
+
tcPr = tcPr.replace('</w:tcPr>', `<w:gridSpan w:val="${cell.colSpan}"/></w:tcPr>`);
|
|
332
|
+
} else if (cell.colSpan > 1 && tcPr === '<w:tcPr/>') {
|
|
333
|
+
tcPr = `<w:tcPr><w:gridSpan w:val="${cell.colSpan}"/></w:tcPr>`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Ensure vMerge is preserved for origin cells
|
|
337
|
+
if (cell.rowSpan > 1 && cell.isMergeOrigin && !tcPr.includes('vMerge')) {
|
|
338
|
+
tcPr = tcPr.replace('</w:tcPr>', '<w:vMerge w:val="restart"/></w:tcPr>');
|
|
339
|
+
} else if (cell.rowSpan > 1 && cell.isMergeOrigin && tcPr === '<w:tcPr/>') {
|
|
340
|
+
tcPr = '<w:tcPr><w:vMerge w:val="restart"/></w:tcPr>';
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return `<w:tc>${tcPr}${content}</w:tc>`;
|
|
344
|
+
}
|