@ansonlai/docx-redline-js 0.1.4 → 0.2.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 +53 -4
- package/ARCHITECTURE.md +75 -11
- package/README.md +62 -3
- package/core/redline-validation.js +156 -0
- package/core/types.js +35 -8
- package/core/word-xml.js +90 -0
- package/dist/docx-redline-js.esm.js +3195 -2592
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +71 -67
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/VALIDATION.md +104 -0
- package/docs/plans/2026-03-01-release-0.1.4-design.md +31 -0
- package/docs/plans/2026-03-01-release-0.1.4.md +108 -0
- package/docs/plans/2026-05-31-architectural changes.md +591 -0
- package/engine/format-application.js +13 -14
- package/engine/format-span-application.js +7 -6
- package/engine/formatting-removal.js +15 -12
- package/engine/oxml-engine.js +146 -55
- package/engine/reconstruction-mapper.js +35 -8
- package/engine/reconstruction-mode.js +14 -13
- package/engine/reconstruction-writer.js +97 -78
- package/engine/rpr-helpers.js +34 -32
- package/engine/run-builders.js +150 -39
- package/engine/surgical-diff-application.js +216 -0
- package/engine/surgical-mode.js +84 -519
- package/engine/surgical-run-splitting.js +96 -0
- package/engine/surgical-spans.js +169 -0
- package/engine/table-cell-context.js +15 -13
- package/engine/table-mode.js +39 -35
- package/index.d.ts +172 -0
- package/index.js +50 -47
- package/package.json +10 -2
- package/pipeline/ingestion-export.js +1 -0
- package/pipeline/ingestion-paragraph.js +37 -12
- package/pipeline/ingestion-table.js +11 -8
- package/scripts/build.mjs +40 -0
- package/scripts/check-types.mjs +29 -0
- package/scripts/export-validation-fixtures.mjs +125 -0
- package/scripts/lib/minimal-zip.mjs +155 -0
- package/scripts/run-tests.mjs +43 -0
- package/scripts/validate-fixtures-xsd.sh +37 -0
- package/scripts/word-com-differential.ps1 +133 -0
- package/scripts/word-com-smoke.ps1 +48 -0
- package/services/comment-locator.js +10 -9
- package/services/revision-comment-management.js +115 -1
- package/services/standalone-operation-runner.js +119 -69
- package/services/table-reconciliation.js +7 -8
package/core/word-xml.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { NS_W } from './types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns true when a node is a WordprocessingML element with the given local name.
|
|
5
|
+
*
|
|
6
|
+
* @param {Node|null|undefined} node - Candidate node
|
|
7
|
+
* @param {string} localName - Word local name, for example `r` or `tbl`
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function isWordElement(node, localName) {
|
|
11
|
+
if (!node || node.nodeType !== 1) return false;
|
|
12
|
+
if (node.namespaceURI === NS_W && node.localName === localName) return true;
|
|
13
|
+
const nodeName = String(node.nodeName || '');
|
|
14
|
+
return nodeName === `w:${localName}` || nodeName === localName;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Creates a WordprocessingML element using namespace-aware DOM APIs when available.
|
|
19
|
+
*
|
|
20
|
+
* @param {Document} xmlDoc - Target document
|
|
21
|
+
* @param {string} qualifiedName - Qualified name, for example `w:r`
|
|
22
|
+
* @returns {Element}
|
|
23
|
+
*/
|
|
24
|
+
export function createWordElement(xmlDoc, qualifiedName) {
|
|
25
|
+
return typeof xmlDoc.createElementNS === 'function'
|
|
26
|
+
? xmlDoc.createElementNS(NS_W, qualifiedName)
|
|
27
|
+
: xmlDoc.createElement(qualifiedName);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function wordElementsByLocalName(xmlDoc, localName) {
|
|
31
|
+
const namespaced = Array.from(xmlDoc?.getElementsByTagNameNS?.(NS_W, localName) || []);
|
|
32
|
+
if (namespaced.length > 0) return namespaced;
|
|
33
|
+
return Array.from(xmlDoc?.getElementsByTagName?.('*') || []).filter(node => isWordElement(node, localName));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Returns true if a document or fragment contains Word tracked-change markup.
|
|
38
|
+
*
|
|
39
|
+
* @param {Document|Element} xmlDoc - Parsed OOXML document or element
|
|
40
|
+
* @returns {boolean}
|
|
41
|
+
*/
|
|
42
|
+
export function containsTrackedChanges(xmlDoc) {
|
|
43
|
+
const trackedChangeNames = [
|
|
44
|
+
'ins',
|
|
45
|
+
'del',
|
|
46
|
+
'moveFrom',
|
|
47
|
+
'moveTo',
|
|
48
|
+
'moveFromRangeStart',
|
|
49
|
+
'moveFromRangeEnd',
|
|
50
|
+
'moveToRangeStart',
|
|
51
|
+
'moveToRangeEnd',
|
|
52
|
+
'rPrChange',
|
|
53
|
+
'pPrChange',
|
|
54
|
+
'cellIns',
|
|
55
|
+
'cellDel'
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
return trackedChangeNames.some(localName => wordElementsByLocalName(xmlDoc, localName).length > 0);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Classifies the shape of an OOXML payload.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} oxml - OOXML payload
|
|
65
|
+
* @returns {'package'|'document'|'fragment'}
|
|
66
|
+
*/
|
|
67
|
+
export function classifyOoxmlSourceType(oxml) {
|
|
68
|
+
const trimmed = String(oxml || '').trim();
|
|
69
|
+
if (/^<\?xml\b[^>]*>\s*<pkg:package\b/i.test(trimmed) || /^<pkg:package\b/i.test(trimmed)) {
|
|
70
|
+
return 'package';
|
|
71
|
+
}
|
|
72
|
+
if (/^<\?xml\b[^>]*>\s*<(?:w:)?document\b/i.test(trimmed) || /^<(?:w:)?document\b/i.test(trimmed)) {
|
|
73
|
+
return 'document';
|
|
74
|
+
}
|
|
75
|
+
return 'fragment';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Adds `sourceType` metadata to OOXML result objects without changing payloads.
|
|
80
|
+
*
|
|
81
|
+
* @template T
|
|
82
|
+
* @param {T & { oxml?: string, sourceType?: 'package'|'document'|'fragment' }} result - Result object
|
|
83
|
+
* @returns {T & { sourceType?: 'package'|'document'|'fragment' }}
|
|
84
|
+
*/
|
|
85
|
+
export function withOoxmlSourceType(result) {
|
|
86
|
+
if (!result || typeof result !== 'object' || result.sourceType || typeof result.oxml !== 'string') {
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
return { ...result, sourceType: classifyOoxmlSourceType(result.oxml) };
|
|
90
|
+
}
|