@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.
- package/AGENTS.md +176 -0
- package/ARCHITECTURE.md +121 -0
- package/LICENSE +21 -0
- package/README.md +177 -0
- package/adapters/config.js +43 -0
- package/adapters/logger.js +89 -0
- package/adapters/xml-adapter.js +74 -0
- package/core/list-targeting.js +398 -0
- package/core/ooxml-identifiers.js +15 -0
- package/core/paragraph-offset-policy.js +50 -0
- package/core/paragraph-targeting.js +501 -0
- package/core/table-targeting.js +233 -0
- package/core/types.js +204 -0
- package/core/xml-query.js +99 -0
- package/dist/docx-redline-js.esm.js +8801 -0
- package/dist/docx-redline-js.esm.js.map +7 -0
- package/dist/docx-redline-js.esm.min.js +195 -0
- package/dist/docx-redline-js.esm.min.js.map +7 -0
- package/engine/format-application.js +358 -0
- package/engine/format-extraction.js +232 -0
- package/engine/format-paragraph-targeting.js +208 -0
- package/engine/format-span-application.js +178 -0
- package/engine/formatting-removal.js +330 -0
- package/engine/oxml-engine.js +279 -0
- package/engine/reconstruction-mapper.js +270 -0
- package/engine/reconstruction-mode.js +38 -0
- package/engine/reconstruction-writer.js +276 -0
- package/engine/rpr-helpers.js +194 -0
- package/engine/run-builders.js +235 -0
- package/engine/surgical-mode.js +520 -0
- package/engine/table-cell-context.js +151 -0
- package/engine/table-mode.js +172 -0
- package/index.js +308 -0
- package/orchestration/list-markdown.js +141 -0
- package/orchestration/list-parsing.js +73 -0
- package/orchestration/list-structural-fallback.js +530 -0
- package/orchestration/redline-operation-converter.js +141 -0
- package/orchestration/route-plan.js +160 -0
- package/package.json +76 -0
- package/pipeline/content-analysis.js +107 -0
- package/pipeline/diff-engine.js +204 -0
- package/pipeline/ingestion-export.js +255 -0
- package/pipeline/ingestion-paragraph.js +351 -0
- package/pipeline/ingestion-table.js +169 -0
- package/pipeline/ingestion-xml.js +39 -0
- package/pipeline/ingestion.js +8 -0
- package/pipeline/list-generation.js +280 -0
- package/pipeline/list-markers.js +77 -0
- package/pipeline/markdown-processor.js +160 -0
- package/pipeline/patching.js +408 -0
- package/pipeline/pipeline.js +326 -0
- package/pipeline/serialization.js +395 -0
- package/services/browser-demo-prompt-context.js +345 -0
- package/services/comment-builders.js +60 -0
- package/services/comment-engine.js +248 -0
- package/services/comment-locator.js +197 -0
- package/services/comment-package.js +113 -0
- package/services/numbering-helpers.js +416 -0
- package/services/numbering-service.js +290 -0
- package/services/package-builder.js +147 -0
- package/services/standalone-docx-plumbing.js +443 -0
- package/services/standalone-operation-runner.js +1169 -0
- package/services/table-reconciliation.js +344 -0
- 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
|
+
}
|