@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,416 @@
|
|
|
1
|
+
import { parseOoxml, serializeOoxml } from '../engine/oxml-engine.js';
|
|
2
|
+
import { WORD_MAIN_NS } from '../core/paragraph-targeting.js';
|
|
3
|
+
|
|
4
|
+
function parseIntegerAttribute(element, names) {
|
|
5
|
+
if (!element || !Array.isArray(names)) return null;
|
|
6
|
+
for (const name of names) {
|
|
7
|
+
const raw = element.getAttribute(name);
|
|
8
|
+
if (raw == null || raw === '') continue;
|
|
9
|
+
const parsed = Number.parseInt(String(raw), 10);
|
|
10
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
11
|
+
}
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function nextAvailableId(startId, occupiedIds, maxPreferred = null) {
|
|
16
|
+
let candidate = Number.isInteger(startId) && startId > 0 ? startId : 1;
|
|
17
|
+
const occupied = occupiedIds instanceof Set ? occupiedIds : new Set();
|
|
18
|
+
|
|
19
|
+
while (occupied.has(candidate)) {
|
|
20
|
+
candidate += 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (Number.isInteger(maxPreferred) && maxPreferred > 0 && candidate > maxPreferred) {
|
|
24
|
+
for (let probe = 1; probe <= maxPreferred; probe += 1) {
|
|
25
|
+
if (!occupied.has(probe)) return probe;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return candidate;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Builds a dynamic numbering-id state from existing numbering XML.
|
|
34
|
+
*
|
|
35
|
+
* This avoids hardcoded ID floors and keeps IDs deterministic relative to the
|
|
36
|
+
* current document numbering definitions.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} numberingXml - Existing `word/numbering.xml` content
|
|
39
|
+
* @param {{
|
|
40
|
+
* minId?: number,
|
|
41
|
+
* maxPreferred?: number
|
|
42
|
+
* }} [options={}] - Optional ID preferences
|
|
43
|
+
* @returns {{
|
|
44
|
+
* nextNumId: number,
|
|
45
|
+
* nextAbstractNumId: number,
|
|
46
|
+
* usedNumIds: Set<number>,
|
|
47
|
+
* usedAbstractNumIds: Set<number>,
|
|
48
|
+
* minId: number,
|
|
49
|
+
* maxPreferred: number
|
|
50
|
+
* }}
|
|
51
|
+
*/
|
|
52
|
+
export function createDynamicNumberingIdState(numberingXml, options = {}) {
|
|
53
|
+
const minId = Number.isInteger(options?.minId) && options.minId > 0 ? options.minId : 1;
|
|
54
|
+
const maxPreferred = Number.isInteger(options?.maxPreferred) && options.maxPreferred >= minId
|
|
55
|
+
? options.maxPreferred
|
|
56
|
+
: 32767;
|
|
57
|
+
|
|
58
|
+
const usedNumIds = new Set();
|
|
59
|
+
const usedAbstractNumIds = new Set();
|
|
60
|
+
|
|
61
|
+
if (String(numberingXml || '').trim()) {
|
|
62
|
+
try {
|
|
63
|
+
const numberingDoc = parseOoxml(numberingXml);
|
|
64
|
+
const abstractNums = Array.from(numberingDoc.getElementsByTagNameNS('*', 'abstractNum'));
|
|
65
|
+
const nums = Array.from(numberingDoc.getElementsByTagNameNS('*', 'num'));
|
|
66
|
+
|
|
67
|
+
for (const abstractNum of abstractNums) {
|
|
68
|
+
const id = parseIntegerAttribute(abstractNum, ['w:abstractNumId', 'abstractNumId']);
|
|
69
|
+
if (id != null) usedAbstractNumIds.add(id);
|
|
70
|
+
}
|
|
71
|
+
for (const num of nums) {
|
|
72
|
+
const id = parseIntegerAttribute(num, ['w:numId', 'numId']);
|
|
73
|
+
if (id != null) usedNumIds.add(id);
|
|
74
|
+
}
|
|
75
|
+
} catch {
|
|
76
|
+
// Ignore malformed numbering XML and fall back to empty sets.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const maxUsedNumId = usedNumIds.size > 0 ? Math.max(...usedNumIds) : 0;
|
|
81
|
+
const maxUsedAbstractNumId = usedAbstractNumIds.size > 0 ? Math.max(...usedAbstractNumIds) : 0;
|
|
82
|
+
const baseNumId = Math.max(minId, maxUsedNumId + 1);
|
|
83
|
+
const baseAbstractNumId = Math.max(minId, maxUsedAbstractNumId + 1);
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
nextNumId: nextAvailableId(baseNumId, usedNumIds, maxPreferred),
|
|
87
|
+
nextAbstractNumId: nextAvailableId(baseAbstractNumId, usedAbstractNumIds, maxPreferred),
|
|
88
|
+
usedNumIds,
|
|
89
|
+
usedAbstractNumIds,
|
|
90
|
+
minId,
|
|
91
|
+
maxPreferred
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeNumberingIdState(state) {
|
|
96
|
+
if (!state || typeof state !== 'object') return null;
|
|
97
|
+
|
|
98
|
+
if (!(state.usedNumIds instanceof Set)) state.usedNumIds = new Set();
|
|
99
|
+
if (!(state.usedAbstractNumIds instanceof Set)) state.usedAbstractNumIds = new Set();
|
|
100
|
+
|
|
101
|
+
if (!Number.isInteger(state.minId) || state.minId < 1) {
|
|
102
|
+
state.minId = 1;
|
|
103
|
+
}
|
|
104
|
+
if (!Number.isInteger(state.maxPreferred) || state.maxPreferred < state.minId) {
|
|
105
|
+
state.maxPreferred = 32767;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!Number.isInteger(state.nextNumId) || state.nextNumId < state.minId) {
|
|
109
|
+
state.nextNumId = state.minId;
|
|
110
|
+
}
|
|
111
|
+
if (!Number.isInteger(state.nextAbstractNumId) || state.nextAbstractNumId < state.minId) {
|
|
112
|
+
state.nextAbstractNumId = state.minId;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
state.nextNumId = nextAvailableId(state.nextNumId, state.usedNumIds, state.maxPreferred);
|
|
116
|
+
state.nextAbstractNumId = nextAvailableId(state.nextAbstractNumId, state.usedAbstractNumIds, state.maxPreferred);
|
|
117
|
+
return state;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Reserves the next available ID from a mutable numbering-id state.
|
|
122
|
+
*
|
|
123
|
+
* @param {ReturnType<typeof createDynamicNumberingIdState>} state
|
|
124
|
+
* @param {'num'|'abstract'} [kind='num']
|
|
125
|
+
* @returns {number|null}
|
|
126
|
+
*/
|
|
127
|
+
export function reserveNextNumberingId(state, kind = 'num') {
|
|
128
|
+
const normalized = normalizeNumberingIdState(state);
|
|
129
|
+
if (!normalized) return null;
|
|
130
|
+
|
|
131
|
+
const useAbstract = kind === 'abstract';
|
|
132
|
+
const id = useAbstract ? normalized.nextAbstractNumId : normalized.nextNumId;
|
|
133
|
+
if (!Number.isInteger(id) || id < 1) return null;
|
|
134
|
+
|
|
135
|
+
if (useAbstract) {
|
|
136
|
+
normalized.usedAbstractNumIds.add(id);
|
|
137
|
+
normalized.nextAbstractNumId = nextAvailableId(id + 1, normalized.usedAbstractNumIds, normalized.maxPreferred);
|
|
138
|
+
} else {
|
|
139
|
+
normalized.usedNumIds.add(id);
|
|
140
|
+
normalized.nextNumId = nextAvailableId(id + 1, normalized.usedNumIds, normalized.maxPreferred);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return id;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Reserves the next available numbering IDs on a mutable numbering-id state.
|
|
148
|
+
*
|
|
149
|
+
* @param {ReturnType<typeof createDynamicNumberingIdState>} state
|
|
150
|
+
* @returns {{ numId: number, abstractNumId: number } | null}
|
|
151
|
+
*/
|
|
152
|
+
export function reserveNextNumberingIdPair(state) {
|
|
153
|
+
const numId = reserveNextNumberingId(state, 'num');
|
|
154
|
+
const abstractNumId = reserveNextNumberingId(state, 'abstract');
|
|
155
|
+
if (numId == null || abstractNumId == null) return null;
|
|
156
|
+
|
|
157
|
+
return { numId, abstractNumId };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function hasXmlParseError(doc) {
|
|
161
|
+
if (!doc || !doc.documentElement) return true;
|
|
162
|
+
if (doc.documentElement.localName === 'parsererror') return true;
|
|
163
|
+
return doc.getElementsByTagName('parsererror').length > 0;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function isDirectWordChild(node, localName) {
|
|
167
|
+
return !!(
|
|
168
|
+
node &&
|
|
169
|
+
node.nodeType === 1 &&
|
|
170
|
+
node.namespaceURI === WORD_MAIN_NS &&
|
|
171
|
+
node.localName === localName
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function insertNumberingNodeInSchemaOrder(root, node, kind) {
|
|
176
|
+
if (!root || !node) return;
|
|
177
|
+
const directChildren = Array.from(root.childNodes || []).filter(
|
|
178
|
+
child => child && child.nodeType === 1 && child.namespaceURI === WORD_MAIN_NS
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
let anchor = null;
|
|
182
|
+
if (kind === 'abstract') {
|
|
183
|
+
anchor = directChildren.find(
|
|
184
|
+
child => child.localName === 'num' || child.localName === 'numIdMacAtCleanup'
|
|
185
|
+
) || null;
|
|
186
|
+
} else {
|
|
187
|
+
anchor = directChildren.find(child => child.localName === 'numIdMacAtCleanup') || null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (anchor) root.insertBefore(node, anchor);
|
|
191
|
+
else root.appendChild(node);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function getAttributeFirst(element, names) {
|
|
195
|
+
for (const name of names || []) {
|
|
196
|
+
const value = element?.getAttribute?.(name);
|
|
197
|
+
if (value != null && value !== '') return value;
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function getElementId(element, names) {
|
|
203
|
+
const raw = getAttributeFirst(element, names);
|
|
204
|
+
const parsed = Number.parseInt(String(raw || ''), 10);
|
|
205
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function setElementId(element, preferredName, idValue) {
|
|
209
|
+
element?.setAttribute?.(preferredName, String(idValue));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function setElementVal(element, value) {
|
|
213
|
+
element?.setAttribute?.('w:val', String(value));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Overwrites all paragraph-level `w:numId` references in a node collection.
|
|
218
|
+
*
|
|
219
|
+
* @param {Node[]|null|undefined} paragraphNodes
|
|
220
|
+
* @param {string|number|null|undefined} targetNumId
|
|
221
|
+
*/
|
|
222
|
+
export function overwriteParagraphNumIds(paragraphNodes, targetNumId) {
|
|
223
|
+
if (!Array.isArray(paragraphNodes) || targetNumId == null) return;
|
|
224
|
+
for (const node of paragraphNodes) {
|
|
225
|
+
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.('*', 'numId') || []);
|
|
226
|
+
for (const numIdNode of numIdNodes) {
|
|
227
|
+
setElementVal(numIdNode, targetNumId);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Extracts the first `w:numId` value from a node collection.
|
|
234
|
+
*
|
|
235
|
+
* @param {Node[]|null|undefined} paragraphNodes
|
|
236
|
+
* @returns {string|null}
|
|
237
|
+
*/
|
|
238
|
+
export function extractFirstParagraphNumId(paragraphNodes) {
|
|
239
|
+
for (const node of paragraphNodes || []) {
|
|
240
|
+
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.('*', 'numId') || []);
|
|
241
|
+
for (const numIdNode of numIdNodes) {
|
|
242
|
+
const numId = getElementId(numIdNode, ['w:val', 'val']);
|
|
243
|
+
if (numId != null) return String(numId);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Builds multilevel decimal numbering XML for explicit-start header conversion.
|
|
251
|
+
*
|
|
252
|
+
* @param {string|number} numId
|
|
253
|
+
* @param {string|number} abstractNumId
|
|
254
|
+
* @param {number} startAt
|
|
255
|
+
* @returns {string}
|
|
256
|
+
*/
|
|
257
|
+
export function buildExplicitDecimalMultilevelNumberingXml(numId, abstractNumId, startAt) {
|
|
258
|
+
const safeNumId = String(numId);
|
|
259
|
+
const safeAbstractNumId = String(abstractNumId);
|
|
260
|
+
const safeStartAt = Number.isInteger(startAt) && startAt > 0 ? startAt : 1;
|
|
261
|
+
const levelsXml = Array.from({ length: 9 }, (_, level) => {
|
|
262
|
+
const lvlText = Array.from({ length: level + 1 }, (_, i) => `%${i + 1}`).join('.') + '.';
|
|
263
|
+
const left = 720 * (level + 1);
|
|
264
|
+
return `
|
|
265
|
+
<w:lvl w:ilvl="${level}">
|
|
266
|
+
<w:start w:val="1"/>
|
|
267
|
+
<w:numFmt w:val="decimal"/>
|
|
268
|
+
<w:lvlText w:val="${lvlText}"/>
|
|
269
|
+
<w:lvlJc w:val="left"/>
|
|
270
|
+
<w:pPr><w:ind w:left="${left}" w:hanging="360"/></w:pPr>
|
|
271
|
+
</w:lvl>`;
|
|
272
|
+
}).join('');
|
|
273
|
+
return `
|
|
274
|
+
<w:numbering xmlns:w="${WORD_MAIN_NS}">
|
|
275
|
+
<w:abstractNum w:abstractNumId="${safeAbstractNumId}">
|
|
276
|
+
<w:multiLevelType w:val="multilevel"/>
|
|
277
|
+
${levelsXml}
|
|
278
|
+
</w:abstractNum>
|
|
279
|
+
<w:num w:numId="${safeNumId}">
|
|
280
|
+
<w:abstractNumId w:val="${safeAbstractNumId}"/>
|
|
281
|
+
<w:lvlOverride w:ilvl="0">
|
|
282
|
+
<w:startOverride w:val="${safeStartAt}"/>
|
|
283
|
+
</w:lvlOverride>
|
|
284
|
+
</w:num>
|
|
285
|
+
</w:numbering>`.trim();
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Remaps incoming numbering payload IDs to document-safe IDs, and updates the
|
|
290
|
+
* provided replacement nodes to reference the remapped `w:numId` values.
|
|
291
|
+
*
|
|
292
|
+
* @param {string} numberingXml
|
|
293
|
+
* @param {Node[]} replacementNodes
|
|
294
|
+
* @param {ReturnType<typeof createDynamicNumberingIdState>} numberingIdState
|
|
295
|
+
* @returns {{ numberingXml: string, replacementNodes: Node[] }}
|
|
296
|
+
*/
|
|
297
|
+
export function remapNumberingPayloadForDocument(numberingXml, replacementNodes, numberingIdState) {
|
|
298
|
+
const numberingDoc = parseOoxml(numberingXml);
|
|
299
|
+
if (hasXmlParseError(numberingDoc)) {
|
|
300
|
+
return {
|
|
301
|
+
numberingXml: String(numberingXml || ''),
|
|
302
|
+
replacementNodes: Array.isArray(replacementNodes)
|
|
303
|
+
? replacementNodes.map(node => node?.cloneNode ? node.cloneNode(true) : node)
|
|
304
|
+
: []
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const abstractNumMap = new Map();
|
|
309
|
+
const numIdMap = new Map();
|
|
310
|
+
|
|
311
|
+
const abstractNums = Array.from(numberingDoc.getElementsByTagNameNS('*', 'abstractNum'));
|
|
312
|
+
for (const abstractNum of abstractNums) {
|
|
313
|
+
const oldId = getElementId(abstractNum, ['w:abstractNumId', 'abstractNumId']);
|
|
314
|
+
if (oldId == null) continue;
|
|
315
|
+
const newId = reserveNextNumberingId(numberingIdState, 'abstract');
|
|
316
|
+
if (newId == null) continue;
|
|
317
|
+
abstractNumMap.set(oldId, newId);
|
|
318
|
+
setElementId(abstractNum, 'w:abstractNumId', newId);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const nums = Array.from(numberingDoc.getElementsByTagNameNS('*', 'num'));
|
|
322
|
+
for (const num of nums) {
|
|
323
|
+
const oldNumId = getElementId(num, ['w:numId', 'numId']);
|
|
324
|
+
if (oldNumId == null) continue;
|
|
325
|
+
const newNumId = reserveNextNumberingId(numberingIdState, 'num');
|
|
326
|
+
if (newNumId == null) continue;
|
|
327
|
+
numIdMap.set(oldNumId, newNumId);
|
|
328
|
+
setElementId(num, 'w:numId', newNumId);
|
|
329
|
+
|
|
330
|
+
const abstractNumIdNode = Array.from(num.getElementsByTagNameNS('*', 'abstractNumId'))[0] || null;
|
|
331
|
+
if (abstractNumIdNode) {
|
|
332
|
+
const oldAbsRef = getElementId(abstractNumIdNode, ['w:val', 'val']);
|
|
333
|
+
if (oldAbsRef != null && abstractNumMap.has(oldAbsRef)) {
|
|
334
|
+
setElementVal(abstractNumIdNode, abstractNumMap.get(oldAbsRef));
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const clonedNodes = Array.isArray(replacementNodes)
|
|
340
|
+
? replacementNodes.map(node => node?.cloneNode ? node.cloneNode(true) : node)
|
|
341
|
+
: [];
|
|
342
|
+
for (const node of clonedNodes) {
|
|
343
|
+
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.('*', 'numId') || []);
|
|
344
|
+
for (const numIdNode of numIdNodes) {
|
|
345
|
+
const oldNumRef = getElementId(numIdNode, ['w:val', 'val']);
|
|
346
|
+
if (oldNumRef != null && numIdMap.has(oldNumRef)) {
|
|
347
|
+
setElementVal(numIdNode, numIdMap.get(oldNumRef));
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
numberingXml: serializeOoxml(numberingDoc),
|
|
354
|
+
replacementNodes: clonedNodes
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Merges incoming numbering definitions into an existing numbering part while
|
|
360
|
+
* preserving schema child order (`abstractNum*` before `num*`).
|
|
361
|
+
*
|
|
362
|
+
* @param {string} existingNumberingXml
|
|
363
|
+
* @param {string} incomingNumberingXml
|
|
364
|
+
* @returns {string}
|
|
365
|
+
*/
|
|
366
|
+
export function mergeNumberingXmlBySchemaOrder(existingNumberingXml, incomingNumberingXml) {
|
|
367
|
+
const existingText = String(existingNumberingXml || '');
|
|
368
|
+
const incomingText = String(incomingNumberingXml || '');
|
|
369
|
+
if (!incomingText.trim()) return existingText;
|
|
370
|
+
if (!existingText.trim()) return incomingText;
|
|
371
|
+
|
|
372
|
+
try {
|
|
373
|
+
const existingDoc = parseOoxml(existingText);
|
|
374
|
+
const incomingDoc = parseOoxml(incomingText);
|
|
375
|
+
if (hasXmlParseError(existingDoc) || hasXmlParseError(incomingDoc)) {
|
|
376
|
+
return existingText;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const existingRoot = existingDoc.documentElement;
|
|
380
|
+
const incomingRoot = incomingDoc.documentElement;
|
|
381
|
+
if (!existingRoot || !incomingRoot) return existingText;
|
|
382
|
+
|
|
383
|
+
const existingAbstractIds = new Set(
|
|
384
|
+
Array.from(existingRoot.childNodes || [])
|
|
385
|
+
.filter(node => isDirectWordChild(node, 'abstractNum'))
|
|
386
|
+
.map(node => parseIntegerAttribute(node, ['w:abstractNumId', 'abstractNumId']))
|
|
387
|
+
.filter(id => id != null)
|
|
388
|
+
);
|
|
389
|
+
const existingNumIds = new Set(
|
|
390
|
+
Array.from(existingRoot.childNodes || [])
|
|
391
|
+
.filter(node => isDirectWordChild(node, 'num'))
|
|
392
|
+
.map(node => parseIntegerAttribute(node, ['w:numId', 'numId']))
|
|
393
|
+
.filter(id => id != null)
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
for (const incomingNode of Array.from(incomingRoot.childNodes || [])) {
|
|
397
|
+
if (!isDirectWordChild(incomingNode, 'abstractNum')) continue;
|
|
398
|
+
const incomingId = parseIntegerAttribute(incomingNode, ['w:abstractNumId', 'abstractNumId']);
|
|
399
|
+
if (incomingId == null || existingAbstractIds.has(incomingId)) continue;
|
|
400
|
+
insertNumberingNodeInSchemaOrder(existingRoot, existingDoc.importNode(incomingNode, true), 'abstract');
|
|
401
|
+
existingAbstractIds.add(incomingId);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
for (const incomingNode of Array.from(incomingRoot.childNodes || [])) {
|
|
405
|
+
if (!isDirectWordChild(incomingNode, 'num')) continue;
|
|
406
|
+
const incomingId = parseIntegerAttribute(incomingNode, ['w:numId', 'numId']);
|
|
407
|
+
if (incomingId == null || existingNumIds.has(incomingId)) continue;
|
|
408
|
+
insertNumberingNodeInSchemaOrder(existingRoot, existingDoc.importNode(incomingNode, true), 'num');
|
|
409
|
+
existingNumIds.add(incomingId);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return serializeOoxml(existingDoc);
|
|
413
|
+
} catch {
|
|
414
|
+
return existingText;
|
|
415
|
+
}
|
|
416
|
+
}
|