@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.
Files changed (42) hide show
  1. package/AGENTS.md +91 -5
  2. package/ARCHITECTURE.md +62 -9
  3. package/README.md +94 -3
  4. package/core/types.js +35 -8
  5. package/core/word-xml.js +90 -0
  6. package/dist/docx-redline-js.esm.js +1149 -367
  7. package/dist/docx-redline-js.esm.js.map +4 -4
  8. package/dist/docx-redline-js.esm.min.js +78 -74
  9. package/dist/docx-redline-js.esm.min.js.map +4 -4
  10. package/docs/VALIDATION.md +48 -0
  11. package/docs/plans/2026-03-01-release-0.1.4-design.md +31 -0
  12. package/docs/plans/2026-03-01-release-0.1.4.md +108 -0
  13. package/engine/format-application.js +13 -14
  14. package/engine/format-span-application.js +7 -6
  15. package/engine/formatting-removal.js +15 -12
  16. package/engine/oxml-engine.js +146 -55
  17. package/engine/reconstruction-mapper.js +35 -8
  18. package/engine/reconstruction-mode.js +14 -13
  19. package/engine/reconstruction-writer.js +97 -78
  20. package/engine/rpr-helpers.js +34 -32
  21. package/engine/run-builders.js +150 -39
  22. package/engine/surgical-diff-application.js +216 -0
  23. package/engine/surgical-mode.js +84 -519
  24. package/engine/surgical-run-splitting.js +96 -0
  25. package/engine/surgical-spans.js +169 -0
  26. package/engine/table-cell-context.js +15 -13
  27. package/engine/table-mode.js +39 -35
  28. package/index.d.ts +148 -0
  29. package/index.js +26 -19
  30. package/package.json +8 -1
  31. package/pipeline/ingestion-export.js +1 -0
  32. package/pipeline/ingestion-paragraph.js +37 -12
  33. package/pipeline/ingestion-table.js +11 -8
  34. package/scripts/build.mjs +35 -0
  35. package/scripts/check-types.mjs +28 -0
  36. package/scripts/export-validation-fixtures.mjs +68 -0
  37. package/scripts/run-tests.mjs +43 -0
  38. package/scripts/word-com-smoke.ps1 +48 -0
  39. package/services/comment-locator.js +10 -9
  40. package/services/revision-comment-management.js +501 -0
  41. package/services/standalone-operation-runner.js +119 -69
  42. 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 { getElementsByTag } from '../core/xml-query.js';
12
-
13
- const W14_NS = 'http://schemas.microsoft.com/office/word/2010/wordml';
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 = getElementsByTag(xmlDoc, 'w:tbl');
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.nodeName === 'w:tc') return true;
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 = getElementsByTag(p, 'w:t');
73
+ const textNodes = getElementsByTagNSOrTag(p, NS_W, 't');
72
74
  let paragraphText = '';
73
75
  for (const t of textNodes) {
74
76
  paragraphText += t.textContent || '';
@@ -2,23 +2,26 @@
2
2
  * Table-specific reconciliation and transformation flows.
3
3
  */
4
4
 
5
- import { NS_W, getNextRevisionId, getRevisionTimestamp } from '../core/types.js';
6
- import {
7
- getElementsByTag,
8
- getElementsByTagNS,
9
- getFirstElementByTag,
10
- getFirstElementByTagNS,
11
- getXmlParseError
12
- } from '../core/xml-query.js';
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
- function noChanges(serializer, xmlDoc) {
20
- return { oxml: serializer.serializeToString(xmlDoc), hasChanges: false };
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 = getElementsByTag(xmlDoc, 'w:tbl');
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 = getFirstElementByTag(reconciledDoc, 'w:tbl');
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
- const date = getRevisionTimestamp();
140
- paragraphs.forEach(p => {
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.createElementNS(NS_W, 'w:delText');
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.createElementNS(NS_W, 'w:del');
154
- del.setAttribute('w:id', String(getNextRevisionId()));
155
- del.setAttribute('w:author', author);
156
- del.setAttribute('w:date', date);
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 {