@ansonlai/docx-redline-js 0.8.4 → 0.10.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 (72) hide show
  1. package/ARCHITECTURE.md +679 -460
  2. package/CHANGELOG.md +208 -1
  3. package/README.md +152 -16
  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/core/word-xml.js +160 -118
  9. package/dist/docx-redline-js.esm.js +4338 -796
  10. package/dist/docx-redline-js.esm.js.map +4 -4
  11. package/dist/docx-redline-js.esm.min.js +111 -108
  12. package/dist/docx-redline-js.esm.min.js.map +4 -4
  13. package/dist/docx-redline.bundle.cjs +4463 -921
  14. package/dist/docx-redline.bundle.cjs.map +4 -4
  15. package/dist/docx-redline.bundle.js +4463 -921
  16. package/dist/docx-redline.bundle.js.map +4 -4
  17. package/docs/AGENT_FAST_START.md +2 -0
  18. package/docs/AGENT_KNOWLEDGE_BASE.md +49 -7
  19. package/docs/SKILL_AUTHORING.md +169 -150
  20. package/docs/TESTING.md +1146 -1059
  21. package/docs/schemas/document-operations.schema.json +55 -2
  22. package/document/docx-document.js +1022 -1010
  23. package/engine/oxml-engine.js +21 -22
  24. package/engine/paragraph-mark-revisions.js +292 -0
  25. package/engine/reconstruction-mapper.js +312 -311
  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 +53 -2
  32. package/index.js +13 -2
  33. package/node/cli-help.js +1 -0
  34. package/node/cli.js +21 -3
  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/markdown-processor.js +5 -2
  42. package/pipeline/pipeline.js +206 -204
  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 +23 -3
  52. package/services/capture-engine.js +228 -226
  53. package/services/comment-replies.js +1 -1
  54. package/services/document-operation-applier.js +214 -0
  55. package/services/document-operation-contract.js +76 -0
  56. package/services/document-operation-mutations.js +688 -51
  57. package/services/error-recovery.js +17 -0
  58. package/services/inspection-comparison.js +82 -0
  59. package/services/list-conversion-diagnostics.js +296 -0
  60. package/services/list-format-change.js +586 -0
  61. package/services/list-format-mutation.js +416 -0
  62. package/services/list-format.js +305 -0
  63. package/services/numbering-allocation.js +642 -0
  64. package/services/numbering-helpers.js +354 -524
  65. package/services/operation-batch-compiler.js +183 -11
  66. package/services/operation-preflight.js +43 -33
  67. package/services/receipt-collector.js +288 -288
  68. package/services/revision-comment-management.js +38 -5
  69. package/services/revision-fidelity-verifier.js +484 -0
  70. package/services/standalone-docx-plumbing.js +627 -631
  71. package/services/standalone-operation-runner.d.ts +182 -5
  72. package/services/table-reconciliation.js +59 -7
@@ -5,8 +5,9 @@
5
5
  */
6
6
 
7
7
  import { NS_W, RunKind, ContainerKind } from '../core/types.js';
8
+ import { getCurrentParagraphProperties } from '../core/current-properties.js';
8
9
  import { appendParagraphBoundary, advanceOffsetForParagraphBoundary } from '../core/paragraph-offset-policy.js';
9
- import { parseOoxmlSafe, serializeXml } from '../adapters/xml-adapter.js';
10
+ import { parseOoxmlSafe, serializeXml } from '../adapters/xml-adapter.js';
10
11
  import { warn, error as logError } from '../adapters/logger.js';
11
12
  import {
12
13
  getElementsByTagNS,
@@ -21,29 +22,29 @@ let containerIdCounter = 0;
21
22
  /**
22
23
  * Ingests OOXML paragraph content and builds a run-aware text model.
23
24
  *
24
- * @param {string} ooxmlString - OOXML input
25
- * @param {{ xmlDoc?: Document|null }} [options={}] - Optional pre-parsed XML document
26
- * @returns {import('../core/types.js').IngestionResult}
27
- */
28
- export function ingestOoxml(ooxmlString, options = {}) {
29
- const runModel = [];
30
- let acceptedText = '';
31
- const preParsedDoc = options.xmlDoc || null;
32
-
33
- if (!ooxmlString && !preParsedDoc) {
34
- return { runModel, acceptedText, pPr: null };
35
- }
36
-
37
- try {
38
- const parsed = preParsedDoc ? { doc: preParsedDoc, error: null } : parseOoxmlSafe(ooxmlString, 'application/xml');
39
- const doc = parsed.doc;
40
- if (parsed.error || !doc) {
41
- logError('OOXML parse error:', parsed.error?.message);
42
- return { runModel, acceptedText, pPr: null, error: parsed.error };
43
- }
44
-
45
- const parseError = getXmlParseError(doc);
46
- if (parseError) {
25
+ * @param {string} ooxmlString - OOXML input
26
+ * @param {{ xmlDoc?: Document|null }} [options={}] - Optional pre-parsed XML document
27
+ * @returns {import('../core/types.js').IngestionResult}
28
+ */
29
+ export function ingestOoxml(ooxmlString, options = {}) {
30
+ const runModel = [];
31
+ let acceptedText = '';
32
+ const preParsedDoc = options.xmlDoc || null;
33
+
34
+ if (!ooxmlString && !preParsedDoc) {
35
+ return { runModel, acceptedText, pPr: null };
36
+ }
37
+
38
+ try {
39
+ const parsed = preParsedDoc ? { doc: preParsedDoc, error: null } : parseOoxmlSafe(ooxmlString, 'application/xml');
40
+ const doc = parsed.doc;
41
+ if (parsed.error || !doc) {
42
+ logError('OOXML parse error:', parsed.error?.message);
43
+ return { runModel, acceptedText, pPr: null, error: parsed.error };
44
+ }
45
+
46
+ const parseError = getXmlParseError(doc);
47
+ if (parseError) {
47
48
  logError('OOXML parse error:', parseError.textContent);
48
49
  return { runModel, acceptedText, pPr: null };
49
50
  }
@@ -81,52 +82,46 @@ export function ingestParagraphElement(paragraphElement) {
81
82
  * @returns {Object|null}
82
83
  */
83
84
  export function detectNumberingContext(pElement) {
84
- const pPr = getFirstElementByTagNS(pElement, NS_W, 'pPr');
85
- if (!pPr) return null;
86
-
87
- const numPr = getFirstElementByTagNS(pPr, NS_W, 'numPr');
88
- if (!numPr) return null;
89
-
90
- const numIdEl = getFirstElementByTagNS(numPr, NS_W, 'numId');
91
- const ilvlEl = getFirstElementByTagNS(numPr, NS_W, 'ilvl');
92
- if (!numIdEl) return null;
85
+ // Current numPr only: a w:pPrChange snapshot must not make a historical list binding look current.
86
+ const { numPr } = getCurrentParagraphProperties(pElement);
87
+ if (!numPr || !numPr.numIdElement) return null;
93
88
 
94
- const numId = numIdEl.getAttribute('w:val');
95
- // In WordprocessingML, numId="0" explicitly removes numbering inherited
96
- // from a paragraph style. It is not a reusable list definition.
97
- if (!/^\d+$/.test(numId) || Number.parseInt(numId, 10) === 0) return null;
98
- const type = numId === '1' ? 'bullet' : (numId === '2' ? 'numbered' : 'unknown');
89
+ const numId = numPr.numId;
90
+ // In WordprocessingML, numId="0" explicitly removes numbering inherited
91
+ // from a paragraph style. It is not a reusable list definition.
92
+ if (!/^\d+$/.test(numId) || Number.parseInt(numId, 10) === 0) return null;
93
+ const type = numId === '1' ? 'bullet' : (numId === '2' ? 'numbered' : 'unknown');
99
94
 
100
95
  return {
101
96
  numId,
102
- ilvl: parseInt(ilvlEl?.getAttribute('w:val') || '0', 10),
97
+ ilvl: parseInt(numPr.ilvl || '0', 10),
103
98
  type
104
99
  };
105
100
  }
106
101
 
107
- function ingestParagraphElements(paragraphs, options = {}) {
102
+ function ingestParagraphElements(paragraphs, options = {}) {
108
103
  const includeParagraphBoundaries = options.includeParagraphBoundaries ?? true;
109
104
  const runModel = [];
110
105
  let acceptedText = '';
111
106
  let currentOffset = 0;
112
107
  let firstPPr = null;
113
108
 
114
- for (let pIndex = 0; pIndex < paragraphs.length; pIndex++) {
115
- const pElement = paragraphs[pIndex];
116
- const pPr = getFirstElementByTagNS(pElement, NS_W, 'pPr');
117
-
118
- if (pIndex === 0) {
119
- firstPPr = pPr;
120
- }
121
-
122
- runModel.push({
123
- kind: RunKind.PARAGRAPH_START,
124
- // Keep pPr as a DOM reference; serialize only when downstream actually needs string XML.
125
- pPrElement: pPr || null,
126
- startOffset: currentOffset,
127
- endOffset: currentOffset,
128
- text: ''
129
- });
109
+ for (let pIndex = 0; pIndex < paragraphs.length; pIndex++) {
110
+ const pElement = paragraphs[pIndex];
111
+ const pPr = getFirstElementByTagNS(pElement, NS_W, 'pPr');
112
+
113
+ if (pIndex === 0) {
114
+ firstPPr = pPr;
115
+ }
116
+
117
+ runModel.push({
118
+ kind: RunKind.PARAGRAPH_START,
119
+ // Keep pPr as a DOM reference; serialize only when downstream actually needs string XML.
120
+ pPrElement: pPr || null,
121
+ startOffset: currentOffset,
122
+ endOffset: currentOffset,
123
+ text: ''
124
+ });
130
125
 
131
126
  const result = processNodeRecursive(pElement, currentOffset, runModel);
132
127
  acceptedText += result.text;
@@ -222,36 +217,36 @@ function getNodeHandlers(runModel) {
222
217
  return contentResult;
223
218
  });
224
219
 
225
- handlers.set('del', (child, offset) => {
226
- const deletionEntry = processDeletion(child, offset);
227
- if (deletionEntry) {
228
- runModel.push(deletionEntry);
229
- }
230
- return { offset, text: '' };
231
- });
232
-
233
- handlers.set('moveFrom', (child, offset) => {
234
- const deletionEntry = processDeletion(child, offset);
235
- if (deletionEntry) {
236
- runModel.push(deletionEntry);
237
- }
238
- return { offset, text: '' };
239
- });
240
-
241
- handlers.set('moveTo', (child, offset) => processNodeRecursive(child, offset, runModel));
242
-
243
- for (const markerName of ['moveFromRangeStart', 'moveFromRangeEnd', 'moveToRangeStart', 'moveToRangeEnd']) {
244
- handlers.set(markerName, (child, offset) => {
245
- runModel.push({
246
- kind: RunKind.BOOKMARK,
247
- nodeXml: serializeXml(child),
248
- startOffset: offset,
249
- endOffset: offset,
250
- text: ''
251
- });
252
- return { offset, text: '' };
253
- });
254
- }
220
+ handlers.set('del', (child, offset) => {
221
+ const deletionEntry = processDeletion(child, offset);
222
+ if (deletionEntry) {
223
+ runModel.push(deletionEntry);
224
+ }
225
+ return { offset, text: '' };
226
+ });
227
+
228
+ handlers.set('moveFrom', (child, offset) => {
229
+ const deletionEntry = processDeletion(child, offset);
230
+ if (deletionEntry) {
231
+ runModel.push(deletionEntry);
232
+ }
233
+ return { offset, text: '' };
234
+ });
235
+
236
+ handlers.set('moveTo', (child, offset) => processNodeRecursive(child, offset, runModel));
237
+
238
+ for (const markerName of ['moveFromRangeStart', 'moveFromRangeEnd', 'moveToRangeStart', 'moveToRangeEnd']) {
239
+ handlers.set(markerName, (child, offset) => {
240
+ runModel.push({
241
+ kind: RunKind.BOOKMARK,
242
+ nodeXml: serializeXml(child),
243
+ startOffset: offset,
244
+ endOffset: offset,
245
+ text: ''
246
+ });
247
+ return { offset, text: '' };
248
+ });
249
+ }
255
250
 
256
251
  handlers.set('bookmarkStart', (child, offset) => {
257
252
  runModel.push({
@@ -332,11 +327,11 @@ function processRun(runElement, startOffset) {
332
327
  text += '\n';
333
328
  } else if (nodeName.endsWith(':tab') || nodeName === 'tab') {
334
329
  text += '\t';
335
- } else if (nodeName.endsWith(':noBreakHyphen') || nodeName === 'noBreakHyphen') {
336
- text += '\u2011';
337
- } else if (nodeName.endsWith(':softHyphen') || nodeName === 'softHyphen') {
338
- text += '\u00ad';
339
- }
330
+ } else if (nodeName.endsWith(':noBreakHyphen') || nodeName === 'noBreakHyphen') {
331
+ text += '\u2011';
332
+ } else if (nodeName.endsWith(':softHyphen') || nodeName === 'softHyphen') {
333
+ text += '\u00ad';
334
+ }
340
335
  }
341
336
 
342
337
  if (!text) return null;
@@ -350,13 +345,13 @@ function processRun(runElement, startOffset) {
350
345
  };
351
346
  }
352
347
 
353
- function processDeletion(delElement, offset) {
354
- const author = delElement.getAttribute('w:author') || '';
355
-
356
- // Deleted text is retained as a zero-width deletion model entry for revision-aware callers.
357
- // It is intentionally not added to acceptedText by the w:del handler above.
358
- let text = '';
359
- const delTexts = getElementsByTagNS(delElement, NS_W, 'delText');
348
+ function processDeletion(delElement, offset) {
349
+ const author = delElement.getAttribute('w:author') || '';
350
+
351
+ // Deleted text is retained as a zero-width deletion model entry for revision-aware callers.
352
+ // It is intentionally not added to acceptedText by the w:del handler above.
353
+ let text = '';
354
+ const delTexts = getElementsByTagNS(delElement, NS_W, 'delText');
360
355
  for (const delText of delTexts) {
361
356
  text += delText.textContent || '';
362
357
  }