@ansonlai/docx-redline-js 0.8.2 → 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.
@@ -1,4 +1,4 @@
1
- // @ansonlai/docx-redline-js standalone bundle v0.8.2 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js standalone bundle v0.8.3 — https://github.com/AnsonLai/docx-redline-js
2
2
  var __create = Object.create;
3
3
  var __defProp = Object.defineProperty;
4
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -10452,7 +10452,37 @@ async function executeListGeneration(options) {
10452
10452
  }];
10453
10453
  }
10454
10454
  }
10455
- if (generateRedlines && deletionRuns.length > 0) {
10455
+ const sourceParagraphCount = (originalRunModel || []).filter((run) => run.kind === RunKind.PARAGRAPH_START).length;
10456
+ if (generateRedlines && sourceParagraphCount > 0 && (sourceParagraphCount > 1 || deletionRuns.length === 0)) {
10457
+ let segment = null;
10458
+ const flushSegment = () => {
10459
+ if (!segment) return;
10460
+ results.push(serializeToOoxml(segment, null, [], {
10461
+ author,
10462
+ generateRedlines,
10463
+ revisionIdAllocator
10464
+ }));
10465
+ segment = null;
10466
+ };
10467
+ for (const run of originalRunModel) {
10468
+ if (run.kind === RunKind.PARAGRAPH_START) {
10469
+ flushSegment();
10470
+ segment = [{
10471
+ ...run,
10472
+ pPrElement: null,
10473
+ pPrXml: addParagraphMarkRevision(
10474
+ run.pPrElement || run.pPrXml || null,
10475
+ "del",
10476
+ author,
10477
+ revisionIdAllocator
10478
+ )
10479
+ }];
10480
+ } else if (segment && (run.kind === "text" || run.kind === "run")) {
10481
+ segment.push({ ...run, kind: "deletion", author });
10482
+ }
10483
+ }
10484
+ flushSegment();
10485
+ } else if (generateRedlines && deletionRuns.length > 0) {
10456
10486
  const deletedPPr = addParagraphMarkRevision(sourcePPr, "del", author, revisionIdAllocator);
10457
10487
  const deletedParagraph = serializeToOoxml(deletionRuns, deletedPPr, [], {
10458
10488
  author,
@@ -16650,12 +16680,12 @@ function validateParagraphBoundaryMutation(targetParagraph, modifiedText, option
16650
16680
  }
16651
16681
 
16652
16682
  // core/list-targeting.js
16653
- function getFirstDescendantByLocalName(node, localName2) {
16654
- if (!node || typeof node.getElementsByTagNameNS !== "function") return null;
16655
- const namespaced = node.getElementsByTagNameNS(WORD_MAIN_NS, localName2);
16656
- if (namespaced.length > 0) return namespaced[0];
16657
- const anyNs = node.getElementsByTagNameNS("*", localName2);
16658
- return anyNs.length > 0 ? anyNs[0] : null;
16683
+ function getDirectChildByLocalName(node, localName2) {
16684
+ if (!node) return null;
16685
+ for (const child of Array.from(node.childNodes || [])) {
16686
+ if (child?.nodeType === 1 && child.localName === localName2) return child;
16687
+ }
16688
+ return null;
16659
16689
  }
16660
16690
  function readValAttribute(element) {
16661
16691
  if (!element) return null;
@@ -16772,15 +16802,15 @@ function buildListEntriesForInsertion(parsedItems, normalizedTargetText, anchorL
16772
16802
  }
16773
16803
  function getParagraphListInfo(paragraph) {
16774
16804
  if (!paragraph) return null;
16775
- const pPr = getFirstDescendantByLocalName(paragraph, "pPr");
16805
+ const pPr = getDirectChildByLocalName(paragraph, "pPr");
16776
16806
  if (!pPr) return null;
16777
- const numPr = getFirstDescendantByLocalName(pPr, "numPr");
16807
+ const numPr = getDirectChildByLocalName(pPr, "numPr");
16778
16808
  if (!numPr) return null;
16779
- const numIdEl = getFirstDescendantByLocalName(numPr, "numId");
16809
+ const numIdEl = getDirectChildByLocalName(numPr, "numId");
16780
16810
  if (!numIdEl) return null;
16781
16811
  const numId = readValAttribute(numIdEl);
16782
16812
  if (!numId || numId === "0") return null;
16783
- const ilvlEl = getFirstDescendantByLocalName(numPr, "ilvl");
16813
+ const ilvlEl = getDirectChildByLocalName(numPr, "ilvl");
16784
16814
  const ilvlRaw = readValAttribute(ilvlEl);
16785
16815
  const ilvl = Number.parseInt(ilvlRaw || "0", 10);
16786
16816
  return {
@@ -17190,8 +17220,13 @@ function enforceListBindingOnParagraphNodes(nodes, options = {}) {
17190
17220
  }
17191
17221
  return updated;
17192
17222
  }
17223
+ function parseOxmlFragment(oxml) {
17224
+ const text2 = String(oxml || "");
17225
+ const declared = /xmlns:w\s*=/.test(text2);
17226
+ return parseOoxmlSafe(declared ? text2 : `<w:root xmlns:w="${NS_W}">${text2}</w:root>`, "application/xml").doc;
17227
+ }
17193
17228
  function getFirstParagraphFromOxml(oxml) {
17194
- const doc = parseOoxmlSafe(oxml, "application/xml").doc;
17229
+ const doc = parseOxmlFragment(oxml);
17195
17230
  if (!doc) return null;
17196
17231
  const parseError = getXmlParseError(doc);
17197
17232
  if (parseError) return null;
@@ -17218,17 +17253,17 @@ function setElementVal(element, value) {
17218
17253
  element.setAttribute("w:val", String(value));
17219
17254
  }
17220
17255
  function extractFirstParagraphNumIdFromOxml(oxml) {
17221
- const doc = parseOoxmlSafe(oxml, "application/xml").doc;
17256
+ const doc = parseOxmlFragment(oxml);
17222
17257
  if (!doc) return null;
17223
17258
  const parseError = getXmlParseError(doc);
17224
17259
  if (parseError) return null;
17225
17260
  const paragraphs = getDocumentParagraphNodes(doc);
17226
- const firstParagraph = paragraphs[0] || null;
17227
- if (!firstParagraph) return null;
17228
- const numIdNodes = Array.from(firstParagraph.getElementsByTagNameNS("*", "numId"));
17229
- for (const numIdNode of numIdNodes) {
17230
- const numId = getElementId(numIdNode, ["w:val", "val"]);
17231
- if (numId != null) return String(numId);
17261
+ for (const paragraph of paragraphs) {
17262
+ const numIdNodes = Array.from(paragraph.getElementsByTagNameNS("*", "numId"));
17263
+ for (const numIdNode of numIdNodes) {
17264
+ const numId = getElementId(numIdNode, ["w:val", "val"]);
17265
+ if (numId != null) return String(numId);
17266
+ }
17232
17267
  }
17233
17268
  return null;
17234
17269
  }
@@ -17724,6 +17759,8 @@ function createDynamicNumberingIdState(numberingXml, options = {}) {
17724
17759
  const baseNumId = Math.max(minId, maxUsedNumId + 1);
17725
17760
  const baseAbstractNumId = Math.max(minId, maxUsedAbstractNumId + 1);
17726
17761
  return {
17762
+ // Kept so list edits can recognise which existing definition a source list uses and continue it.
17763
+ sourceNumberingXml: String(numberingXml || ""),
17727
17764
  nextNumId: nextAvailableId(baseNumId, usedNumIds, maxPreferred),
17728
17765
  nextAbstractNumId: nextAvailableId(baseAbstractNumId, usedAbstractNumIds, maxPreferred),
17729
17766
  usedNumIds,
@@ -17864,6 +17901,82 @@ function buildExplicitDecimalMultilevelNumberingXml(numId, abstractNumId, startA
17864
17901
  </w:num>
17865
17902
  </w:numbering>`.trim();
17866
17903
  }
17904
+ function isTrackedDeletedParagraph(node) {
17905
+ if (!node || node.nodeType !== 1 || node.localName !== "p") return false;
17906
+ const pPr = Array.from(node.childNodes || []).find((child) => isDirectWordChild(child, "pPr"));
17907
+ const rPr = pPr && Array.from(pPr.childNodes || []).find((child) => isDirectWordChild(child, "rPr"));
17908
+ return !!rPr && Array.from(rPr.childNodes || []).some((child) => isDirectWordChild(child, "del"));
17909
+ }
17910
+ function paragraphNumIdNodes(node) {
17911
+ return Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []).filter((numIdNode) => numIdNode.parentNode?.localName === "numPr");
17912
+ }
17913
+ function levelFormats(abstractNum) {
17914
+ const formats = /* @__PURE__ */ new Map();
17915
+ for (const lvl of Array.from(abstractNum?.getElementsByTagNameNS?.("*", "lvl") || [])) {
17916
+ const ilvl = getElementId2(lvl, ["w:ilvl", "ilvl"]);
17917
+ const fmt = Array.from(lvl.getElementsByTagNameNS("*", "numFmt"))[0]?.getAttribute("w:val") || null;
17918
+ if (ilvl != null && fmt) formats.set(ilvl, fmt);
17919
+ }
17920
+ return formats;
17921
+ }
17922
+ function normalizeListFormat(format) {
17923
+ return format === "bullet" || format === "circle" || format === "square" ? "bullet" : format;
17924
+ }
17925
+ function abstractNumForNumId(numberingDoc, numId) {
17926
+ const num = Array.from(numberingDoc.getElementsByTagNameNS("*", "num")).find((candidate) => getElementId2(candidate, ["w:numId", "numId"]) === numId);
17927
+ const abstractId = num && getElementId2(Array.from(num.getElementsByTagNameNS("*", "abstractNumId"))[0], ["w:val", "val"]);
17928
+ if (abstractId == null) return null;
17929
+ return Array.from(numberingDoc.getElementsByTagNameNS("*", "abstractNum")).find((candidate) => getElementId2(candidate, ["w:abstractNumId", "abstractNumId"]) === abstractId) || null;
17930
+ }
17931
+ function reuseSourceListNumbering(numberingXml, replacementNodes, sourceParagraphs, numberingIdState) {
17932
+ const sourceXml = numberingIdState?.sourceNumberingXml;
17933
+ if (!numberingXml || !String(sourceXml || "").trim() || !Array.isArray(replacementNodes)) return null;
17934
+ const sourceNumIds = /* @__PURE__ */ new Set();
17935
+ for (const paragraph of sourceParagraphs || []) {
17936
+ for (const numIdNode of paragraphNumIdNodes(paragraph)) {
17937
+ const id = getElementId2(numIdNode, ["w:val", "val"]);
17938
+ if (id != null && id !== 0) sourceNumIds.add(id);
17939
+ }
17940
+ }
17941
+ if (sourceNumIds.size !== 1) return null;
17942
+ const [sourceNumId] = sourceNumIds;
17943
+ const sourceDoc = parseOoxml(sourceXml);
17944
+ const payloadDoc = parseOoxml(numberingXml);
17945
+ if (hasXmlParseError(sourceDoc) || hasXmlParseError(payloadDoc)) return null;
17946
+ const sourceAbstract = abstractNumForNumId(sourceDoc, sourceNumId);
17947
+ if (!sourceAbstract) return null;
17948
+ const sourceFormats = levelFormats(sourceAbstract);
17949
+ const newParagraphNumIds = [];
17950
+ for (const node of replacementNodes) {
17951
+ if (isTrackedDeletedParagraph(node)) continue;
17952
+ for (const numIdNode of paragraphNumIdNodes(node)) {
17953
+ const ilvlNode = Array.from(numIdNode.parentNode.childNodes || []).find((child) => child.localName === "ilvl");
17954
+ newParagraphNumIds.push({
17955
+ numId: getElementId2(numIdNode, ["w:val", "val"]),
17956
+ ilvl: ilvlNode ? getElementId2(ilvlNode, ["w:val", "val"]) ?? 0 : 0
17957
+ });
17958
+ }
17959
+ }
17960
+ if (newParagraphNumIds.length === 0) return null;
17961
+ const payloadFormats = /* @__PURE__ */ new Map();
17962
+ for (const { numId, ilvl } of newParagraphNumIds) {
17963
+ if (numId == null) return null;
17964
+ if (!payloadFormats.has(numId)) {
17965
+ const abstract = abstractNumForNumId(payloadDoc, numId);
17966
+ if (!abstract) return null;
17967
+ payloadFormats.set(numId, levelFormats(abstract));
17968
+ }
17969
+ const generated = payloadFormats.get(numId).get(ilvl);
17970
+ const existing = sourceFormats.get(ilvl);
17971
+ if (!generated || !existing || normalizeListFormat(generated) !== normalizeListFormat(existing)) return null;
17972
+ }
17973
+ const clonedNodes = replacementNodes.map((node) => node?.cloneNode ? node.cloneNode(true) : node);
17974
+ for (const node of clonedNodes) {
17975
+ if (isTrackedDeletedParagraph(node)) continue;
17976
+ for (const numIdNode of paragraphNumIdNodes(node)) setElementVal2(numIdNode, sourceNumId);
17977
+ }
17978
+ return { numId: sourceNumId, replacementNodes: clonedNodes };
17979
+ }
17867
17980
  function remapNumberingPayloadForDocument(numberingXml, replacementNodes, numberingIdState) {
17868
17981
  const numberingDoc = parseOoxml(numberingXml);
17869
17982
  if (hasXmlParseError(numberingDoc)) {
@@ -17901,6 +18014,7 @@ function remapNumberingPayloadForDocument(numberingXml, replacementNodes, number
17901
18014
  }
17902
18015
  const clonedNodes = Array.isArray(replacementNodes) ? replacementNodes.map((node) => node?.cloneNode ? node.cloneNode(true) : node) : [];
17903
18016
  for (const node of clonedNodes) {
18017
+ if (isTrackedDeletedParagraph(node)) continue;
17904
18018
  const numIdNodes = Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []);
17905
18019
  for (const numIdNode of numIdNodes) {
17906
18020
  const oldNumRef = getElementId2(numIdNode, ["w:val", "val"]);
@@ -18721,6 +18835,8 @@ function areRevisionTokensEqual(a, b) {
18721
18835
  var attr2 = (node, name) => node?.getAttribute?.(`w:${name}`) || node?.getAttribute?.(name) || "";
18722
18836
  var descendants = (node, name) => Array.from(node?.getElementsByTagNameNS?.(NS_W, name) || []);
18723
18837
  var first = (node, name) => descendants(node, name)[0] || null;
18838
+ var directChild2 = (node, name) => Array.from(node?.childNodes || []).find((child) => child?.nodeType === 1 && child.localName === name && (!child.namespaceURI || child.namespaceURI === NS_W)) || null;
18839
+ var currentPPr = (paragraph) => directChild2(paragraph, "pPr");
18724
18840
  function hasAncestor(node, localName2) {
18725
18841
  let cursor = node?.parentNode;
18726
18842
  while (cursor) {
@@ -18736,11 +18852,11 @@ function parseXml2(xml, partName, required = false) {
18736
18852
  return { doc: parsed.doc, warnings: parsed.warnings || [] };
18737
18853
  }
18738
18854
  function paragraphListProperties(paragraph) {
18739
- const numPr = first(first(paragraph, "pPr"), "numPr");
18855
+ const numPr = directChild2(currentPPr(paragraph), "numPr");
18740
18856
  if (!numPr) return null;
18741
- const numId = attr2(first(numPr, "numId"), "val");
18857
+ const numId = attr2(directChild2(numPr, "numId"), "val");
18742
18858
  if (!numId || numId === "0") return null;
18743
- return { numId, level: Number.parseInt(attr2(first(numPr, "ilvl"), "val") || "0", 10) || 0 };
18859
+ return { numId, level: Number.parseInt(attr2(directChild2(numPr, "ilvl"), "val") || "0", 10) || 0 };
18744
18860
  }
18745
18861
  function parseNumbering(numberingDoc) {
18746
18862
  const abstracts = /* @__PURE__ */ new Map();
@@ -18825,11 +18941,11 @@ function createNumberingResolver(numberingDoc) {
18825
18941
  };
18826
18942
  }
18827
18943
  function headingLevel(paragraph) {
18828
- const pPr = first(paragraph, "pPr");
18829
- const style = attr2(first(pPr, "pStyle"), "val");
18944
+ const pPr = currentPPr(paragraph);
18945
+ const style = attr2(directChild2(pPr, "pStyle"), "val");
18830
18946
  const match = style.match(/^heading\s*([1-9])$/i);
18831
18947
  if (match) return Math.min(Number(match[1]), 6);
18832
- const outline = Number.parseInt(attr2(first(pPr, "outlineLvl"), "val"), 10);
18948
+ const outline = Number.parseInt(attr2(directChild2(pPr, "outlineLvl"), "val"), 10);
18833
18949
  return Number.isInteger(outline) ? Math.min(outline + 1, 6) : null;
18834
18950
  }
18835
18951
  function structuralContext(paragraph, text2) {
@@ -19008,7 +19124,7 @@ function inspectDocumentParts(parts, options = {}) {
19008
19124
  const ids = [...new Set([...descendants(paragraph, "commentRangeStart"), ...descendants(paragraph, "commentReference")].map((node) => attr2(node, "id")).filter(Boolean))];
19009
19125
  const authors = revisionAuthors(paragraph);
19010
19126
  const list = resolveNumbering(paragraphListProperties(paragraph));
19011
- const styleId = attr2(first(first(paragraph, "pPr"), "pStyle"), "val") || null;
19127
+ const styleId = attr2(directChild2(currentPPr(paragraph), "pStyle"), "val") || null;
19012
19128
  const structure = structuralContext(paragraph, text2);
19013
19129
  const index = zeroIndex + 1;
19014
19130
  const provision = list?.label && list.format !== "bullet" ? list.label : null;
@@ -24409,7 +24525,9 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
24409
24525
  paragraphText,
24410
24526
  generateRedlines ? createRevisionMetadata(author, xmlDoc) : null,
24411
24527
  author,
24412
- { generateRedlines }
24528
+ // A wholly inserted paragraph must track its own paragraph mark; otherwise Word's
24529
+ // Reject All removes the text but leaves an empty paragraph behind.
24530
+ { generateRedlines, trackParagraphMark: true }
24413
24531
  );
24414
24532
  const imported = xmlDoc.importNode(plainParagraph, true);
24415
24533
  ensureParagraphIdsOnImportedNode(imported, operationSession);
@@ -24540,6 +24658,14 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
24540
24658
  const extracted = extractReplacementNodes(result.oxml);
24541
24659
  let replacementNodes = removeListPackagingSentinel(extracted.replacementNodes, result.warnings);
24542
24660
  let numberingXml = extracted.numberingXml;
24661
+ if (numberingXml && runtimeContext?.numberingIdState && !useTableScope) {
24662
+ const sourceParagraphs = useListScope ? listScopeEdit.paragraphs : explicitRangeParagraphs || [targetParagraph];
24663
+ const continued = reuseSourceListNumbering(numberingXml, replacementNodes, sourceParagraphs, runtimeContext.numberingIdState);
24664
+ if (continued) {
24665
+ replacementNodes = continued.replacementNodes;
24666
+ numberingXml = null;
24667
+ }
24668
+ }
24543
24669
  if (numberingXml && runtimeContext?.numberingIdState) {
24544
24670
  const normalizedNumbering = remapNumberingPayloadForDocument(numberingXml, replacementNodes, runtimeContext.numberingIdState);
24545
24671
  replacementNodes = normalizedNumbering.replacementNodes;