@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.
Files changed (64) hide show
  1. package/AGENTS.md +176 -0
  2. package/ARCHITECTURE.md +121 -0
  3. package/LICENSE +21 -0
  4. package/README.md +177 -0
  5. package/adapters/config.js +43 -0
  6. package/adapters/logger.js +89 -0
  7. package/adapters/xml-adapter.js +74 -0
  8. package/core/list-targeting.js +398 -0
  9. package/core/ooxml-identifiers.js +15 -0
  10. package/core/paragraph-offset-policy.js +50 -0
  11. package/core/paragraph-targeting.js +501 -0
  12. package/core/table-targeting.js +233 -0
  13. package/core/types.js +204 -0
  14. package/core/xml-query.js +99 -0
  15. package/dist/docx-redline-js.esm.js +8801 -0
  16. package/dist/docx-redline-js.esm.js.map +7 -0
  17. package/dist/docx-redline-js.esm.min.js +195 -0
  18. package/dist/docx-redline-js.esm.min.js.map +7 -0
  19. package/engine/format-application.js +358 -0
  20. package/engine/format-extraction.js +232 -0
  21. package/engine/format-paragraph-targeting.js +208 -0
  22. package/engine/format-span-application.js +178 -0
  23. package/engine/formatting-removal.js +330 -0
  24. package/engine/oxml-engine.js +279 -0
  25. package/engine/reconstruction-mapper.js +270 -0
  26. package/engine/reconstruction-mode.js +38 -0
  27. package/engine/reconstruction-writer.js +276 -0
  28. package/engine/rpr-helpers.js +194 -0
  29. package/engine/run-builders.js +235 -0
  30. package/engine/surgical-mode.js +520 -0
  31. package/engine/table-cell-context.js +151 -0
  32. package/engine/table-mode.js +172 -0
  33. package/index.js +308 -0
  34. package/orchestration/list-markdown.js +141 -0
  35. package/orchestration/list-parsing.js +73 -0
  36. package/orchestration/list-structural-fallback.js +530 -0
  37. package/orchestration/redline-operation-converter.js +141 -0
  38. package/orchestration/route-plan.js +160 -0
  39. package/package.json +76 -0
  40. package/pipeline/content-analysis.js +107 -0
  41. package/pipeline/diff-engine.js +204 -0
  42. package/pipeline/ingestion-export.js +255 -0
  43. package/pipeline/ingestion-paragraph.js +351 -0
  44. package/pipeline/ingestion-table.js +169 -0
  45. package/pipeline/ingestion-xml.js +39 -0
  46. package/pipeline/ingestion.js +8 -0
  47. package/pipeline/list-generation.js +280 -0
  48. package/pipeline/list-markers.js +77 -0
  49. package/pipeline/markdown-processor.js +160 -0
  50. package/pipeline/patching.js +408 -0
  51. package/pipeline/pipeline.js +326 -0
  52. package/pipeline/serialization.js +395 -0
  53. package/services/browser-demo-prompt-context.js +345 -0
  54. package/services/comment-builders.js +60 -0
  55. package/services/comment-engine.js +248 -0
  56. package/services/comment-locator.js +197 -0
  57. package/services/comment-package.js +113 -0
  58. package/services/numbering-helpers.js +416 -0
  59. package/services/numbering-service.js +290 -0
  60. package/services/package-builder.js +147 -0
  61. package/services/standalone-docx-plumbing.js +443 -0
  62. package/services/standalone-operation-runner.js +1169 -0
  63. package/services/table-reconciliation.js +344 -0
  64. 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
+ }
package/standalone.js ADDED
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @deprecated Import from `./index.js` for host-agnostic reconciliation APIs.
3
+ * This shim is kept for backward compatibility with older local imports.
4
+ */
5
+ export * from './index.js';