@ansonlai/docx-redline-js 0.1.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 (64) hide show
  1. package/AGENTS.md +176 -0
  2. package/ARCHITECTURE.md +121 -0
  3. package/LICENSE +21 -0
  4. package/README.md +177 -0
  5. package/adapters/config.js +43 -0
  6. package/adapters/logger.js +89 -0
  7. package/adapters/xml-adapter.js +74 -0
  8. package/core/list-targeting.js +398 -0
  9. package/core/ooxml-identifiers.js +15 -0
  10. package/core/paragraph-offset-policy.js +50 -0
  11. package/core/paragraph-targeting.js +501 -0
  12. package/core/table-targeting.js +233 -0
  13. package/core/types.js +204 -0
  14. package/core/xml-query.js +99 -0
  15. package/dist/docx-redline-js.esm.js +8801 -0
  16. package/dist/docx-redline-js.esm.js.map +7 -0
  17. package/dist/docx-redline-js.esm.min.js +195 -0
  18. package/dist/docx-redline-js.esm.min.js.map +7 -0
  19. package/engine/format-application.js +358 -0
  20. package/engine/format-extraction.js +232 -0
  21. package/engine/format-paragraph-targeting.js +208 -0
  22. package/engine/format-span-application.js +178 -0
  23. package/engine/formatting-removal.js +330 -0
  24. package/engine/oxml-engine.js +279 -0
  25. package/engine/reconstruction-mapper.js +270 -0
  26. package/engine/reconstruction-mode.js +38 -0
  27. package/engine/reconstruction-writer.js +276 -0
  28. package/engine/rpr-helpers.js +194 -0
  29. package/engine/run-builders.js +235 -0
  30. package/engine/surgical-mode.js +520 -0
  31. package/engine/table-cell-context.js +151 -0
  32. package/engine/table-mode.js +172 -0
  33. package/index.js +308 -0
  34. package/orchestration/list-markdown.js +141 -0
  35. package/orchestration/list-parsing.js +73 -0
  36. package/orchestration/list-structural-fallback.js +530 -0
  37. package/orchestration/redline-operation-converter.js +141 -0
  38. package/orchestration/route-plan.js +160 -0
  39. package/package.json +76 -0
  40. package/pipeline/content-analysis.js +107 -0
  41. package/pipeline/diff-engine.js +204 -0
  42. package/pipeline/ingestion-export.js +255 -0
  43. package/pipeline/ingestion-paragraph.js +351 -0
  44. package/pipeline/ingestion-table.js +169 -0
  45. package/pipeline/ingestion-xml.js +39 -0
  46. package/pipeline/ingestion.js +8 -0
  47. package/pipeline/list-generation.js +280 -0
  48. package/pipeline/list-markers.js +77 -0
  49. package/pipeline/markdown-processor.js +160 -0
  50. package/pipeline/patching.js +408 -0
  51. package/pipeline/pipeline.js +326 -0
  52. package/pipeline/serialization.js +395 -0
  53. package/services/browser-demo-prompt-context.js +345 -0
  54. package/services/comment-builders.js +60 -0
  55. package/services/comment-engine.js +248 -0
  56. package/services/comment-locator.js +197 -0
  57. package/services/comment-package.js +113 -0
  58. package/services/numbering-helpers.js +416 -0
  59. package/services/numbering-service.js +290 -0
  60. package/services/package-builder.js +147 -0
  61. package/services/standalone-docx-plumbing.js +443 -0
  62. package/services/standalone-operation-runner.js +1169 -0
  63. package/services/table-reconciliation.js +344 -0
  64. package/standalone.js +5 -0
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Run property (w:rPr) helper utilities.
3
+ *
4
+ * This module owns low-level formatting element operations, including
5
+ * schema-order insertion, format extraction, and format add/remove transforms.
6
+ */
7
+
8
+ /**
9
+ * Canonical OOXML run-property schema ordering.
10
+ * Shared by all rPr synchronizers.
11
+ */
12
+ export const RPR_SCHEMA_ORDER = [
13
+ 'w:rStyle', 'w:rFonts', 'w:b', 'w:bCs', 'w:i', 'w:iCs', 'w:caps', 'w:smallCaps',
14
+ 'w:strike', 'w:dstrike', 'w:outline', 'w:shadow', 'w:emboss', 'w:imprint', 'w:noProof',
15
+ 'w:snapToGrid', 'w:vanish', 'w:webHidden', 'w:color', 'w:spacing', 'w:w', 'w:kern',
16
+ 'w:position', 'w:sz', 'w:szCs', 'w:highlight', 'w:u', 'w:effect', 'w:bdr', 'w:shd',
17
+ 'w:fitText', 'w:vertAlign', 'w:rtl', 'w:cs', 'w:em', 'w:lang', 'w:eastAsianLayout',
18
+ 'w:specVanish', 'w:oMath'
19
+ ];
20
+
21
+ /**
22
+ * Inserts an rPr child node in schema order.
23
+ *
24
+ * @param {Element} rPr - Run properties element
25
+ * @param {Element} el - Child element to insert
26
+ */
27
+ export function insertRPrChildInOrder(rPr, el) {
28
+ const myIndex = RPR_SCHEMA_ORDER.indexOf(el.nodeName);
29
+ const myPriority = myIndex === -1 ? 999 : myIndex;
30
+
31
+ let inserted = false;
32
+ for (const child of Array.from(rPr.childNodes)) {
33
+ if (child.nodeType !== 1) continue;
34
+ const childIndex = RPR_SCHEMA_ORDER.indexOf(child.nodeName);
35
+ const childPriority = childIndex === -1 ? 999 : childIndex;
36
+ if (childPriority > myPriority) {
37
+ rPr.insertBefore(el, child);
38
+ inserted = true;
39
+ break;
40
+ }
41
+ }
42
+ if (!inserted) rPr.appendChild(el);
43
+ }
44
+
45
+ /**
46
+ * Shared override routine used by add/remove format transforms.
47
+ *
48
+ * @param {Document} xmlDoc - XML document
49
+ * @param {Element} rPr - Run properties target
50
+ * @param {{bold?: boolean, italic?: boolean, underline?: boolean, strikethrough?: boolean}} formatFlags - Flags to apply
51
+ * @param {'remove'|'add'} mode - Override mode
52
+ */
53
+ function _applyOverrides(xmlDoc, rPr, formatFlags, mode) {
54
+ if (!rPr || !formatFlags) return;
55
+
56
+ const applyBold = !!formatFlags.bold;
57
+ const applyItalic = !!formatFlags.italic;
58
+ const applyUnderline = !!formatFlags.underline;
59
+ const applyStrike = !!formatFlags.strikethrough;
60
+
61
+ const removalSet = new Set();
62
+ if (applyBold) {
63
+ removalSet.add('w:b');
64
+ removalSet.add('w:bCs');
65
+ }
66
+ if (applyItalic) {
67
+ removalSet.add('w:i');
68
+ removalSet.add('w:iCs');
69
+ }
70
+ if (applyUnderline) removalSet.add('w:u');
71
+ if (applyStrike) removalSet.add('w:strike');
72
+
73
+ if (removalSet.size > 0) {
74
+ const toRemove = [];
75
+ for (const child of Array.from(rPr.childNodes)) {
76
+ if (removalSet.has(child.nodeName)) {
77
+ toRemove.push(child);
78
+ }
79
+ }
80
+ for (const el of toRemove) {
81
+ rPr.removeChild(el);
82
+ }
83
+ }
84
+
85
+ if (applyBold) {
86
+ const b = xmlDoc.createElement('w:b');
87
+ b.setAttribute('w:val', mode === 'add' ? '1' : '0');
88
+ insertRPrChildInOrder(rPr, b);
89
+
90
+ const bCs = xmlDoc.createElement('w:bCs');
91
+ bCs.setAttribute('w:val', mode === 'add' ? '1' : '0');
92
+ insertRPrChildInOrder(rPr, bCs);
93
+ }
94
+ if (applyItalic) {
95
+ const i = xmlDoc.createElement('w:i');
96
+ i.setAttribute('w:val', mode === 'add' ? '1' : '0');
97
+ insertRPrChildInOrder(rPr, i);
98
+
99
+ const iCs = xmlDoc.createElement('w:iCs');
100
+ iCs.setAttribute('w:val', mode === 'add' ? '1' : '0');
101
+ insertRPrChildInOrder(rPr, iCs);
102
+ }
103
+ if (applyUnderline) {
104
+ const u = xmlDoc.createElement('w:u');
105
+ u.setAttribute('w:val', mode === 'add' ? 'single' : 'none');
106
+ insertRPrChildInOrder(rPr, u);
107
+ }
108
+ if (applyStrike) {
109
+ const strike = xmlDoc.createElement('w:strike');
110
+ strike.setAttribute('w:val', mode === 'add' ? '1' : '0');
111
+ insertRPrChildInOrder(rPr, strike);
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Builds an rPr XML snippet that explicitly removes formatting while preserving other properties.
117
+ *
118
+ * @param {Document} xmlDoc - XML document
119
+ * @param {Element} originalRun - Source run
120
+ * @param {Object} formatToRemove - Format flags to remove
121
+ * @param {XMLSerializer} serializer - Serializer instance
122
+ * @returns {string}
123
+ */
124
+ export function buildOverrideRPrXml(xmlDoc, originalRun, formatToRemove, serializer) {
125
+ const baseRPr = originalRun.getElementsByTagName('w:rPr')[0] || null;
126
+ const rPr = baseRPr ? baseRPr.cloneNode(true) : xmlDoc.createElement('w:rPr');
127
+ _applyOverrides(xmlDoc, rPr, formatToRemove, 'remove');
128
+
129
+ let rPrXml = serializer.serializeToString(rPr);
130
+ rPrXml = rPrXml.replace(/\s+xmlns:[^=]+="[^"]*"/g, '');
131
+ return rPrXml === '<w:rPr/>' ? '' : rPrXml;
132
+ }
133
+
134
+ /**
135
+ * Removes formatting tags and adds explicit off overrides for the specified flags.
136
+ *
137
+ * @param {Document} xmlDoc - XML document
138
+ * @param {Element} rPr - Run properties
139
+ * @param {Object} formatToRemove - Format flags to remove
140
+ */
141
+ export function applyFormatOverridesToRPr(xmlDoc, rPr, formatToRemove) {
142
+ _applyOverrides(xmlDoc, rPr, formatToRemove, 'remove');
143
+ }
144
+
145
+ /**
146
+ * Extracts format flags from a run properties element.
147
+ *
148
+ * @param {Element|null} rPr - Run properties element
149
+ * @returns {{ bold: boolean, italic: boolean, underline: boolean, strikethrough: boolean, hasFormatting: boolean }}
150
+ */
151
+ export function extractFormatFromRPr(rPr) {
152
+ const format = { bold: false, italic: false, underline: false, strikethrough: false, hasFormatting: false };
153
+ if (!rPr) return format;
154
+
155
+ for (const child of Array.from(rPr.childNodes)) {
156
+ if (child.nodeName === 'w:b') format.bold = isFormattingElementEnabled(child, false);
157
+ if (child.nodeName === 'w:i') format.italic = isFormattingElementEnabled(child, false);
158
+ if (child.nodeName === 'w:u') format.underline = isFormattingElementEnabled(child, true);
159
+ if (child.nodeName === 'w:strike') format.strikethrough = isFormattingElementEnabled(child, false);
160
+
161
+ if (child.nodeName === 'w:rStyle') {
162
+ const styleRef = child.getAttribute('w:val');
163
+ if (styleRef) {
164
+ const lowerStyle = styleRef.toLowerCase();
165
+ if (lowerStyle.includes('bold') || lowerStyle.includes('strong')) format.bold = true;
166
+ if (lowerStyle.includes('italic') || lowerStyle.includes('emphasis')) format.italic = true;
167
+ if (lowerStyle.includes('underline')) format.underline = true;
168
+ }
169
+ }
170
+ }
171
+
172
+ format.hasFormatting = format.bold || format.italic || format.underline || format.strikethrough;
173
+ return format;
174
+ }
175
+
176
+ /**
177
+ * Determines whether a formatting element is effectively "on".
178
+ *
179
+ * @param {Element} element - Formatting element
180
+ * @param {boolean} isUnderline - Underline semantic handling
181
+ * @returns {boolean}
182
+ */
183
+ function isFormattingElementEnabled(element, isUnderline) {
184
+ const rawValue = element.getAttribute('w:val') || element.getAttribute('val') || '';
185
+ const value = rawValue.toLowerCase();
186
+
187
+ if (!value) return true;
188
+
189
+ if (isUnderline) {
190
+ return value !== 'none' && value !== '0' && value !== 'false' && value !== 'off';
191
+ }
192
+
193
+ return value !== '0' && value !== 'false' && value !== 'off';
194
+ }
@@ -0,0 +1,235 @@
1
+ /**
2
+ * OOXML run/track-change builders.
3
+ *
4
+ * This module centralizes creation of `w:r`, `w:ins`, `w:del`, `w:rPrChange`,
5
+ * and `w:pPrChange` elements used by surgical and reconstruction modes.
6
+ */
7
+
8
+ import { RPR_SCHEMA_ORDER } from './rpr-helpers.js';
9
+ import { createRevisionMetadata, getRevisionTimestamp } from '../core/types.js';
10
+ import { getFirstElementByTag } from '../core/xml-query.js';
11
+
12
+ /**
13
+ * Creates an insertion/deletion wrapper.
14
+ *
15
+ * @param {Document} xmlDoc - XML document
16
+ * @param {'ins'|'del'} type - Wrapper type
17
+ * @param {Element|null} run - Optional run to append
18
+ * @param {string} author - Change author
19
+ * @returns {Element}
20
+ */
21
+ export function createTrackChange(xmlDoc, type, run, author) {
22
+ const wrapper = xmlDoc.createElement(type === 'ins' ? 'w:ins' : 'w:del');
23
+ const metadata = createRevisionMetadata(author);
24
+ wrapper.setAttribute('w:id', String(metadata.id));
25
+ wrapper.setAttribute('w:author', metadata.author);
26
+ wrapper.setAttribute('w:date', metadata.date);
27
+ if (run) {
28
+ wrapper.appendChild(run);
29
+ }
30
+ return wrapper;
31
+ }
32
+
33
+ /**
34
+ * Creates a text run with optional formatting.
35
+ *
36
+ * @param {Document} xmlDoc - XML document
37
+ * @param {string} text - Text content
38
+ * @param {Element|null} rPr - Run properties
39
+ * @param {boolean} isDelete - Use `w:delText` instead of `w:t`
40
+ * @returns {Element}
41
+ */
42
+ export function createTextRun(xmlDoc, text, rPr, isDelete) {
43
+ const run = xmlDoc.createElement('w:r');
44
+ if (rPr) run.appendChild(rPr.cloneNode(true));
45
+
46
+ const textEl = xmlDoc.createElement(isDelete ? 'w:delText' : 'w:t');
47
+ textEl.setAttribute('xml:space', 'preserve');
48
+ textEl.textContent = text;
49
+ run.appendChild(textEl);
50
+
51
+ return run;
52
+ }
53
+
54
+ /**
55
+ * Creates an array of runs with formatting applied from hints.
56
+ *
57
+ * @param {Document} xmlDoc - XML document
58
+ * @param {string} text - Text to split and format
59
+ * @param {Element|null} baseRPr - Base run properties
60
+ * @param {Array} formatHints - Formatting hints
61
+ * @param {number} baseOffset - Absolute base offset
62
+ * @param {string} [author] - Change author
63
+ * @param {boolean} [generateRedlines] - Whether to create rPrChange
64
+ * @returns {Element[]}
65
+ */
66
+ export function createFormattedRuns(xmlDoc, text, baseRPr, formatHints, baseOffset, author, generateRedlines) {
67
+ if (!text) return [];
68
+
69
+ const breaks = new Set([0, text.length]);
70
+ for (const hint of formatHints) {
71
+ const localStart = Math.max(0, hint.start - baseOffset);
72
+ const localEnd = Math.min(text.length, hint.end - baseOffset);
73
+ if (localStart >= 0 && localStart < text.length) breaks.add(localStart);
74
+ if (localEnd > 0 && localEnd <= text.length) breaks.add(localEnd);
75
+ }
76
+
77
+ const sortedBreaks = Array.from(breaks).sort((a, b) => a - b);
78
+ const runs = [];
79
+
80
+ for (let i = 0; i < sortedBreaks.length - 1; i++) {
81
+ const start = sortedBreaks[i];
82
+ const end = sortedBreaks[i + 1];
83
+ const segment = text.slice(start, end);
84
+ if (!segment) continue;
85
+
86
+ const segmentBaseOffset = baseOffset + start;
87
+ const segmentEndOffset = baseOffset + end;
88
+
89
+ const applicableHints = formatHints.filter(h =>
90
+ h.start <= segmentBaseOffset && h.end >= segmentEndOffset
91
+ );
92
+
93
+ const combinedFormat = {};
94
+ applicableHints.forEach(h => {
95
+ if (h.format) Object.assign(combinedFormat, h.format);
96
+ });
97
+
98
+ const formattedRPr = injectFormattingToRPr(xmlDoc, baseRPr, combinedFormat, author, generateRedlines);
99
+ runs.push(createTextRunWithRPrElement(xmlDoc, segment, formattedRPr, false));
100
+ }
101
+
102
+ return runs;
103
+ }
104
+
105
+ /**
106
+ * Creates a text run with an existing rPr element (no clone).
107
+ *
108
+ * @param {Document} xmlDoc - XML document
109
+ * @param {string} text - Text content
110
+ * @param {Element|null} rPrElement - Run properties element
111
+ * @param {boolean} isDelete - Use `w:delText` instead of `w:t`
112
+ * @returns {Element}
113
+ */
114
+ export function createTextRunWithRPrElement(xmlDoc, text, rPrElement, isDelete) {
115
+ const run = xmlDoc.createElement('w:r');
116
+ if (rPrElement) run.appendChild(rPrElement);
117
+
118
+ const textEl = xmlDoc.createElement(isDelete ? 'w:delText' : 'w:t');
119
+ textEl.setAttribute('xml:space', 'preserve');
120
+ textEl.textContent = text;
121
+ run.appendChild(textEl);
122
+
123
+ return run;
124
+ }
125
+
126
+ /**
127
+ * Creates a new rPr synchronized to the requested core formatting flags.
128
+ *
129
+ * @param {Document} xmlDoc - XML document
130
+ * @param {Element|null} baseRPr - Base run properties
131
+ * @param {Object|null} format - Format flags
132
+ * @param {string} [author] - Change author
133
+ * @param {boolean} [generateRedlines] - Whether to create rPrChange
134
+ * @returns {Element}
135
+ */
136
+ export function injectFormattingToRPr(xmlDoc, baseRPr, format, author, generateRedlines) {
137
+ const rPr = xmlDoc.createElement('w:rPr');
138
+
139
+ if (baseRPr) {
140
+ Array.from(baseRPr.childNodes).forEach(child => {
141
+ if (!['w:b', 'w:bCs', 'w:i', 'w:iCs', 'w:u', 'w:strike', 'w:rPrChange'].includes(child.nodeName)) {
142
+ rPr.appendChild(child.cloneNode(true));
143
+ }
144
+ });
145
+ }
146
+
147
+ const activeFormat = format || { bold: false, italic: false, underline: false, strikethrough: false };
148
+
149
+ if (author && generateRedlines) {
150
+ createRPrChange(xmlDoc, rPr, author, baseRPr);
151
+ }
152
+
153
+ const syncElement = (tagName, isOn, valOn = null, valOff = '0') => {
154
+ const el = xmlDoc.createElement(tagName);
155
+ if (isOn) {
156
+ if (valOn) el.setAttribute('w:val', valOn);
157
+ } else if (valOff) {
158
+ el.setAttribute('w:val', valOff);
159
+ }
160
+
161
+ const myIndex = RPR_SCHEMA_ORDER.indexOf(tagName);
162
+ const myPriority = myIndex === -1 ? 999 : myIndex;
163
+
164
+ let inserted = false;
165
+ for (const child of Array.from(rPr.childNodes)) {
166
+ if (child.nodeType !== 1) continue;
167
+ const childIndex = RPR_SCHEMA_ORDER.indexOf(child.nodeName);
168
+ const childPriority = childIndex === -1 ? 999 : childIndex;
169
+ if (childPriority > myPriority) {
170
+ rPr.insertBefore(el, child);
171
+ inserted = true;
172
+ break;
173
+ }
174
+ }
175
+ if (!inserted) rPr.appendChild(el);
176
+ };
177
+
178
+ syncElement('w:b', !!activeFormat.bold, '1', '0');
179
+ syncElement('w:bCs', !!activeFormat.bold, '1', '0');
180
+ syncElement('w:i', !!activeFormat.italic, '1', '0');
181
+ syncElement('w:iCs', !!activeFormat.italic, '1', '0');
182
+ syncElement('w:u', !!activeFormat.underline, 'single', 'none');
183
+ syncElement('w:strike', !!activeFormat.strikethrough, '1', '0');
184
+
185
+ return rPr;
186
+ }
187
+
188
+ /**
189
+ * Creates and attaches a `w:rPrChange` snapshot.
190
+ *
191
+ * @param {Document} xmlDoc - XML document
192
+ * @param {Element} rPr - Target run properties
193
+ * @param {string} author - Change author
194
+ * @param {string} dateStr - ISO date string
195
+ * @param {Element} [sourceNode] - Optional source for previous state snapshot
196
+ * @returns {Element}
197
+ */
198
+ export function snapshotAndAttachRPrChange(xmlDoc, rPr, author, dateStr, sourceNode) {
199
+ const rPrChange = xmlDoc.createElement('w:rPrChange');
200
+ rPrChange.setAttribute('w:id', String(createRevisionMetadata(author).id));
201
+ rPrChange.setAttribute('w:author', author);
202
+ rPrChange.setAttribute('w:date', dateStr);
203
+
204
+ const previousRPr = xmlDoc.createElement('w:rPr');
205
+ const source = sourceNode || rPr;
206
+
207
+ Array.from(source.childNodes).forEach(child => {
208
+ if (child.nodeName !== 'w:rPrChange') {
209
+ previousRPr.appendChild(child.cloneNode(true));
210
+ }
211
+ });
212
+
213
+ rPrChange.appendChild(previousRPr);
214
+
215
+ const existing = getFirstElementByTag(rPr, 'w:rPrChange');
216
+ if (existing) {
217
+ rPr.removeChild(existing);
218
+ }
219
+
220
+ rPr.appendChild(rPrChange);
221
+ return rPrChange;
222
+ }
223
+
224
+ /**
225
+ * Creates `w:rPrChange` for track formatting changes.
226
+ *
227
+ * @param {Document} xmlDoc - XML document
228
+ * @param {Element} rPr - Run properties target
229
+ * @param {string} author - Change author
230
+ * @param {Element} [previousRPrArg] - Optional explicit previous-state source
231
+ * @returns {void}
232
+ */
233
+ function createRPrChange(xmlDoc, rPr, author, previousRPrArg) {
234
+ snapshotAndAttachRPrChange(xmlDoc, rPr, author, getRevisionTimestamp(), previousRPrArg || rPr);
235
+ }