@ansonlai/docx-redline-js 0.8.1 → 0.8.3
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/CHANGELOG.md +586 -539
- package/README.md +901 -890
- package/core/list-targeting.js +12 -10
- package/dist/docx-redline-js.esm.js +280 -33
- package/dist/docx-redline-js.esm.js.map +3 -3
- package/dist/docx-redline-js.esm.min.js +86 -86
- package/dist/docx-redline-js.esm.min.js.map +3 -3
- package/dist/docx-redline.bundle.cjs +280 -33
- package/dist/docx-redline.bundle.cjs.map +3 -3
- package/dist/docx-redline.bundle.js +280 -33
- package/dist/docx-redline.bundle.js.map +3 -3
- package/engine/reconstruction-mode.js +183 -54
- package/orchestration/list-structural-fallback.js +530 -518
- package/package.json +1 -1
- package/pipeline/diff-engine.js +274 -258
- package/pipeline/list-generation.js +203 -168
- package/scripts/run-tests.mjs +14 -2
- package/services/document-inspection.js +10 -7
- package/services/document-operation-mutations.js +1597 -1586
- package/services/numbering-helpers.js +108 -0
|
@@ -83,6 +83,8 @@ export function createDynamicNumberingIdState(numberingXml, options = {}) {
|
|
|
83
83
|
const baseAbstractNumId = Math.max(minId, maxUsedAbstractNumId + 1);
|
|
84
84
|
|
|
85
85
|
return {
|
|
86
|
+
// Kept so list edits can recognise which existing definition a source list uses and continue it.
|
|
87
|
+
sourceNumberingXml: String(numberingXml || ''),
|
|
86
88
|
nextNumId: nextAvailableId(baseNumId, usedNumIds, maxPreferred),
|
|
87
89
|
nextAbstractNumId: nextAvailableId(baseAbstractNumId, usedAbstractNumIds, maxPreferred),
|
|
88
90
|
usedNumIds,
|
|
@@ -285,6 +287,109 @@ export function buildExplicitDecimalMultilevelNumberingXml(numId, abstractNumId,
|
|
|
285
287
|
</w:numbering>`.trim();
|
|
286
288
|
}
|
|
287
289
|
|
|
290
|
+
function isTrackedDeletedParagraph(node) {
|
|
291
|
+
if (!node || node.nodeType !== 1 || node.localName !== 'p') return false;
|
|
292
|
+
const pPr = Array.from(node.childNodes || []).find(child => isDirectWordChild(child, 'pPr'));
|
|
293
|
+
const rPr = pPr && Array.from(pPr.childNodes || []).find(child => isDirectWordChild(child, 'rPr'));
|
|
294
|
+
return !!rPr && Array.from(rPr.childNodes || []).some(child => isDirectWordChild(child, 'del'));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function paragraphNumIdNodes(node) {
|
|
298
|
+
return Array.from(node?.getElementsByTagNameNS?.('*', 'numId') || [])
|
|
299
|
+
.filter(numIdNode => numIdNode.parentNode?.localName === 'numPr');
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function levelFormats(abstractNum) {
|
|
303
|
+
const formats = new Map();
|
|
304
|
+
for (const lvl of Array.from(abstractNum?.getElementsByTagNameNS?.('*', 'lvl') || [])) {
|
|
305
|
+
const ilvl = getElementId(lvl, ['w:ilvl', 'ilvl']);
|
|
306
|
+
const fmt = Array.from(lvl.getElementsByTagNameNS('*', 'numFmt'))[0]?.getAttribute('w:val') || null;
|
|
307
|
+
if (ilvl != null && fmt) formats.set(ilvl, fmt);
|
|
308
|
+
}
|
|
309
|
+
return formats;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function normalizeListFormat(format) {
|
|
313
|
+
return format === 'bullet' || format === 'circle' || format === 'square' ? 'bullet' : format;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function abstractNumForNumId(numberingDoc, numId) {
|
|
317
|
+
const num = Array.from(numberingDoc.getElementsByTagNameNS('*', 'num'))
|
|
318
|
+
.find(candidate => getElementId(candidate, ['w:numId', 'numId']) === numId);
|
|
319
|
+
const abstractId = num && getElementId(Array.from(num.getElementsByTagNameNS('*', 'abstractNumId'))[0], ['w:val', 'val']);
|
|
320
|
+
if (abstractId == null) return null;
|
|
321
|
+
return Array.from(numberingDoc.getElementsByTagNameNS('*', 'abstractNum'))
|
|
322
|
+
.find(candidate => getElementId(candidate, ['w:abstractNumId', 'abstractNumId']) === abstractId) || null;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Lets a same-kind list replacement continue the list it replaces instead of allocating a new
|
|
327
|
+
* numbering definition. When every new list paragraph's generated definition has the same format
|
|
328
|
+
* as the single existing list used by the source paragraphs at each level in use, the new
|
|
329
|
+
* paragraphs are bound to that existing numId and the generated numbering payload is dropped.
|
|
330
|
+
* Format-changing replacements (and anything ambiguous) return null and take the allocation path.
|
|
331
|
+
*
|
|
332
|
+
* @param {string|null} numberingXml - Generated numbering payload
|
|
333
|
+
* @param {Node[]} replacementNodes - Generated paragraphs (tracked-deleted source copies are left alone)
|
|
334
|
+
* @param {Node[]} sourceParagraphs - Paragraphs being replaced
|
|
335
|
+
* @param {{ sourceNumberingXml?: string }|null} numberingIdState
|
|
336
|
+
* @returns {{ numId: number, replacementNodes: Node[] }|null}
|
|
337
|
+
*/
|
|
338
|
+
export function reuseSourceListNumbering(numberingXml, replacementNodes, sourceParagraphs, numberingIdState) {
|
|
339
|
+
const sourceXml = numberingIdState?.sourceNumberingXml;
|
|
340
|
+
if (!numberingXml || !String(sourceXml || '').trim() || !Array.isArray(replacementNodes)) return null;
|
|
341
|
+
|
|
342
|
+
const sourceNumIds = new Set();
|
|
343
|
+
for (const paragraph of sourceParagraphs || []) {
|
|
344
|
+
for (const numIdNode of paragraphNumIdNodes(paragraph)) {
|
|
345
|
+
const id = getElementId(numIdNode, ['w:val', 'val']);
|
|
346
|
+
if (id != null && id !== 0) sourceNumIds.add(id);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (sourceNumIds.size !== 1) return null;
|
|
350
|
+
const [sourceNumId] = sourceNumIds;
|
|
351
|
+
|
|
352
|
+
const sourceDoc = parseOoxml(sourceXml);
|
|
353
|
+
const payloadDoc = parseOoxml(numberingXml);
|
|
354
|
+
if (hasXmlParseError(sourceDoc) || hasXmlParseError(payloadDoc)) return null;
|
|
355
|
+
const sourceAbstract = abstractNumForNumId(sourceDoc, sourceNumId);
|
|
356
|
+
if (!sourceAbstract) return null;
|
|
357
|
+
const sourceFormats = levelFormats(sourceAbstract);
|
|
358
|
+
|
|
359
|
+
const newParagraphNumIds = [];
|
|
360
|
+
for (const node of replacementNodes) {
|
|
361
|
+
if (isTrackedDeletedParagraph(node)) continue;
|
|
362
|
+
for (const numIdNode of paragraphNumIdNodes(node)) {
|
|
363
|
+
const ilvlNode = Array.from(numIdNode.parentNode.childNodes || []).find(child => child.localName === 'ilvl');
|
|
364
|
+
newParagraphNumIds.push({
|
|
365
|
+
numId: getElementId(numIdNode, ['w:val', 'val']),
|
|
366
|
+
ilvl: ilvlNode ? (getElementId(ilvlNode, ['w:val', 'val']) ?? 0) : 0
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (newParagraphNumIds.length === 0) return null;
|
|
371
|
+
|
|
372
|
+
const payloadFormats = new Map();
|
|
373
|
+
for (const { numId, ilvl } of newParagraphNumIds) {
|
|
374
|
+
if (numId == null) return null;
|
|
375
|
+
if (!payloadFormats.has(numId)) {
|
|
376
|
+
const abstract = abstractNumForNumId(payloadDoc, numId);
|
|
377
|
+
if (!abstract) return null;
|
|
378
|
+
payloadFormats.set(numId, levelFormats(abstract));
|
|
379
|
+
}
|
|
380
|
+
const generated = payloadFormats.get(numId).get(ilvl);
|
|
381
|
+
const existing = sourceFormats.get(ilvl);
|
|
382
|
+
if (!generated || !existing || normalizeListFormat(generated) !== normalizeListFormat(existing)) return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const clonedNodes = replacementNodes.map(node => (node?.cloneNode ? node.cloneNode(true) : node));
|
|
386
|
+
for (const node of clonedNodes) {
|
|
387
|
+
if (isTrackedDeletedParagraph(node)) continue;
|
|
388
|
+
for (const numIdNode of paragraphNumIdNodes(node)) setElementVal(numIdNode, sourceNumId);
|
|
389
|
+
}
|
|
390
|
+
return { numId: sourceNumId, replacementNodes: clonedNodes };
|
|
391
|
+
}
|
|
392
|
+
|
|
288
393
|
/**
|
|
289
394
|
* Remaps incoming numbering payload IDs to document-safe IDs, and updates the
|
|
290
395
|
* provided replacement nodes to reference the remapped `w:numId` values.
|
|
@@ -340,6 +445,9 @@ export function remapNumberingPayloadForDocument(numberingXml, replacementNodes,
|
|
|
340
445
|
? replacementNodes.map(node => node?.cloneNode ? node.cloneNode(true) : node)
|
|
341
446
|
: [];
|
|
342
447
|
for (const node of clonedNodes) {
|
|
448
|
+
// A paragraph whose mark is tracked-deleted is retained source content. Its numId refers to the
|
|
449
|
+
// document's own definitions, never to this payload, so it must keep pointing at them.
|
|
450
|
+
if (isTrackedDeletedParagraph(node)) continue;
|
|
343
451
|
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.('*', 'numId') || []);
|
|
344
452
|
for (const numIdNode of numIdNodes) {
|
|
345
453
|
const oldNumRef = getElementId(numIdNode, ['w:val', 'val']);
|