@ansonlai/docx-redline-js 0.1.3 → 0.1.6
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 +91 -5
- package/ARCHITECTURE.md +62 -9
- package/README.md +94 -3
- package/core/types.js +35 -8
- package/core/word-xml.js +90 -0
- package/dist/docx-redline-js.esm.js +1149 -367
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +78 -74
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/VALIDATION.md +48 -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/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 +148 -0
- package/index.js +26 -19
- package/package.json +8 -1
- 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 +35 -0
- package/scripts/check-types.mjs +28 -0
- package/scripts/export-validation-fixtures.mjs +68 -0
- package/scripts/run-tests.mjs +43 -0
- package/scripts/word-com-smoke.ps1 +48 -0
- package/services/comment-locator.js +10 -9
- package/services/revision-comment-management.js +501 -0
- package/services/standalone-operation-runner.js +119 -69
- package/services/table-reconciliation.js +7 -8
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { createWordElement, isWordElement } from '../core/word-xml.js';
|
|
2
|
+
import { getRunChildText, isTextLikeRunChild } from './surgical-spans.js';
|
|
3
|
+
|
|
4
|
+
export function getRunContentPieces(runElement) {
|
|
5
|
+
const pieces = [];
|
|
6
|
+
let offset = 0;
|
|
7
|
+
|
|
8
|
+
for (const child of Array.from(runElement.childNodes || [])) {
|
|
9
|
+
if (isWordElement(child, 'rPr')) continue;
|
|
10
|
+
if (!isTextLikeRunChild(child)) continue;
|
|
11
|
+
|
|
12
|
+
const text = getRunChildText(child);
|
|
13
|
+
if (text.length === 0) continue;
|
|
14
|
+
|
|
15
|
+
pieces.push({
|
|
16
|
+
node: child,
|
|
17
|
+
start: offset,
|
|
18
|
+
end: offset + text.length,
|
|
19
|
+
text
|
|
20
|
+
});
|
|
21
|
+
offset += text.length;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return pieces;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getRunTextLength(pieces) {
|
|
28
|
+
if (pieces.length === 0) return 0;
|
|
29
|
+
return pieces[pieces.length - 1].end;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function sliceRunPieces(xmlDoc, pieces, start, end, asDeletedText) {
|
|
33
|
+
const sliced = [];
|
|
34
|
+
if (end <= start) return sliced;
|
|
35
|
+
|
|
36
|
+
pieces.forEach(piece => {
|
|
37
|
+
const overlapStart = Math.max(start, piece.start);
|
|
38
|
+
const overlapEnd = Math.min(end, piece.end);
|
|
39
|
+
if (overlapEnd <= overlapStart) return;
|
|
40
|
+
|
|
41
|
+
const localStart = overlapStart - piece.start;
|
|
42
|
+
const localEnd = overlapEnd - piece.start;
|
|
43
|
+
const text = piece.text.slice(localStart, localEnd);
|
|
44
|
+
|
|
45
|
+
sliced.push(cloneRunPiece(xmlDoc, piece.node, text, asDeletedText));
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return sliced;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createRunFromPieces(xmlDoc, pieces, rPr) {
|
|
52
|
+
const run = createWordElement(xmlDoc, 'w:r');
|
|
53
|
+
if (rPr) run.appendChild(rPr.cloneNode(true));
|
|
54
|
+
pieces.forEach(piece => run.appendChild(piece));
|
|
55
|
+
return run;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function insertRunPiecesBefore(xmlDoc, parent, referenceNode, pieces, rPr) {
|
|
59
|
+
if (pieces.length === 0) return null;
|
|
60
|
+
const run = createRunFromPieces(xmlDoc, pieces, rPr);
|
|
61
|
+
parent.insertBefore(run, referenceNode);
|
|
62
|
+
return run;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function cloneRunPiece(xmlDoc, sourceNode, text, asDeletedText) {
|
|
66
|
+
if (asDeletedText) {
|
|
67
|
+
const delText = createWordElement(xmlDoc, 'w:delText');
|
|
68
|
+
delText.setAttribute('xml:space', 'preserve');
|
|
69
|
+
delText.textContent = text;
|
|
70
|
+
return delText;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (isWordElement(sourceNode, 't')) {
|
|
74
|
+
const textNode = sourceNode.cloneNode(false);
|
|
75
|
+
textNode.textContent = text;
|
|
76
|
+
if (/^\s|\s$/.test(text)) {
|
|
77
|
+
textNode.setAttribute('xml:space', 'preserve');
|
|
78
|
+
}
|
|
79
|
+
return textNode;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (text === '\n' && (isWordElement(sourceNode, 'br') || isWordElement(sourceNode, 'cr'))) {
|
|
83
|
+
return sourceNode.cloneNode(true);
|
|
84
|
+
}
|
|
85
|
+
if (text === '\t' && isWordElement(sourceNode, 'tab')) {
|
|
86
|
+
return sourceNode.cloneNode(true);
|
|
87
|
+
}
|
|
88
|
+
if (text === '\u2011' && isWordElement(sourceNode, 'noBreakHyphen')) {
|
|
89
|
+
return sourceNode.cloneNode(true);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const textNode = createWordElement(xmlDoc, 'w:t');
|
|
93
|
+
textNode.setAttribute('xml:space', 'preserve');
|
|
94
|
+
textNode.textContent = text;
|
|
95
|
+
return textNode;
|
|
96
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { appendParagraphBoundary } from '../core/paragraph-offset-policy.js';
|
|
2
|
+
import { getFirstElementByTag } from '../core/xml-query.js';
|
|
3
|
+
import { isWordElement } from '../core/word-xml.js';
|
|
4
|
+
|
|
5
|
+
export function getRunChildText(child) {
|
|
6
|
+
if (isWordElement(child, 't')) return child.textContent || '';
|
|
7
|
+
if (isWordElement(child, 'br') || isWordElement(child, 'cr')) return '\n';
|
|
8
|
+
if (isWordElement(child, 'tab')) return '\t';
|
|
9
|
+
if (isWordElement(child, 'noBreakHyphen')) return '\u2011';
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isTextLikeRunChild(child) {
|
|
14
|
+
return isWordElement(child, 't')
|
|
15
|
+
|| isWordElement(child, 'br')
|
|
16
|
+
|| isWordElement(child, 'cr')
|
|
17
|
+
|| isWordElement(child, 'tab')
|
|
18
|
+
|| isWordElement(child, 'noBreakHyphen');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildSurgicalTextSpans(paragraphs) {
|
|
22
|
+
let fullText = '';
|
|
23
|
+
const textSpans = [];
|
|
24
|
+
|
|
25
|
+
paragraphs.forEach((paragraph, paragraphIndex) => {
|
|
26
|
+
const container = paragraph.parentNode;
|
|
27
|
+
|
|
28
|
+
for (let child = paragraph.firstChild; child; child = child.nextSibling) {
|
|
29
|
+
if (isWordElement(child, 'r')) {
|
|
30
|
+
fullText += processRunElement(child, paragraph, container, fullText.length, textSpans).text;
|
|
31
|
+
} else if (isWordElement(child, 'hyperlink')) {
|
|
32
|
+
for (let hc = child.firstChild; hc; hc = hc.nextSibling) {
|
|
33
|
+
if (isWordElement(hc, 'r')) {
|
|
34
|
+
fullText += processRunElement(hc, paragraph, container, fullText.length, textSpans).text;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fullText = appendParagraphBoundary(fullText, paragraphIndex, paragraphs.length);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return { fullText, textSpans };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function processRunElement(run, paragraph, container, currentOffset, textSpans) {
|
|
47
|
+
const rPr = getFirstElementByTag(run, 'w:rPr');
|
|
48
|
+
let localOffset = currentOffset;
|
|
49
|
+
const textParts = [];
|
|
50
|
+
|
|
51
|
+
for (let child = run.firstChild; child; child = child.nextSibling) {
|
|
52
|
+
if (isWordElement(child, 't')) {
|
|
53
|
+
const text = child.textContent || '';
|
|
54
|
+
if (text.length === 0) continue;
|
|
55
|
+
|
|
56
|
+
textSpans.push({
|
|
57
|
+
charStart: localOffset,
|
|
58
|
+
charEnd: localOffset + text.length,
|
|
59
|
+
textElement: child,
|
|
60
|
+
runElement: run,
|
|
61
|
+
paragraph,
|
|
62
|
+
container,
|
|
63
|
+
rPr
|
|
64
|
+
});
|
|
65
|
+
localOffset += text.length;
|
|
66
|
+
textParts.push(text);
|
|
67
|
+
} else if (isTextLikeRunChild(child)) {
|
|
68
|
+
const text = getRunChildText(child);
|
|
69
|
+
textSpans.push({
|
|
70
|
+
charStart: localOffset,
|
|
71
|
+
charEnd: localOffset + 1,
|
|
72
|
+
textElement: child,
|
|
73
|
+
runElement: run,
|
|
74
|
+
paragraph,
|
|
75
|
+
container,
|
|
76
|
+
rPr
|
|
77
|
+
});
|
|
78
|
+
localOffset += 1;
|
|
79
|
+
textParts.push(text);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { text: textParts.join('') };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function buildSpanIndex(textSpans) {
|
|
87
|
+
const spans = textSpans
|
|
88
|
+
.slice()
|
|
89
|
+
.sort((a, b) => a.charStart - b.charStart || a.charEnd - b.charEnd);
|
|
90
|
+
|
|
91
|
+
const starts = spans.map(span => span.charStart);
|
|
92
|
+
const ends = spans.map(span => span.charEnd);
|
|
93
|
+
|
|
94
|
+
return { spans, starts, ends };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function forEachOverlappingSpan(spanIndex, startPos, endPos, callback) {
|
|
98
|
+
if (endPos <= startPos || spanIndex.spans.length === 0) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let index = upperBound(spanIndex.ends, startPos);
|
|
103
|
+
while (index < spanIndex.spans.length) {
|
|
104
|
+
const span = spanIndex.spans[index];
|
|
105
|
+
if (span.charStart >= endPos) {
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
callback(span);
|
|
109
|
+
index++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function findContainingSpan(spanIndex, pos) {
|
|
114
|
+
if (spanIndex.spans.length === 0) return null;
|
|
115
|
+
|
|
116
|
+
const index = upperBound(spanIndex.starts, pos) - 1;
|
|
117
|
+
if (index < 0) return null;
|
|
118
|
+
|
|
119
|
+
const span = spanIndex.spans[index];
|
|
120
|
+
return pos >= span.charStart && pos < span.charEnd ? span : null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function findFirstSpanEndingAt(spanIndex, pos) {
|
|
124
|
+
const index = lowerBound(spanIndex.ends, pos);
|
|
125
|
+
if (index < spanIndex.spans.length && spanIndex.ends[index] === pos) {
|
|
126
|
+
return spanIndex.spans[index];
|
|
127
|
+
}
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function findLastSpanEndingBeforeOrAt(spanIndex, pos) {
|
|
132
|
+
const index = upperBound(spanIndex.ends, pos) - 1;
|
|
133
|
+
if (index >= 0) {
|
|
134
|
+
return spanIndex.spans[index];
|
|
135
|
+
}
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function upperBound(values, target) {
|
|
140
|
+
let left = 0;
|
|
141
|
+
let right = values.length;
|
|
142
|
+
|
|
143
|
+
while (left < right) {
|
|
144
|
+
const middle = (left + right) >> 1;
|
|
145
|
+
if (values[middle] <= target) {
|
|
146
|
+
left = middle + 1;
|
|
147
|
+
} else {
|
|
148
|
+
right = middle;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return left;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function lowerBound(values, target) {
|
|
156
|
+
let left = 0;
|
|
157
|
+
let right = values.length;
|
|
158
|
+
|
|
159
|
+
while (left < right) {
|
|
160
|
+
const middle = (left + right) >> 1;
|
|
161
|
+
if (values[middle] < target) {
|
|
162
|
+
left = middle + 1;
|
|
163
|
+
} else {
|
|
164
|
+
right = middle;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return left;
|
|
169
|
+
}
|
|
@@ -6,11 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { getDocumentParagraphs } from './format-extraction.js';
|
|
9
|
-
import { log } from '../adapters/logger.js';
|
|
10
|
-
import { buildParagraphOnlyPackage } from '../services/package-builder.js';
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
import { log } from '../adapters/logger.js';
|
|
10
|
+
import { buildParagraphOnlyPackage } from '../services/package-builder.js';
|
|
11
|
+
import { getElementsByTagNSOrTag } from '../core/xml-query.js';
|
|
12
|
+
import { NS_W } from '../core/types.js';
|
|
13
|
+
import { isWordElement } from '../core/word-xml.js';
|
|
14
|
+
|
|
15
|
+
const W14_NS = 'http://schemas.microsoft.com/office/word/2010/wordml';
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Detects whether the current XML is table-wrapped and resolves target paragraph context.
|
|
@@ -30,19 +32,19 @@ const W14_NS = 'http://schemas.microsoft.com/office/word/2010/wordml';
|
|
|
30
32
|
*/
|
|
31
33
|
export function detectTableCellContext(xmlDoc, originalText, options = {}) {
|
|
32
34
|
const { targetParagraphId = null } = options;
|
|
33
|
-
const tables =
|
|
35
|
+
const tables = getElementsByTagNSOrTag(xmlDoc, NS_W, 'tbl');
|
|
34
36
|
if (tables.length === 0) {
|
|
35
37
|
return { hasTableWrapper: false, isTableCellParagraph: false, paragraphs: [], paragraph: null, tableElement: null };
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
const allParagraphs = getDocumentParagraphs(xmlDoc);
|
|
39
41
|
const paragraphsInCells = allParagraphs.filter(p => {
|
|
40
|
-
let parent = p.parentNode;
|
|
41
|
-
while (parent) {
|
|
42
|
-
if (parent
|
|
43
|
-
parent = parent.parentNode;
|
|
44
|
-
}
|
|
45
|
-
return false;
|
|
42
|
+
let parent = p.parentNode;
|
|
43
|
+
while (parent) {
|
|
44
|
+
if (isWordElement(parent, 'tc')) return true;
|
|
45
|
+
parent = parent.parentNode;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
log(`[OxmlEngine] Table wrapper detected: ${tables.length} tables, ${paragraphsInCells.length} paragraphs in cells`);
|
|
@@ -68,7 +70,7 @@ export function detectTableCellContext(xmlDoc, originalText, options = {}) {
|
|
|
68
70
|
const normalizedTarget = originalText.trim();
|
|
69
71
|
if (!targetParagraph) {
|
|
70
72
|
for (const p of paragraphsInCells) {
|
|
71
|
-
const textNodes =
|
|
73
|
+
const textNodes = getElementsByTagNSOrTag(p, NS_W, 't');
|
|
72
74
|
let paragraphText = '';
|
|
73
75
|
for (const t of textNodes) {
|
|
74
76
|
paragraphText += t.textContent || '';
|
package/engine/table-mode.js
CHANGED
|
@@ -2,23 +2,26 @@
|
|
|
2
2
|
* Table-specific reconciliation and transformation flows.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { NS_W,
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
getFirstElementByTag,
|
|
10
|
-
getFirstElementByTagNS,
|
|
11
|
-
|
|
12
|
-
|
|
5
|
+
import { NS_W, createRevisionMetadata } from '../core/types.js';
|
|
6
|
+
import {
|
|
7
|
+
getElementsByTagNS,
|
|
8
|
+
getElementsByTagNSOrTag,
|
|
9
|
+
getFirstElementByTag,
|
|
10
|
+
getFirstElementByTagNS,
|
|
11
|
+
getFirstElementByTagNSOrTag,
|
|
12
|
+
getXmlParseError
|
|
13
|
+
} from '../core/xml-query.js';
|
|
13
14
|
import { createParser } from '../adapters/xml-adapter.js';
|
|
14
|
-
import { log, error } from '../adapters/logger.js';
|
|
15
|
-
import { diffTablesWithVirtualGrid, serializeVirtualGridToOoxml, generateTableOoxml } from '../services/table-reconciliation.js';
|
|
16
|
-
import { parseTable } from '../pipeline/pipeline.js';
|
|
17
|
-
import { ingestTableToVirtualGrid } from '../pipeline/ingestion.js';
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
import { log, error } from '../adapters/logger.js';
|
|
16
|
+
import { diffTablesWithVirtualGrid, serializeVirtualGridToOoxml, generateTableOoxml } from '../services/table-reconciliation.js';
|
|
17
|
+
import { parseTable } from '../pipeline/pipeline.js';
|
|
18
|
+
import { ingestTableToVirtualGrid } from '../pipeline/ingestion.js';
|
|
19
|
+
import { createWordElement, withOoxmlSourceType } from '../core/word-xml.js';
|
|
20
|
+
import { markParagraphMarkDeleted } from './run-builders.js';
|
|
21
|
+
|
|
22
|
+
function noChanges(serializer, xmlDoc) {
|
|
23
|
+
return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: false });
|
|
24
|
+
}
|
|
22
25
|
|
|
23
26
|
/**
|
|
24
27
|
* Applies structural reconciliation to tables using Virtual Grid.
|
|
@@ -31,8 +34,8 @@ function noChanges(serializer, xmlDoc) {
|
|
|
31
34
|
* @param {boolean} [generateRedlines=true] - Track change toggle
|
|
32
35
|
* @returns {{ oxml: string, hasChanges: boolean }}
|
|
33
36
|
*/
|
|
34
|
-
export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines = true) {
|
|
35
|
-
const tableNodes =
|
|
37
|
+
export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines = true) {
|
|
38
|
+
const tableNodes = getElementsByTagNSOrTag(xmlDoc, NS_W, 'tbl');
|
|
36
39
|
const newTableData = parseTable(modifiedText);
|
|
37
40
|
const hasNewContent = newTableData.rows.length > 0 || newTableData.headers.length > 0;
|
|
38
41
|
|
|
@@ -60,7 +63,7 @@ export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parse
|
|
|
60
63
|
return noChanges(serializer, xmlDoc);
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
const newTableNode =
|
|
66
|
+
const newTableNode = getFirstElementByTagNSOrTag(reconciledDoc, NS_W, 'tbl');
|
|
64
67
|
if (!newTableNode) {
|
|
65
68
|
error('[OxmlEngine] No table found in reconciled OOXML');
|
|
66
69
|
return noChanges(serializer, xmlDoc);
|
|
@@ -69,7 +72,7 @@ export function applyTableReconciliation(xmlDoc, modifiedText, serializer, parse
|
|
|
69
72
|
const importedTable = xmlDoc.importNode(newTableNode, true);
|
|
70
73
|
targetTable.parentNode.replaceChild(importedTable, targetTable);
|
|
71
74
|
|
|
72
|
-
return { oxml: serializer.serializeToString(xmlDoc), hasChanges: true };
|
|
75
|
+
return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: true });
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
/**
|
|
@@ -135,25 +138,26 @@ export function applyTextToTableTransformation(xmlDoc, modifiedText, serializer,
|
|
|
135
138
|
|
|
136
139
|
const importedTable = workingDoc.importNode(newTableElement, true);
|
|
137
140
|
|
|
138
|
-
if (generateRedlines) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
const runs = getElementsByTagNS(p, NS_W, 'r');
|
|
142
|
-
runs.forEach(run => {
|
|
141
|
+
if (generateRedlines) {
|
|
142
|
+
paragraphs.forEach(p => {
|
|
143
|
+
markParagraphMarkDeleted(workingDoc, p, author);
|
|
144
|
+
const runs = getElementsByTagNS(p, NS_W, 'r');
|
|
145
|
+
runs.forEach(run => {
|
|
143
146
|
const textNodes = getElementsByTagNS(run, NS_W, 't');
|
|
144
147
|
textNodes.forEach(t => {
|
|
145
148
|
const text = t.textContent || '';
|
|
146
149
|
if (text.trim()) {
|
|
147
|
-
const delText = workingDoc
|
|
148
|
-
delText.textContent = text;
|
|
149
|
-
t.parentNode.replaceChild(delText, t);
|
|
150
|
+
const delText = createWordElement(workingDoc, 'w:delText');
|
|
151
|
+
delText.textContent = text;
|
|
152
|
+
t.parentNode.replaceChild(delText, t);
|
|
150
153
|
}
|
|
151
154
|
});
|
|
152
|
-
|
|
153
|
-
const del = workingDoc
|
|
154
|
-
|
|
155
|
-
del.setAttribute('w:
|
|
156
|
-
del.setAttribute('w:
|
|
155
|
+
|
|
156
|
+
const del = createWordElement(workingDoc, 'w:del');
|
|
157
|
+
const metadata = createRevisionMetadata(author);
|
|
158
|
+
del.setAttribute('w:id', String(metadata.id));
|
|
159
|
+
del.setAttribute('w:author', metadata.author);
|
|
160
|
+
del.setAttribute('w:date', metadata.date);
|
|
157
161
|
run.parentNode.insertBefore(del, run);
|
|
158
162
|
del.appendChild(run);
|
|
159
163
|
});
|
|
@@ -168,5 +172,5 @@ export function applyTextToTableTransformation(xmlDoc, modifiedText, serializer,
|
|
|
168
172
|
}
|
|
169
173
|
|
|
170
174
|
log('[OxmlEngine] Text-to-table transformation complete');
|
|
171
|
-
return { oxml: serializer.serializeToString(workingDoc), hasChanges: true };
|
|
172
|
-
}
|
|
175
|
+
return withOoxmlSourceType({ oxml: serializer.serializeToString(workingDoc), hasChanges: true });
|
|
176
|
+
}
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export type OoxmlSourceType = 'package' | 'document' | 'fragment';
|
|
2
|
+
export type RedlineStatus = 'ok' | 'no-op' | 'error';
|
|
3
|
+
export type ExistingRevisionsPolicy = 'reject-input' | 'accept-all-first';
|
|
4
|
+
|
|
5
|
+
export interface RedlineError {
|
|
6
|
+
code: 'PARSE_ERROR' | 'TARGET_NOT_FOUND' | 'EXISTING_REVISIONS' | string;
|
|
7
|
+
message: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface RedlineOptions {
|
|
11
|
+
generateRedlines?: boolean;
|
|
12
|
+
author?: string;
|
|
13
|
+
targetParagraphId?: string | null;
|
|
14
|
+
existingRevisions?: ExistingRevisionsPolicy;
|
|
15
|
+
[key: string]: unknown;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RedlineResult {
|
|
19
|
+
oxml: string;
|
|
20
|
+
hasChanges: boolean;
|
|
21
|
+
sourceType?: OoxmlSourceType;
|
|
22
|
+
status?: RedlineStatus;
|
|
23
|
+
error?: RedlineError;
|
|
24
|
+
warnings?: string[];
|
|
25
|
+
numberingXml?: string;
|
|
26
|
+
useNativeApi?: boolean;
|
|
27
|
+
[key: string]: unknown;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface TableReconciliationResult extends RedlineResult {
|
|
31
|
+
isMarkdownTable: boolean;
|
|
32
|
+
tableData?: {
|
|
33
|
+
headers?: string[];
|
|
34
|
+
rows?: string[][];
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface RevisionFilterOptions {
|
|
40
|
+
author?: string;
|
|
41
|
+
allAuthors?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface AcceptTrackedChangesResult {
|
|
45
|
+
oxml: string;
|
|
46
|
+
hasChanges: boolean;
|
|
47
|
+
acceptedCount: number;
|
|
48
|
+
warnings: string[];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RejectTrackedChangesResult {
|
|
52
|
+
oxml: string;
|
|
53
|
+
hasChanges: boolean;
|
|
54
|
+
rejectedCount: number;
|
|
55
|
+
warnings: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface DeleteCommentsResult {
|
|
59
|
+
oxml: string;
|
|
60
|
+
hasChanges: boolean;
|
|
61
|
+
commentsRemoved: number;
|
|
62
|
+
referencesRemoved: number;
|
|
63
|
+
warnings: string[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface XmlProvider {
|
|
67
|
+
DOMParser: typeof DOMParser;
|
|
68
|
+
XMLSerializer: typeof XMLSerializer;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface LoggerConfig {
|
|
72
|
+
log?: (...args: unknown[]) => void;
|
|
73
|
+
warn?: (...args: unknown[]) => void;
|
|
74
|
+
error?: (...args: unknown[]) => void;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface DocumentOperationResult {
|
|
78
|
+
documentXml: string;
|
|
79
|
+
hasChanges: boolean;
|
|
80
|
+
numberingXml?: string | null;
|
|
81
|
+
commentsXml?: string | null;
|
|
82
|
+
warnings?: string[];
|
|
83
|
+
status?: RedlineStatus;
|
|
84
|
+
error?: RedlineError;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function configureXmlProvider(provider: XmlProvider): void;
|
|
88
|
+
export function configureLogger(logger: LoggerConfig): void;
|
|
89
|
+
export function setDefaultAuthor(name: string): void;
|
|
90
|
+
export function getDefaultAuthor(): string;
|
|
91
|
+
export function setPlatform(label: string): void;
|
|
92
|
+
export function getPlatform(): string;
|
|
93
|
+
|
|
94
|
+
export function applyRedlineToOxml(
|
|
95
|
+
oxml: string,
|
|
96
|
+
originalText: string,
|
|
97
|
+
modifiedText: string,
|
|
98
|
+
options?: RedlineOptions
|
|
99
|
+
): Promise<RedlineResult>;
|
|
100
|
+
|
|
101
|
+
export function applyRedlineToOxmlWithListFallback(
|
|
102
|
+
oxml: string,
|
|
103
|
+
originalText: string,
|
|
104
|
+
modifiedText: string,
|
|
105
|
+
options?: RedlineOptions
|
|
106
|
+
): Promise<RedlineResult>;
|
|
107
|
+
|
|
108
|
+
export function reconcileMarkdownTableOoxml(
|
|
109
|
+
oxml: string,
|
|
110
|
+
originalText: string,
|
|
111
|
+
markdownTable: string,
|
|
112
|
+
options?: RedlineOptions
|
|
113
|
+
): Promise<TableReconciliationResult>;
|
|
114
|
+
|
|
115
|
+
export function ingestWordOoxmlToPlainText(oxml: string): string;
|
|
116
|
+
export function ingestWordOoxmlToMarkdown(oxml: string): string;
|
|
117
|
+
export function ingestOoxml(oxml: string): unknown;
|
|
118
|
+
export function preprocessMarkdown(text: string): { cleanText: string; formatHints: unknown[] };
|
|
119
|
+
export function serializeToOoxml(runModel: unknown[], pPrXml?: string | null, formatHints?: unknown[], options?: Record<string, unknown>): string;
|
|
120
|
+
export function wrapInDocumentFragment(rawOoxml: string, options?: Record<string, unknown>): string;
|
|
121
|
+
|
|
122
|
+
export function injectCommentsIntoOoxml(oxml: string, comments: unknown[], options?: Record<string, unknown>): unknown;
|
|
123
|
+
export function injectCommentsIntoPackage(packageXml: string, comments: unknown[], options?: Record<string, unknown>): unknown;
|
|
124
|
+
export function acceptTrackedChangesInOoxml(oxml: string, options?: RevisionFilterOptions): AcceptTrackedChangesResult;
|
|
125
|
+
export function rejectTrackedChangesInOoxml(oxml: string, options?: RevisionFilterOptions): RejectTrackedChangesResult;
|
|
126
|
+
export function deleteCommentsByAuthorInOoxml(oxml: string, options?: RevisionFilterOptions): DeleteCommentsResult;
|
|
127
|
+
export function containsTrackedChanges(xmlDoc: Document | Element): boolean;
|
|
128
|
+
|
|
129
|
+
export function applyHighlightToOoxml(oxml: string, targetText: string, color: string, options?: Record<string, unknown>): string;
|
|
130
|
+
export function generateTableOoxml(headersOrData: unknown, rowsOrOptions?: unknown, options?: Record<string, unknown>): string;
|
|
131
|
+
export function extractReplacementNodesFromOoxml(oxml: string): unknown;
|
|
132
|
+
export function validateDocxPackage(zip: unknown): Promise<unknown> | unknown;
|
|
133
|
+
export function ensureNumberingArtifactsInZip(zip: unknown, numberingXml: string): Promise<unknown> | unknown;
|
|
134
|
+
export function ensureCommentsArtifactsInZip(zip: unknown, commentsXml: string): Promise<unknown> | unknown;
|
|
135
|
+
export function createDynamicNumberingIdState(numberingXml?: string): unknown;
|
|
136
|
+
|
|
137
|
+
export function parseOoxml(ooxml: string): Document;
|
|
138
|
+
export function serializeOoxml(node: Node): string;
|
|
139
|
+
export function sanitizeAiResponse(text: string): string;
|
|
140
|
+
|
|
141
|
+
export class ReconciliationPipeline {
|
|
142
|
+
constructor(options?: Record<string, unknown>);
|
|
143
|
+
execute(oxml: string, modifiedText: string, options?: Record<string, unknown>): Promise<unknown>;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export class NumberingService {
|
|
147
|
+
constructor(...args: unknown[]);
|
|
148
|
+
}
|
package/index.js
CHANGED
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
enforceListBindingOnParagraphNodes,
|
|
26
26
|
stripSingleLineListMarkerPrefix
|
|
27
27
|
} from './orchestration/list-structural-fallback.js';
|
|
28
|
+
import { withOoxmlSourceType } from './core/word-xml.js';
|
|
29
|
+
export { containsTrackedChanges } from './core/word-xml.js';
|
|
28
30
|
|
|
29
31
|
/**
|
|
30
32
|
* Standalone-safe redline wrapper.
|
|
@@ -37,15 +39,15 @@ export async function applyRedlineToOxml(oxml, originalText, modifiedText, optio
|
|
|
37
39
|
const result = await applyRedlineToOxmlEngine(oxml, originalText, modifiedText, options);
|
|
38
40
|
if (result?.useNativeApi && typeof result?.oxml !== 'string') {
|
|
39
41
|
const existingWarnings = Array.isArray(result?.warnings) ? result.warnings : [];
|
|
40
|
-
return {
|
|
41
|
-
...result,
|
|
42
|
-
oxml,
|
|
43
|
-
hasChanges: false,
|
|
44
|
-
warnings: [
|
|
45
|
-
...existingWarnings,
|
|
46
|
-
'Standalone mode cannot execute native Word API fallback for this operation.'
|
|
47
|
-
]
|
|
48
|
-
};
|
|
42
|
+
return withOoxmlSourceType({
|
|
43
|
+
...result,
|
|
44
|
+
oxml,
|
|
45
|
+
hasChanges: false,
|
|
46
|
+
warnings: [
|
|
47
|
+
...existingWarnings,
|
|
48
|
+
'Standalone mode cannot execute native Word API fallback for this operation.'
|
|
49
|
+
]
|
|
50
|
+
});
|
|
49
51
|
}
|
|
50
52
|
return result;
|
|
51
53
|
}
|
|
@@ -137,14 +139,14 @@ export async function applyRedlineToOxmlWithListFallback(oxml, originalText, mod
|
|
|
137
139
|
numberingXml: fallbackResult.numberingXml
|
|
138
140
|
});
|
|
139
141
|
const fallbackWarnings = Array.isArray(fallbackResult?.warnings) ? fallbackResult.warnings : [];
|
|
140
|
-
return {
|
|
142
|
+
return withOoxmlSourceType({
|
|
141
143
|
oxml: wrappedOxml,
|
|
142
144
|
hasChanges: true,
|
|
143
145
|
warnings: fallbackWarnings,
|
|
144
146
|
listStructuralFallbackApplied: true,
|
|
145
147
|
listStructuralFallbackKey: fallbackResult.listStructuralFallbackKey || null,
|
|
146
148
|
listStructuralFallbackNumberingXml: fallbackResult.numberingXml || null
|
|
147
|
-
};
|
|
149
|
+
});
|
|
148
150
|
}
|
|
149
151
|
preflightFallbackWarnings = Array.isArray(fallbackResult?.warnings) ? fallbackResult.warnings : [];
|
|
150
152
|
}
|
|
@@ -202,7 +204,7 @@ export async function applyRedlineToOxmlWithListFallback(oxml, originalText, mod
|
|
|
202
204
|
const existingWarnings = Array.isArray(baseResult?.warnings) ? baseResult.warnings : [];
|
|
203
205
|
const fallbackWarnings = Array.isArray(fallbackResult?.warnings) ? fallbackResult.warnings : [];
|
|
204
206
|
|
|
205
|
-
return {
|
|
207
|
+
return withOoxmlSourceType({
|
|
206
208
|
...baseResult,
|
|
207
209
|
oxml: wrappedOxml,
|
|
208
210
|
hasChanges: true,
|
|
@@ -210,7 +212,7 @@ export async function applyRedlineToOxmlWithListFallback(oxml, originalText, mod
|
|
|
210
212
|
listStructuralFallbackApplied: true,
|
|
211
213
|
listStructuralFallbackKey: fallbackResult.listStructuralFallbackKey || null,
|
|
212
214
|
listStructuralFallbackNumberingXml: fallbackResult.numberingXml || null
|
|
213
|
-
};
|
|
215
|
+
});
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
export { sanitizeAiResponse, parseOoxml, serializeOoxml };
|
|
@@ -234,12 +236,17 @@ export { preprocessMarkdown } from './pipeline/markdown-processor.js';
|
|
|
234
236
|
export { serializeToOoxml, wrapInDocumentFragment } from './pipeline/serialization.js';
|
|
235
237
|
|
|
236
238
|
// Comment engine
|
|
237
|
-
export {
|
|
238
|
-
injectCommentsIntoOoxml,
|
|
239
|
-
injectCommentsIntoPackage,
|
|
240
|
-
buildCommentElement,
|
|
241
|
-
buildCommentsPartXml
|
|
242
|
-
} from './services/comment-engine.js';
|
|
239
|
+
export {
|
|
240
|
+
injectCommentsIntoOoxml,
|
|
241
|
+
injectCommentsIntoPackage,
|
|
242
|
+
buildCommentElement,
|
|
243
|
+
buildCommentsPartXml
|
|
244
|
+
} from './services/comment-engine.js';
|
|
245
|
+
export {
|
|
246
|
+
acceptTrackedChangesInOoxml,
|
|
247
|
+
rejectTrackedChangesInOoxml,
|
|
248
|
+
deleteCommentsByAuthorInOoxml
|
|
249
|
+
} from './services/revision-comment-management.js';
|
|
243
250
|
|
|
244
251
|
// Formatting removal utilities
|
|
245
252
|
export {
|