@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.
- package/ARCHITECTURE.md +679 -460
- package/CHANGELOG.md +208 -1
- package/README.md +152 -16
- 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/core/word-xml.js +160 -118
- package/dist/docx-redline-js.esm.js +4338 -796
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +111 -108
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/dist/docx-redline.bundle.cjs +4463 -921
- package/dist/docx-redline.bundle.cjs.map +4 -4
- package/dist/docx-redline.bundle.js +4463 -921
- package/dist/docx-redline.bundle.js.map +4 -4
- package/docs/AGENT_FAST_START.md +2 -0
- package/docs/AGENT_KNOWLEDGE_BASE.md +49 -7
- package/docs/SKILL_AUTHORING.md +169 -150
- package/docs/TESTING.md +1146 -1059
- package/docs/schemas/document-operations.schema.json +55 -2
- package/document/docx-document.js +1022 -1010
- package/engine/oxml-engine.js +21 -22
- package/engine/paragraph-mark-revisions.js +292 -0
- package/engine/reconstruction-mapper.js +312 -311
- 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 +53 -2
- package/index.js +13 -2
- package/node/cli-help.js +1 -0
- package/node/cli.js +21 -3
- 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/markdown-processor.js +5 -2
- package/pipeline/pipeline.js +206 -204
- 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 +23 -3
- package/services/capture-engine.js +228 -226
- package/services/comment-replies.js +1 -1
- package/services/document-operation-applier.js +214 -0
- package/services/document-operation-contract.js +76 -0
- package/services/document-operation-mutations.js +688 -51
- package/services/error-recovery.js +17 -0
- package/services/inspection-comparison.js +82 -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 +183 -11
- package/services/operation-preflight.js +43 -33
- package/services/receipt-collector.js +288 -288
- package/services/revision-comment-management.js +38 -5
- package/services/revision-fidelity-verifier.js +484 -0
- package/services/standalone-docx-plumbing.js +627 -631
- package/services/standalone-operation-runner.d.ts +182 -5
- 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
|
-
|
|
85
|
-
|
|
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 =
|
|
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(
|
|
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
|
}
|