@ansonlai/docx-redline-js 0.8.3 → 0.8.4
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 +627 -586
- package/README.md +905 -901
- package/dist/docx-redline-js.esm.js +771 -609
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +92 -89
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/dist/docx-redline.bundle.cjs +771 -609
- package/dist/docx-redline.bundle.cjs.map +4 -4
- package/dist/docx-redline.bundle.js +771 -609
- package/dist/docx-redline.bundle.js.map +4 -4
- package/docs/AGENT_KNOWLEDGE_BASE.md +4 -1
- package/docs/schemas/document-operations.schema.json +168 -168
- package/engine/oxml-engine.js +215 -195
- package/engine/reconstruction-mode.js +183 -183
- package/index.d.ts +4 -0
- package/index.js +36 -1
- package/node/cli.js +2 -1
- package/package.json +1 -1
- package/pipeline/serialization.js +111 -94
- package/services/batch-operation-orchestrator.js +6 -0
- package/services/document-operation-applier.js +2 -0
- package/services/document-operation-contract.js +553 -537
- package/services/document-operation-mutations.js +2721 -2603
- package/services/operation-batch-compiler.js +440 -436
- package/services/operation-heuristics.js +197 -173
- package/services/revision-comment-management.js +601 -594
- package/services/standalone-operation-runner.d.ts +429 -427
- package/services/table-reconciliation.js +48 -46
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ansonlai/docx-redline-js standalone CommonJS bundle v0.8.
|
|
1
|
+
// @ansonlai/docx-redline-js standalone CommonJS bundle v0.8.4 — 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;
|
|
@@ -9871,7 +9871,14 @@ function buildRunXmlWithHints(item, formatHints, options = {}) {
|
|
|
9871
9871
|
function buildSimpleRun(text2, rPrXml) {
|
|
9872
9872
|
if (!text2) return "";
|
|
9873
9873
|
const rPr = rPrXml || "";
|
|
9874
|
-
return `<w:r>${rPr}
|
|
9874
|
+
return `<w:r>${rPr}${buildRunContent(text2, "w:t")}</w:r>`;
|
|
9875
|
+
}
|
|
9876
|
+
function buildRunContent(text2, textTag) {
|
|
9877
|
+
return String(text2).split(/(\r\n|[\n\r\v\t])/).map((part) => {
|
|
9878
|
+
if (part === " ") return "<w:tab/>";
|
|
9879
|
+
if (part === "\n" || part === "\r" || part === "\r\n" || part === "\v") return "<w:br/>";
|
|
9880
|
+
return part ? `<${textTag} xml:space="preserve">${escapeXml(part)}</${textTag}>` : "";
|
|
9881
|
+
}).join("");
|
|
9875
9882
|
}
|
|
9876
9883
|
function buildDeletionXml(item, options = {}) {
|
|
9877
9884
|
const metadata = createRevisionMetadata(
|
|
@@ -9884,7 +9891,7 @@ function buildDeletionXml(item, options = {}) {
|
|
|
9884
9891
|
if (font) {
|
|
9885
9892
|
rPr = applyFont(rPr, font);
|
|
9886
9893
|
}
|
|
9887
|
-
return `<w:del w:id="${metadata.id}" w:author="${escapeXml(metadata.author)}" w:date="${metadata.date}"><w:r>${rPr}
|
|
9894
|
+
return `<w:del w:id="${metadata.id}" w:author="${escapeXml(metadata.author)}" w:date="${metadata.date}"><w:r>${rPr}${buildRunContent(item.text, "w:delText")}</w:r></w:del>`;
|
|
9888
9895
|
}
|
|
9889
9896
|
function buildInsertionXml(item, formatHints, options = {}) {
|
|
9890
9897
|
const metadata = createRevisionMetadata(
|
|
@@ -10225,7 +10232,7 @@ function generateTableOoxml(tableData, options = {}) {
|
|
|
10225
10232
|
revisionIdAllocator = null,
|
|
10226
10233
|
trackAsBlock = false
|
|
10227
10234
|
} = options;
|
|
10228
|
-
const
|
|
10235
|
+
const trackRows = generateRedlines && trackAsBlock;
|
|
10229
10236
|
const numCols = tableData.headers?.length || (tableData.rows?.[0]?.length || 1);
|
|
10230
10237
|
const tblPr = `
|
|
10231
10238
|
<w:tblPr>
|
|
@@ -10253,14 +10260,16 @@ function generateTableOoxml(tableData, options = {}) {
|
|
|
10253
10260
|
const cellText = row[c] || "";
|
|
10254
10261
|
const { cleanText, formatHints } = preprocessMarkdown(cellText);
|
|
10255
10262
|
const runModel = [{
|
|
10256
|
-
kind: generateRedlines
|
|
10263
|
+
kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
|
|
10257
10264
|
text: cleanText,
|
|
10258
10265
|
rPrXml: isHeaderRow ? "<w:rPr><w:b/></w:rPr>" : "",
|
|
10259
10266
|
author,
|
|
10260
10267
|
startOffset: 0,
|
|
10261
10268
|
endOffset: cleanText.length
|
|
10262
10269
|
}];
|
|
10263
|
-
const
|
|
10270
|
+
const cellMarkMeta = trackRows ? createRevisionMetadata(author, revisionIdAllocator) : null;
|
|
10271
|
+
const cellPPr = cellMarkMeta ? `<w:pPr><w:rPr><w:ins w:id="${cellMarkMeta.id}" w:author="${escapeXml(cellMarkMeta.author)}" w:date="${cellMarkMeta.date}"/></w:rPr></w:pPr>` : null;
|
|
10272
|
+
const runsOoxml = serializeToOoxml(runModel, cellPPr, formatHints, {
|
|
10264
10273
|
author,
|
|
10265
10274
|
generateRedlines,
|
|
10266
10275
|
revisionIdAllocator
|
|
@@ -10269,13 +10278,11 @@ function generateTableOoxml(tableData, options = {}) {
|
|
|
10269
10278
|
cellsXml += `<w:tc>${tcPr}${runsOoxml}</w:tc>`;
|
|
10270
10279
|
}
|
|
10271
10280
|
const trPr = isHeaderRow ? "<w:trPr><w:tblHeader/><w:cantSplit/></w:trPr>" : "<w:trPr><w:cantSplit/></w:trPr>";
|
|
10272
|
-
|
|
10273
|
-
|
|
10274
|
-
|
|
10275
|
-
if (tableInsertMeta) {
|
|
10276
|
-
tableXml = `<w:ins w:id="${tableInsertMeta.id}" w:author="${escapeXml(tableInsertMeta.author)}" w:date="${tableInsertMeta.date}">${tableXml}</w:ins>`;
|
|
10281
|
+
const rowMeta = trackRows ? createRevisionMetadata(author, revisionIdAllocator) : null;
|
|
10282
|
+
const trackedTrPr = rowMeta ? trPr.replace("</w:trPr>", `<w:ins w:id="${rowMeta.id}" w:author="${escapeXml(rowMeta.author)}" w:date="${rowMeta.date}"/></w:trPr>`) : trPr;
|
|
10283
|
+
rowsXml += `<w:tr>${trackedTrPr}${cellsXml}</w:tr>`;
|
|
10277
10284
|
}
|
|
10278
|
-
return
|
|
10285
|
+
return `<w:tbl>${tblPr}${tblGrid}${rowsXml}</w:tbl>`;
|
|
10279
10286
|
}
|
|
10280
10287
|
function diffTablesWithVirtualGrid(oldGrid, newTableData) {
|
|
10281
10288
|
const operations = [];
|
|
@@ -15199,6 +15206,11 @@ function rejectTrackedChangesInOoxml(oxml, options = {}) {
|
|
|
15199
15206
|
}
|
|
15200
15207
|
if (removeNode(insNode)) rejectedCount += 1;
|
|
15201
15208
|
}
|
|
15209
|
+
for (const tableNode of getWordElementsByLocalName(xmlDoc, "tbl")) {
|
|
15210
|
+
if (!tableNode.parentNode) continue;
|
|
15211
|
+
const hasRow = Array.from(tableNode.childNodes || []).some((child) => isWordElement5(child, "tr"));
|
|
15212
|
+
if (!hasRow && removeNode(tableNode)) rejectedCount += 1;
|
|
15213
|
+
}
|
|
15202
15214
|
for (const delNode of getWordElementsByLocalName(xmlDoc, "del")) {
|
|
15203
15215
|
if (!delNode.parentNode || !authorMatchesNode(delNode, filter)) continue;
|
|
15204
15216
|
if (isParagraphMarkRevisionMarker(delNode)) {
|
|
@@ -15828,7 +15840,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
15828
15840
|
operationWarnings.push("Input was sanitized; pass sanitizeInput: false to disable.");
|
|
15829
15841
|
}
|
|
15830
15842
|
let structuredAnalysis = null;
|
|
15831
|
-
if (options.structuredContent !== false) {
|
|
15843
|
+
if (options.structuredContent !== false && options.literalTextEdit !== true) {
|
|
15832
15844
|
structuredAnalysis = analyzeStructuredContent(sanitizedText);
|
|
15833
15845
|
if (!structuredAnalysis.valid) {
|
|
15834
15846
|
if (options.explicitStructuredContent === true) {
|
|
@@ -15847,13 +15859,16 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
15847
15859
|
}
|
|
15848
15860
|
}
|
|
15849
15861
|
const { cleanText: cleanModifiedText, formatHints } = preprocessMarkdown(sanitizedText);
|
|
15850
|
-
const hasTextChanges = existingRevisionsPolicy === "slice-cross-author" ? cleanModifiedText !== originalText : cleanModifiedText.trim() !== originalText.trim();
|
|
15851
|
-
const hasFormatHints = formatHints.length > 0;
|
|
15852
15862
|
const { existingFormatHints, textSpans, paragraphs } = extractFormattingFromOoxml(xmlDoc);
|
|
15863
|
+
const hasLiteralTextChanges = existingRevisionsPolicy === "slice-cross-author" ? cleanModifiedText !== originalText : cleanModifiedText.trim() !== originalText.trim();
|
|
15864
|
+
const hasListStructureChange = !hasLiteralTextChanges && options.explicitStructuredContent === true && isListTargetLoose(cleanModifiedText) && !sourceMatchesListShape(paragraphs, cleanModifiedText);
|
|
15865
|
+
const hasTextChanges = hasLiteralTextChanges || hasListStructureChange;
|
|
15866
|
+
const hasFormatHints = formatHints.length > 0;
|
|
15853
15867
|
const hasExistingFormatting = existingFormatHints.length > 0;
|
|
15854
|
-
const
|
|
15868
|
+
const { textSpans: targetSpans } = buildTextSpansFromParagraphs(paragraphs);
|
|
15869
|
+
const visibleText = targetSpans.map((span) => textSpanVisibleText(span)).join("");
|
|
15855
15870
|
const targetFound = originalText.includes("\n") || originalText.includes("\r") ? originalText.split(/\r?\n/).map(normalizeTargetText).filter(Boolean).every((line) => paragraphs.some((paragraph) => {
|
|
15856
|
-
const paragraphText =
|
|
15871
|
+
const paragraphText = targetSpans.filter((span) => span.paragraph === paragraph).map(textSpanVisibleText).join("");
|
|
15857
15872
|
return normalizeTargetText(paragraphText).includes(line);
|
|
15858
15873
|
})) : visibleText.includes(originalText.trim()) || visibleText.replace(/[\t\n\u2011]/g, "").includes(originalText.trim().replace(/[\t\n\u2011]/g, "")) || normalizeTargetText(visibleText).includes(normalizeTargetText(originalText));
|
|
15859
15874
|
if (hasTextChanges && typeof originalText === "string" && originalText.trim() && !targetFound) {
|
|
@@ -15967,7 +15982,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
15967
15982
|
const tables = getElementsByTagNSOrTag(xmlDoc, NS_W, "tbl");
|
|
15968
15983
|
const hasTables = tables.length > 0;
|
|
15969
15984
|
const isMarkdownTable = /^\|.+\|/.test(cleanModifiedText.trim()) && cleanModifiedText.includes("\n");
|
|
15970
|
-
const isTargetList = isListTargetLoose(cleanModifiedText);
|
|
15985
|
+
const isTargetList = options.literalTextEdit !== true && isListTargetLoose(cleanModifiedText);
|
|
15971
15986
|
const isStructuredContent = options.structuredContent !== false && structuredAnalysis?.requiresStructuredContent === true;
|
|
15972
15987
|
const tableCellContext = initialTableCellContext;
|
|
15973
15988
|
const usesSurgicalTextMode = hasTables || existingRevisionsPolicy === "slice-cross-author";
|
|
@@ -16082,6 +16097,10 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
16082
16097
|
throw caught;
|
|
16083
16098
|
}
|
|
16084
16099
|
}
|
|
16100
|
+
function sourceMatchesListShape(paragraphs, listText) {
|
|
16101
|
+
const itemCount = String(listText).split(/\r?\n/).filter((line) => line.trim()).length;
|
|
16102
|
+
return paragraphs.length === itemCount && paragraphs.every((paragraph) => getElementsByTagNSOrTag(paragraph, NS_W, "numPr").length > 0);
|
|
16103
|
+
}
|
|
16085
16104
|
function normalizeTargetText(text2) {
|
|
16086
16105
|
return String(text2 || "").replace(/[\t\n\u2011]/g, " ").replace(/\s+/g, " ").trim();
|
|
16087
16106
|
}
|
|
@@ -17551,659 +17570,659 @@ async function executeSingleLineListStructuralFallback(plan, options = {}) {
|
|
|
17551
17570
|
};
|
|
17552
17571
|
}
|
|
17553
17572
|
|
|
17554
|
-
//
|
|
17555
|
-
|
|
17556
|
-
|
|
17557
|
-
|
|
17558
|
-
|
|
17559
|
-
|
|
17560
|
-
|
|
17561
|
-
|
|
17562
|
-
}
|
|
17563
|
-
|
|
17564
|
-
return node.getAttribute(`w:${name}`) || node.getAttribute(name) || "";
|
|
17565
|
-
}
|
|
17566
|
-
function xmlSpaceAttribute(node) {
|
|
17567
|
-
return node.getAttribute("xml:space") || node.getAttribute("space") || node.getAttributeNS?.("http://www.w3.org/XML/1998/namespace", "space") || "";
|
|
17568
|
-
}
|
|
17569
|
-
function isParagraphMarkRevision(node) {
|
|
17570
|
-
return localNameOf3(node.parentNode) === "rPr";
|
|
17573
|
+
// services/numbering-helpers.js
|
|
17574
|
+
function parseIntegerAttribute(element, names) {
|
|
17575
|
+
if (!element || !Array.isArray(names)) return null;
|
|
17576
|
+
for (const name of names) {
|
|
17577
|
+
const raw = element.getAttribute(name);
|
|
17578
|
+
if (raw == null || raw === "") continue;
|
|
17579
|
+
const parsed = Number.parseInt(String(raw), 10);
|
|
17580
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
17581
|
+
}
|
|
17582
|
+
return null;
|
|
17571
17583
|
}
|
|
17572
|
-
function
|
|
17573
|
-
|
|
17574
|
-
|
|
17575
|
-
|
|
17576
|
-
|
|
17577
|
-
|
|
17584
|
+
function nextAvailableId(startId, occupiedIds, maxPreferred = null) {
|
|
17585
|
+
let candidate = Number.isInteger(startId) && startId > 0 ? startId : 1;
|
|
17586
|
+
const occupied = occupiedIds instanceof Set ? occupiedIds : /* @__PURE__ */ new Set();
|
|
17587
|
+
while (occupied.has(candidate)) {
|
|
17588
|
+
candidate += 1;
|
|
17589
|
+
}
|
|
17590
|
+
if (Number.isInteger(maxPreferred) && maxPreferred > 0 && candidate > maxPreferred) {
|
|
17591
|
+
for (let probe = 1; probe <= maxPreferred; probe += 1) {
|
|
17592
|
+
if (!occupied.has(probe)) return probe;
|
|
17578
17593
|
}
|
|
17579
|
-
|
|
17580
|
-
|
|
17581
|
-
|
|
17582
|
-
|
|
17583
|
-
|
|
17594
|
+
}
|
|
17595
|
+
return candidate;
|
|
17596
|
+
}
|
|
17597
|
+
function createDynamicNumberingIdState(numberingXml, options = {}) {
|
|
17598
|
+
const minId = Number.isInteger(options?.minId) && options.minId > 0 ? options.minId : 1;
|
|
17599
|
+
const maxPreferred = Number.isInteger(options?.maxPreferred) && options.maxPreferred >= minId ? options.maxPreferred : 32767;
|
|
17600
|
+
const usedNumIds = /* @__PURE__ */ new Set();
|
|
17601
|
+
const usedAbstractNumIds = /* @__PURE__ */ new Set();
|
|
17602
|
+
if (String(numberingXml || "").trim()) {
|
|
17584
17603
|
try {
|
|
17585
|
-
|
|
17586
|
-
|
|
17587
|
-
|
|
17604
|
+
const numberingDoc = parseOoxml(numberingXml);
|
|
17605
|
+
const abstractNums = Array.from(numberingDoc.getElementsByTagNameNS("*", "abstractNum"));
|
|
17606
|
+
const nums = Array.from(numberingDoc.getElementsByTagNameNS("*", "num"));
|
|
17607
|
+
for (const abstractNum of abstractNums) {
|
|
17608
|
+
const id = parseIntegerAttribute(abstractNum, ["w:abstractNumId", "abstractNumId"]);
|
|
17609
|
+
if (id != null) usedAbstractNumIds.add(id);
|
|
17610
|
+
}
|
|
17611
|
+
for (const num of nums) {
|
|
17612
|
+
const id = parseIntegerAttribute(num, ["w:numId", "numId"]);
|
|
17613
|
+
if (id != null) usedNumIds.add(id);
|
|
17614
|
+
}
|
|
17615
|
+
} catch {
|
|
17588
17616
|
}
|
|
17589
17617
|
}
|
|
17618
|
+
const maxUsedNumId = usedNumIds.size > 0 ? Math.max(...usedNumIds) : 0;
|
|
17619
|
+
const maxUsedAbstractNumId = usedAbstractNumIds.size > 0 ? Math.max(...usedAbstractNumIds) : 0;
|
|
17620
|
+
const baseNumId = Math.max(minId, maxUsedNumId + 1);
|
|
17621
|
+
const baseAbstractNumId = Math.max(minId, maxUsedAbstractNumId + 1);
|
|
17622
|
+
return {
|
|
17623
|
+
// Kept so list edits can recognise which existing definition a source list uses and continue it.
|
|
17624
|
+
sourceNumberingXml: String(numberingXml || ""),
|
|
17625
|
+
nextNumId: nextAvailableId(baseNumId, usedNumIds, maxPreferred),
|
|
17626
|
+
nextAbstractNumId: nextAvailableId(baseAbstractNumId, usedAbstractNumIds, maxPreferred),
|
|
17627
|
+
usedNumIds,
|
|
17628
|
+
usedAbstractNumIds,
|
|
17629
|
+
minId,
|
|
17630
|
+
maxPreferred
|
|
17631
|
+
};
|
|
17590
17632
|
}
|
|
17591
|
-
function
|
|
17592
|
-
|
|
17593
|
-
|
|
17594
|
-
|
|
17595
|
-
if (!
|
|
17596
|
-
|
|
17597
|
-
return { valid: false, issues };
|
|
17633
|
+
function normalizeNumberingIdState(state) {
|
|
17634
|
+
if (!state || typeof state !== "object") return null;
|
|
17635
|
+
if (!(state.usedNumIds instanceof Set)) state.usedNumIds = /* @__PURE__ */ new Set();
|
|
17636
|
+
if (!(state.usedAbstractNumIds instanceof Set)) state.usedAbstractNumIds = /* @__PURE__ */ new Set();
|
|
17637
|
+
if (!Number.isInteger(state.minId) || state.minId < 1) {
|
|
17638
|
+
state.minId = 1;
|
|
17598
17639
|
}
|
|
17599
|
-
|
|
17600
|
-
|
|
17601
|
-
addIssue("PARSE_ERROR", "error", `OOXML does not parse as XML: ${error2}`);
|
|
17602
|
-
return { valid: false, issues };
|
|
17640
|
+
if (!Number.isInteger(state.maxPreferred) || state.maxPreferred < state.minId) {
|
|
17641
|
+
state.maxPreferred = 32767;
|
|
17603
17642
|
}
|
|
17604
|
-
|
|
17605
|
-
|
|
17606
|
-
const revisions = insElements.concat(delElements);
|
|
17607
|
-
for (const paragraph of elementsByLocalName(doc, "p")) {
|
|
17608
|
-
const nested = Array.from(paragraph.getElementsByTagName("*")).find((el) => el !== paragraph && localNameOf3(el) === "p");
|
|
17609
|
-
if (nested) {
|
|
17610
|
-
addIssue("NESTED_PARAGRAPH", "error", `<${paragraph.nodeName}> contains nested <${nested.nodeName}>.`);
|
|
17611
|
-
}
|
|
17643
|
+
if (!Number.isInteger(state.nextNumId) || state.nextNumId < state.minId) {
|
|
17644
|
+
state.nextNumId = state.minId;
|
|
17612
17645
|
}
|
|
17613
|
-
|
|
17614
|
-
|
|
17615
|
-
const sectPrIndexes = children.map((child, index) => localNameOf3(child) === "sectPr" ? index : -1).filter((index) => index >= 0);
|
|
17616
|
-
if (sectPrIndexes.length > 1) {
|
|
17617
|
-
addIssue("MULTIPLE_BODY_SECTPR", "error", "<w:body> contains multiple direct <w:sectPr> elements.");
|
|
17618
|
-
} else if (sectPrIndexes.length === 1 && sectPrIndexes[0] !== children.length - 1) {
|
|
17619
|
-
addIssue("SECTPR_NOT_LAST", "error", "<w:sectPr> is not the last element child of <w:body>.");
|
|
17620
|
-
}
|
|
17646
|
+
if (!Number.isInteger(state.nextAbstractNumId) || state.nextAbstractNumId < state.minId) {
|
|
17647
|
+
state.nextAbstractNumId = state.minId;
|
|
17621
17648
|
}
|
|
17622
|
-
|
|
17623
|
-
|
|
17624
|
-
|
|
17625
|
-
|
|
17626
|
-
|
|
17627
|
-
|
|
17628
|
-
|
|
17629
|
-
|
|
17630
|
-
|
|
17631
|
-
|
|
17632
|
-
|
|
17633
|
-
|
|
17634
|
-
|
|
17649
|
+
state.nextNumId = nextAvailableId(state.nextNumId, state.usedNumIds, state.maxPreferred);
|
|
17650
|
+
state.nextAbstractNumId = nextAvailableId(state.nextAbstractNumId, state.usedAbstractNumIds, state.maxPreferred);
|
|
17651
|
+
return state;
|
|
17652
|
+
}
|
|
17653
|
+
function reserveNextNumberingId(state, kind = "num") {
|
|
17654
|
+
const normalized = normalizeNumberingIdState(state);
|
|
17655
|
+
if (!normalized) return null;
|
|
17656
|
+
const useAbstract = kind === "abstract";
|
|
17657
|
+
const id = useAbstract ? normalized.nextAbstractNumId : normalized.nextNumId;
|
|
17658
|
+
if (!Number.isInteger(id) || id < 1) return null;
|
|
17659
|
+
if (useAbstract) {
|
|
17660
|
+
normalized.usedAbstractNumIds.add(id);
|
|
17661
|
+
normalized.nextAbstractNumId = nextAvailableId(id + 1, normalized.usedAbstractNumIds, normalized.maxPreferred);
|
|
17662
|
+
} else {
|
|
17663
|
+
normalized.usedNumIds.add(id);
|
|
17664
|
+
normalized.nextNumId = nextAvailableId(id + 1, normalized.usedNumIds, normalized.maxPreferred);
|
|
17635
17665
|
}
|
|
17636
|
-
|
|
17637
|
-
|
|
17638
|
-
|
|
17639
|
-
|
|
17640
|
-
|
|
17641
|
-
|
|
17642
|
-
|
|
17643
|
-
|
|
17644
|
-
|
|
17666
|
+
return id;
|
|
17667
|
+
}
|
|
17668
|
+
function reserveNextNumberingIdPair(state) {
|
|
17669
|
+
const numId = reserveNextNumberingId(state, "num");
|
|
17670
|
+
const abstractNumId = reserveNextNumberingId(state, "abstract");
|
|
17671
|
+
if (numId == null || abstractNumId == null) return null;
|
|
17672
|
+
return { numId, abstractNumId };
|
|
17673
|
+
}
|
|
17674
|
+
function hasXmlParseError(doc) {
|
|
17675
|
+
if (!doc || !doc.documentElement) return true;
|
|
17676
|
+
if (doc.documentElement.localName === "parsererror") return true;
|
|
17677
|
+
return doc.getElementsByTagName("parsererror").length > 0;
|
|
17678
|
+
}
|
|
17679
|
+
function isDirectWordChild(node, localName2) {
|
|
17680
|
+
return !!(node && node.nodeType === 1 && node.namespaceURI === WORD_MAIN_NS && node.localName === localName2);
|
|
17681
|
+
}
|
|
17682
|
+
function insertNumberingNodeInSchemaOrder(root, node, kind) {
|
|
17683
|
+
if (!root || !node) return;
|
|
17684
|
+
const directChildren = Array.from(root.childNodes || []).filter(
|
|
17685
|
+
(child) => child && child.nodeType === 1 && child.namespaceURI === WORD_MAIN_NS
|
|
17686
|
+
);
|
|
17687
|
+
let anchor = null;
|
|
17688
|
+
if (kind === "abstract") {
|
|
17689
|
+
anchor = directChildren.find(
|
|
17690
|
+
(child) => child.localName === "num" || child.localName === "numIdMacAtCleanup"
|
|
17691
|
+
) || null;
|
|
17692
|
+
} else {
|
|
17693
|
+
anchor = directChildren.find((child) => child.localName === "numIdMacAtCleanup") || null;
|
|
17645
17694
|
}
|
|
17646
|
-
|
|
17647
|
-
|
|
17648
|
-
|
|
17649
|
-
|
|
17650
|
-
|
|
17651
|
-
|
|
17652
|
-
|
|
17653
|
-
|
|
17654
|
-
|
|
17655
|
-
|
|
17656
|
-
|
|
17695
|
+
if (anchor) root.insertBefore(node, anchor);
|
|
17696
|
+
else root.appendChild(node);
|
|
17697
|
+
}
|
|
17698
|
+
function getAttributeFirst2(element, names) {
|
|
17699
|
+
for (const name of names || []) {
|
|
17700
|
+
const value = element?.getAttribute?.(name);
|
|
17701
|
+
if (value != null && value !== "") return value;
|
|
17702
|
+
}
|
|
17703
|
+
return null;
|
|
17704
|
+
}
|
|
17705
|
+
function getElementId2(element, names) {
|
|
17706
|
+
const raw = getAttributeFirst2(element, names);
|
|
17707
|
+
const parsed = Number.parseInt(String(raw || ""), 10);
|
|
17708
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
17709
|
+
}
|
|
17710
|
+
function setElementId(element, preferredName, idValue) {
|
|
17711
|
+
element?.setAttribute?.(preferredName, String(idValue));
|
|
17712
|
+
}
|
|
17713
|
+
function setElementVal2(element, value) {
|
|
17714
|
+
element?.setAttribute?.("w:val", String(value));
|
|
17715
|
+
}
|
|
17716
|
+
function overwriteParagraphNumIds(paragraphNodes, targetNumId) {
|
|
17717
|
+
if (!Array.isArray(paragraphNodes) || targetNumId == null) return;
|
|
17718
|
+
for (const node of paragraphNodes) {
|
|
17719
|
+
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []);
|
|
17720
|
+
for (const numIdNode of numIdNodes) {
|
|
17721
|
+
setElementVal2(numIdNode, targetNumId);
|
|
17657
17722
|
}
|
|
17658
17723
|
}
|
|
17659
|
-
|
|
17660
|
-
|
|
17661
|
-
|
|
17662
|
-
const
|
|
17663
|
-
|
|
17664
|
-
|
|
17665
|
-
|
|
17724
|
+
}
|
|
17725
|
+
function extractFirstParagraphNumId(paragraphNodes) {
|
|
17726
|
+
for (const node of paragraphNodes || []) {
|
|
17727
|
+
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []);
|
|
17728
|
+
for (const numIdNode of numIdNodes) {
|
|
17729
|
+
const numId = getElementId2(numIdNode, ["w:val", "val"]);
|
|
17730
|
+
if (numId != null) return String(numId);
|
|
17666
17731
|
}
|
|
17667
|
-
seenIds.add(id);
|
|
17668
17732
|
}
|
|
17669
|
-
|
|
17670
|
-
for (const node of textNodes) {
|
|
17671
|
-
const text2 = node.textContent || "";
|
|
17672
|
-
if (/^\s|\s$/.test(text2) && xmlSpaceAttribute(node) !== "preserve") {
|
|
17673
|
-
addIssue(
|
|
17674
|
-
"MISSING_SPACE_PRESERVE",
|
|
17675
|
-
"error",
|
|
17676
|
-
`<${node.nodeName}> has boundary whitespace without xml:space="preserve".`
|
|
17677
|
-
);
|
|
17678
|
-
}
|
|
17679
|
-
if (text2 === "") {
|
|
17680
|
-
addIssue("EMPTY_TEXT_ELEMENT", "warning", `<${node.nodeName}> is empty.`);
|
|
17681
|
-
}
|
|
17682
|
-
}
|
|
17683
|
-
for (const revision of revisions) {
|
|
17684
|
-
if (isParagraphMarkRevision(revision)) continue;
|
|
17685
|
-
const hasElementChild = Array.from(revision.childNodes || []).some((child) => child.nodeType === 1);
|
|
17686
|
-
if (!hasElementChild) {
|
|
17687
|
-
addIssue(
|
|
17688
|
-
"EMPTY_REVISION_WRAPPER",
|
|
17689
|
-
"warning",
|
|
17690
|
-
`<${revision.nodeName}> (w:id="${wordAttribute3(revision, "id")}") wraps no content.`
|
|
17691
|
-
);
|
|
17692
|
-
}
|
|
17693
|
-
}
|
|
17694
|
-
for (const resurrection of findForeignDeletedParagraphResurrections(doc)) {
|
|
17695
|
-
const ownerAuthor = resurrection.ownerAuthor || "unattributed";
|
|
17696
|
-
addIssue(
|
|
17697
|
-
"FOREIGN_PARAGRAPH_MARK_DELETION",
|
|
17698
|
-
"warning",
|
|
17699
|
-
`Paragraph deleted by ${ownerAuthor} also contains non-empty insertion content from another author; Accept/Reject lifecycle may not preserve the apparent restoration.`
|
|
17700
|
-
);
|
|
17701
|
-
}
|
|
17702
|
-
return { valid: !issues.some((issue) => issue.severity === "error"), issues };
|
|
17733
|
+
return null;
|
|
17703
17734
|
}
|
|
17704
|
-
|
|
17705
|
-
|
|
17706
|
-
|
|
17707
|
-
|
|
17708
|
-
|
|
17709
|
-
|
|
17710
|
-
|
|
17735
|
+
function buildExplicitDecimalMultilevelNumberingXml(numId, abstractNumId, startAt) {
|
|
17736
|
+
const safeNumId = String(numId);
|
|
17737
|
+
const safeAbstractNumId = String(abstractNumId);
|
|
17738
|
+
const safeStartAt = Number.isInteger(startAt) && startAt > 0 ? startAt : 1;
|
|
17739
|
+
const levelsXml = Array.from({ length: 9 }, (_, level) => {
|
|
17740
|
+
const lvlText = Array.from({ length: level + 1 }, (_2, i2) => `%${i2 + 1}`).join(".") + ".";
|
|
17741
|
+
const left = 720 * (level + 1);
|
|
17742
|
+
return `
|
|
17743
|
+
<w:lvl w:ilvl="${level}">
|
|
17744
|
+
<w:start w:val="1"/>
|
|
17745
|
+
<w:numFmt w:val="decimal"/>
|
|
17746
|
+
<w:lvlText w:val="${lvlText}"/>
|
|
17747
|
+
<w:lvlJc w:val="left"/>
|
|
17748
|
+
<w:pPr><w:ind w:left="${left}" w:hanging="360"/></w:pPr>
|
|
17749
|
+
</w:lvl>`;
|
|
17750
|
+
}).join("");
|
|
17751
|
+
return `
|
|
17752
|
+
<w:numbering xmlns:w="${WORD_MAIN_NS}">
|
|
17753
|
+
<w:abstractNum w:abstractNumId="${safeAbstractNumId}">
|
|
17754
|
+
<w:multiLevelType w:val="multilevel"/>
|
|
17755
|
+
${levelsXml}
|
|
17756
|
+
</w:abstractNum>
|
|
17757
|
+
<w:num w:numId="${safeNumId}">
|
|
17758
|
+
<w:abstractNumId w:val="${safeAbstractNumId}"/>
|
|
17759
|
+
<w:lvlOverride w:ilvl="0">
|
|
17760
|
+
<w:startOverride w:val="${safeStartAt}"/>
|
|
17761
|
+
</w:lvlOverride>
|
|
17762
|
+
</w:num>
|
|
17763
|
+
</w:numbering>`.trim();
|
|
17711
17764
|
}
|
|
17712
|
-
function
|
|
17713
|
-
|
|
17765
|
+
function isTrackedDeletedParagraph(node) {
|
|
17766
|
+
if (!node || node.nodeType !== 1 || node.localName !== "p") return false;
|
|
17767
|
+
const pPr = Array.from(node.childNodes || []).find((child) => isDirectWordChild(child, "pPr"));
|
|
17768
|
+
const rPr = pPr && Array.from(pPr.childNodes || []).find((child) => isDirectWordChild(child, "rPr"));
|
|
17769
|
+
return !!rPr && Array.from(rPr.childNodes || []).some((child) => isDirectWordChild(child, "del"));
|
|
17714
17770
|
}
|
|
17715
|
-
function
|
|
17716
|
-
|
|
17717
|
-
const matrix = rowElements.map((row) => {
|
|
17718
|
-
const cellElements = getDirectWordChildren(row, "tc");
|
|
17719
|
-
return cellElements.map((cell) => {
|
|
17720
|
-
const paragraphs = getDirectWordChildren(cell, "p");
|
|
17721
|
-
if (paragraphs.length === 0) return normalizeWhitespaceForTargeting(getParagraphText(cell));
|
|
17722
|
-
const lines = paragraphs.map((p) => normalizeWhitespaceForTargeting(getParagraphText(p))).filter(Boolean);
|
|
17723
|
-
return lines.join("\n");
|
|
17724
|
-
});
|
|
17725
|
-
});
|
|
17726
|
-
const columnCount = matrix.reduce((max2, row) => Math.max(max2, row.length), 0);
|
|
17727
|
-
matrix.forEach((row) => {
|
|
17728
|
-
while (row.length < columnCount) row.push("");
|
|
17729
|
-
});
|
|
17730
|
-
return { matrix, rowElements, columnCount };
|
|
17771
|
+
function paragraphNumIdNodes(node) {
|
|
17772
|
+
return Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []).filter((numIdNode) => numIdNode.parentNode?.localName === "numPr");
|
|
17731
17773
|
}
|
|
17732
|
-
function
|
|
17733
|
-
|
|
17734
|
-
const
|
|
17735
|
-
const
|
|
17736
|
-
|
|
17737
|
-
|
|
17738
|
-
}
|
|
17739
|
-
|
|
17740
|
-
const separator = new Array(columnCount).fill("---");
|
|
17741
|
-
const bodyRows = normalized.slice(1);
|
|
17742
|
-
const toLine = (row) => `| ${row.map((cell) => escapeMarkdownCell(cell)).join(" | ")} |`;
|
|
17743
|
-
return [toLine(header), toLine(separator), ...bodyRows.map(toLine)].join("\n");
|
|
17774
|
+
function levelFormats(abstractNum) {
|
|
17775
|
+
const formats = /* @__PURE__ */ new Map();
|
|
17776
|
+
for (const lvl of Array.from(abstractNum?.getElementsByTagNameNS?.("*", "lvl") || [])) {
|
|
17777
|
+
const ilvl = getElementId2(lvl, ["w:ilvl", "ilvl"]);
|
|
17778
|
+
const fmt = Array.from(lvl.getElementsByTagNameNS("*", "numFmt"))[0]?.getAttribute("w:val") || null;
|
|
17779
|
+
if (ilvl != null && fmt) formats.set(ilvl, fmt);
|
|
17780
|
+
}
|
|
17781
|
+
return formats;
|
|
17744
17782
|
}
|
|
17745
|
-
function
|
|
17746
|
-
|
|
17747
|
-
const normalized = rowValues.map((value) => normalizeWhitespaceForTargeting(value)).filter(Boolean);
|
|
17748
|
-
if (normalized.length < 2) return false;
|
|
17749
|
-
return normalized.every((value) => value === normalized[0]);
|
|
17783
|
+
function normalizeListFormat(format) {
|
|
17784
|
+
return format === "bullet" || format === "circle" || format === "square" ? "bullet" : format;
|
|
17750
17785
|
}
|
|
17751
|
-
function
|
|
17752
|
-
const
|
|
17753
|
-
|
|
17754
|
-
if (
|
|
17755
|
-
|
|
17756
|
-
if (/^\(.*\)$/.test(normalized)) return true;
|
|
17757
|
-
if (/:\s*$/.test(normalized)) return true;
|
|
17758
|
-
if (normalized.length <= 90 && !/[.!?]$/.test(normalized) && /[:\[\]()]/.test(normalized)) return true;
|
|
17759
|
-
if (/^[\[(]/.test(normalized)) return true;
|
|
17760
|
-
return false;
|
|
17786
|
+
function abstractNumForNumId(numberingDoc, numId) {
|
|
17787
|
+
const num = Array.from(numberingDoc.getElementsByTagNameNS("*", "num")).find((candidate) => getElementId2(candidate, ["w:numId", "numId"]) === numId);
|
|
17788
|
+
const abstractId = num && getElementId2(Array.from(num.getElementsByTagNameNS("*", "abstractNumId"))[0], ["w:val", "val"]);
|
|
17789
|
+
if (abstractId == null) return null;
|
|
17790
|
+
return Array.from(numberingDoc.getElementsByTagNameNS("*", "abstractNum")).find((candidate) => getElementId2(candidate, ["w:abstractNumId", "abstractNumId"]) === abstractId) || null;
|
|
17761
17791
|
}
|
|
17762
|
-
function
|
|
17763
|
-
const
|
|
17764
|
-
|
|
17765
|
-
|
|
17766
|
-
const
|
|
17767
|
-
|
|
17768
|
-
|
|
17769
|
-
|
|
17770
|
-
scanned += 1;
|
|
17771
|
-
const nextCursor = cursor.nextSibling;
|
|
17772
|
-
if (cursor.nodeType !== 1 || cursor.namespaceURI !== WORD_MAIN_NS || cursor.localName !== "p") {
|
|
17773
|
-
cursor = nextCursor;
|
|
17774
|
-
continue;
|
|
17792
|
+
function reuseSourceListNumbering(numberingXml, replacementNodes, sourceParagraphs, numberingIdState) {
|
|
17793
|
+
const sourceXml = numberingIdState?.sourceNumberingXml;
|
|
17794
|
+
if (!numberingXml || !String(sourceXml || "").trim() || !Array.isArray(replacementNodes)) return null;
|
|
17795
|
+
const sourceNumIds = /* @__PURE__ */ new Set();
|
|
17796
|
+
for (const paragraph of sourceParagraphs || []) {
|
|
17797
|
+
for (const numIdNode of paragraphNumIdNodes(paragraph)) {
|
|
17798
|
+
const id = getElementId2(numIdNode, ["w:val", "val"]);
|
|
17799
|
+
if (id != null && id !== 0) sourceNumIds.add(id);
|
|
17775
17800
|
}
|
|
17776
|
-
|
|
17777
|
-
|
|
17778
|
-
|
|
17779
|
-
|
|
17780
|
-
|
|
17801
|
+
}
|
|
17802
|
+
if (sourceNumIds.size !== 1) return null;
|
|
17803
|
+
const [sourceNumId] = sourceNumIds;
|
|
17804
|
+
const sourceDoc = parseOoxml(sourceXml);
|
|
17805
|
+
const payloadDoc = parseOoxml(numberingXml);
|
|
17806
|
+
if (hasXmlParseError(sourceDoc) || hasXmlParseError(payloadDoc)) return null;
|
|
17807
|
+
const sourceAbstract = abstractNumForNumId(sourceDoc, sourceNumId);
|
|
17808
|
+
if (!sourceAbstract) return null;
|
|
17809
|
+
const sourceFormats = levelFormats(sourceAbstract);
|
|
17810
|
+
const newParagraphNumIds = [];
|
|
17811
|
+
for (const node of replacementNodes) {
|
|
17812
|
+
if (isTrackedDeletedParagraph(node)) continue;
|
|
17813
|
+
for (const numIdNode of paragraphNumIdNodes(node)) {
|
|
17814
|
+
const ilvlNode = Array.from(numIdNode.parentNode.childNodes || []).find((child) => child.localName === "ilvl");
|
|
17815
|
+
newParagraphNumIds.push({
|
|
17816
|
+
numId: getElementId2(numIdNode, ["w:val", "val"]),
|
|
17817
|
+
ilvl: ilvlNode ? getElementId2(ilvlNode, ["w:val", "val"]) ?? 0 : 0
|
|
17818
|
+
});
|
|
17781
17819
|
}
|
|
17782
|
-
if (!isLikelyStructuredTableSourceParagraph(text2)) break;
|
|
17783
|
-
block.push(cursor);
|
|
17784
|
-
cursor = nextCursor;
|
|
17785
17820
|
}
|
|
17786
|
-
|
|
17821
|
+
if (newParagraphNumIds.length === 0) return null;
|
|
17822
|
+
const payloadFormats = /* @__PURE__ */ new Map();
|
|
17823
|
+
for (const { numId, ilvl } of newParagraphNumIds) {
|
|
17824
|
+
if (numId == null) return null;
|
|
17825
|
+
if (!payloadFormats.has(numId)) {
|
|
17826
|
+
const abstract = abstractNumForNumId(payloadDoc, numId);
|
|
17827
|
+
if (!abstract) return null;
|
|
17828
|
+
payloadFormats.set(numId, levelFormats(abstract));
|
|
17829
|
+
}
|
|
17830
|
+
const generated = payloadFormats.get(numId).get(ilvl);
|
|
17831
|
+
const existing = sourceFormats.get(ilvl);
|
|
17832
|
+
if (!generated || !existing || normalizeListFormat(generated) !== normalizeListFormat(existing)) return null;
|
|
17833
|
+
}
|
|
17834
|
+
const clonedNodes = replacementNodes.map((node) => node?.cloneNode ? node.cloneNode(true) : node);
|
|
17835
|
+
for (const node of clonedNodes) {
|
|
17836
|
+
if (isTrackedDeletedParagraph(node)) continue;
|
|
17837
|
+
for (const numIdNode of paragraphNumIdNodes(node)) setElementVal2(numIdNode, sourceNumId);
|
|
17838
|
+
}
|
|
17839
|
+
return { numId: sourceNumId, replacementNodes: clonedNodes };
|
|
17787
17840
|
}
|
|
17788
|
-
function
|
|
17789
|
-
const
|
|
17790
|
-
|
|
17791
|
-
|
|
17792
|
-
|
|
17793
|
-
|
|
17794
|
-
|
|
17795
|
-
if (isMarkdownTableText(rawModified)) return null;
|
|
17796
|
-
const lines = rawModified.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean);
|
|
17797
|
-
if (lines.length < 2) return null;
|
|
17798
|
-
const tableElement = options.tableElement || findContainingWordElement(targetParagraph, "tbl");
|
|
17799
|
-
const rowElement = findContainingWordElement(targetParagraph, "tr");
|
|
17800
|
-
const cellElement = findContainingWordElement(targetParagraph, "tc");
|
|
17801
|
-
if (!tableElement || !rowElement || !cellElement) return null;
|
|
17802
|
-
const currentParagraphText = normalizeWhitespaceForTargeting(
|
|
17803
|
-
options.currentParagraphText || getParagraphText(targetParagraph)
|
|
17804
|
-
);
|
|
17805
|
-
const firstLine = normalizeWhitespaceForTargeting(lines[0]);
|
|
17806
|
-
if (currentParagraphText && firstLine && firstLine !== currentParagraphText) {
|
|
17807
|
-
onWarn("[Table] Multiline cell text did not anchor to original cell text; skipping table-row synthesis heuristic.");
|
|
17808
|
-
return null;
|
|
17841
|
+
function remapNumberingPayloadForDocument(numberingXml, replacementNodes, numberingIdState) {
|
|
17842
|
+
const numberingDoc = parseOoxml(numberingXml);
|
|
17843
|
+
if (hasXmlParseError(numberingDoc)) {
|
|
17844
|
+
return {
|
|
17845
|
+
numberingXml: String(numberingXml || ""),
|
|
17846
|
+
replacementNodes: Array.isArray(replacementNodes) ? replacementNodes.map((node) => node?.cloneNode ? node.cloneNode(true) : node) : []
|
|
17847
|
+
};
|
|
17809
17848
|
}
|
|
17810
|
-
const
|
|
17811
|
-
|
|
17812
|
-
const
|
|
17813
|
-
const
|
|
17814
|
-
|
|
17815
|
-
|
|
17816
|
-
|
|
17817
|
-
|
|
17818
|
-
|
|
17819
|
-
|
|
17849
|
+
const abstractNumMap = /* @__PURE__ */ new Map();
|
|
17850
|
+
const numIdMap = /* @__PURE__ */ new Map();
|
|
17851
|
+
const abstractNums = Array.from(numberingDoc.getElementsByTagNameNS("*", "abstractNum"));
|
|
17852
|
+
for (const abstractNum of abstractNums) {
|
|
17853
|
+
const oldId = getElementId2(abstractNum, ["w:abstractNumId", "abstractNumId"]);
|
|
17854
|
+
if (oldId == null) continue;
|
|
17855
|
+
const newId = reserveNextNumberingId(numberingIdState, "abstract");
|
|
17856
|
+
if (newId == null) continue;
|
|
17857
|
+
abstractNumMap.set(oldId, newId);
|
|
17858
|
+
setElementId(abstractNum, "w:abstractNumId", newId);
|
|
17820
17859
|
}
|
|
17821
|
-
|
|
17822
|
-
|
|
17823
|
-
const
|
|
17824
|
-
if (
|
|
17825
|
-
|
|
17826
|
-
|
|
17827
|
-
|
|
17828
|
-
|
|
17829
|
-
|
|
17830
|
-
|
|
17831
|
-
|
|
17832
|
-
|
|
17833
|
-
|
|
17834
|
-
} else {
|
|
17835
|
-
const newRow = new Array(columnCount).fill("");
|
|
17836
|
-
if (mirrorAcrossColumns) {
|
|
17837
|
-
for (let col = 0; col < columnCount; col++) newRow[col] = extraValue;
|
|
17838
|
-
} else {
|
|
17839
|
-
newRow[colIndex] = extraValue;
|
|
17860
|
+
const nums = Array.from(numberingDoc.getElementsByTagNameNS("*", "num"));
|
|
17861
|
+
for (const num of nums) {
|
|
17862
|
+
const oldNumId = getElementId2(num, ["w:numId", "numId"]);
|
|
17863
|
+
if (oldNumId == null) continue;
|
|
17864
|
+
const newNumId = reserveNextNumberingId(numberingIdState, "num");
|
|
17865
|
+
if (newNumId == null) continue;
|
|
17866
|
+
numIdMap.set(oldNumId, newNumId);
|
|
17867
|
+
setElementId(num, "w:numId", newNumId);
|
|
17868
|
+
const abstractNumIdNode = Array.from(num.getElementsByTagNameNS("*", "abstractNumId"))[0] || null;
|
|
17869
|
+
if (abstractNumIdNode) {
|
|
17870
|
+
const oldAbsRef = getElementId2(abstractNumIdNode, ["w:val", "val"]);
|
|
17871
|
+
if (oldAbsRef != null && abstractNumMap.has(oldAbsRef)) {
|
|
17872
|
+
setElementVal2(abstractNumIdNode, abstractNumMap.get(oldAbsRef));
|
|
17840
17873
|
}
|
|
17841
|
-
matrix.splice(Math.min(insertIndex, matrix.length), 0, newRow);
|
|
17842
17874
|
}
|
|
17843
17875
|
}
|
|
17844
|
-
const
|
|
17845
|
-
|
|
17846
|
-
|
|
17847
|
-
|
|
17848
|
-
|
|
17849
|
-
|
|
17850
|
-
|
|
17851
|
-
|
|
17852
|
-
|
|
17853
|
-
|
|
17854
|
-
const raw = element.getAttribute(name);
|
|
17855
|
-
if (raw == null || raw === "") continue;
|
|
17856
|
-
const parsed = Number.parseInt(String(raw), 10);
|
|
17857
|
-
if (Number.isFinite(parsed)) return parsed;
|
|
17876
|
+
const clonedNodes = Array.isArray(replacementNodes) ? replacementNodes.map((node) => node?.cloneNode ? node.cloneNode(true) : node) : [];
|
|
17877
|
+
for (const node of clonedNodes) {
|
|
17878
|
+
if (isTrackedDeletedParagraph(node)) continue;
|
|
17879
|
+
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []);
|
|
17880
|
+
for (const numIdNode of numIdNodes) {
|
|
17881
|
+
const oldNumRef = getElementId2(numIdNode, ["w:val", "val"]);
|
|
17882
|
+
if (oldNumRef != null && numIdMap.has(oldNumRef)) {
|
|
17883
|
+
setElementVal2(numIdNode, numIdMap.get(oldNumRef));
|
|
17884
|
+
}
|
|
17885
|
+
}
|
|
17858
17886
|
}
|
|
17859
|
-
return
|
|
17887
|
+
return {
|
|
17888
|
+
numberingXml: serializeOoxml(numberingDoc),
|
|
17889
|
+
replacementNodes: clonedNodes
|
|
17890
|
+
};
|
|
17860
17891
|
}
|
|
17861
|
-
function
|
|
17862
|
-
|
|
17863
|
-
const
|
|
17864
|
-
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
|
|
17892
|
+
function mergeNumberingXmlBySchemaOrder(existingNumberingXml, incomingNumberingXml) {
|
|
17893
|
+
const existingText = String(existingNumberingXml || "");
|
|
17894
|
+
const incomingText = String(incomingNumberingXml || "");
|
|
17895
|
+
if (!incomingText.trim()) return existingText;
|
|
17896
|
+
if (!existingText.trim()) return incomingText;
|
|
17897
|
+
try {
|
|
17898
|
+
const existingDoc = parseOoxml(existingText);
|
|
17899
|
+
const incomingDoc = parseOoxml(incomingText);
|
|
17900
|
+
if (hasXmlParseError(existingDoc) || hasXmlParseError(incomingDoc)) {
|
|
17901
|
+
return existingText;
|
|
17870
17902
|
}
|
|
17903
|
+
const existingRoot = existingDoc.documentElement;
|
|
17904
|
+
const incomingRoot = incomingDoc.documentElement;
|
|
17905
|
+
if (!existingRoot || !incomingRoot) return existingText;
|
|
17906
|
+
const existingAbstractIds = new Set(
|
|
17907
|
+
Array.from(existingRoot.childNodes || []).filter((node) => isDirectWordChild(node, "abstractNum")).map((node) => parseIntegerAttribute(node, ["w:abstractNumId", "abstractNumId"])).filter((id) => id != null)
|
|
17908
|
+
);
|
|
17909
|
+
const existingNumIds = new Set(
|
|
17910
|
+
Array.from(existingRoot.childNodes || []).filter((node) => isDirectWordChild(node, "num")).map((node) => parseIntegerAttribute(node, ["w:numId", "numId"])).filter((id) => id != null)
|
|
17911
|
+
);
|
|
17912
|
+
for (const incomingNode of Array.from(incomingRoot.childNodes || [])) {
|
|
17913
|
+
if (!isDirectWordChild(incomingNode, "abstractNum")) continue;
|
|
17914
|
+
const incomingId = parseIntegerAttribute(incomingNode, ["w:abstractNumId", "abstractNumId"]);
|
|
17915
|
+
if (incomingId == null || existingAbstractIds.has(incomingId)) continue;
|
|
17916
|
+
insertNumberingNodeInSchemaOrder(existingRoot, existingDoc.importNode(incomingNode, true), "abstract");
|
|
17917
|
+
existingAbstractIds.add(incomingId);
|
|
17918
|
+
}
|
|
17919
|
+
for (const incomingNode of Array.from(incomingRoot.childNodes || [])) {
|
|
17920
|
+
if (!isDirectWordChild(incomingNode, "num")) continue;
|
|
17921
|
+
const incomingId = parseIntegerAttribute(incomingNode, ["w:numId", "numId"]);
|
|
17922
|
+
if (incomingId == null || existingNumIds.has(incomingId)) continue;
|
|
17923
|
+
insertNumberingNodeInSchemaOrder(existingRoot, existingDoc.importNode(incomingNode, true), "num");
|
|
17924
|
+
existingNumIds.add(incomingId);
|
|
17925
|
+
}
|
|
17926
|
+
return serializeOoxml(existingDoc);
|
|
17927
|
+
} catch {
|
|
17928
|
+
return existingText;
|
|
17871
17929
|
}
|
|
17872
|
-
return candidate;
|
|
17873
17930
|
}
|
|
17874
|
-
|
|
17875
|
-
|
|
17876
|
-
|
|
17877
|
-
|
|
17878
|
-
|
|
17879
|
-
|
|
17931
|
+
|
|
17932
|
+
// core/redline-validation.js
|
|
17933
|
+
var REVISION_ID_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "rPrChange", "pPrChange"]);
|
|
17934
|
+
var REVISION_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}T/;
|
|
17935
|
+
function localNameOf3(node) {
|
|
17936
|
+
return String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
|
|
17937
|
+
}
|
|
17938
|
+
function elementsByLocalName(root, name) {
|
|
17939
|
+
return Array.from(root.getElementsByTagName("*")).filter((el) => localNameOf3(el) === name);
|
|
17940
|
+
}
|
|
17941
|
+
function wordAttribute3(node, name) {
|
|
17942
|
+
return node.getAttribute(`w:${name}`) || node.getAttribute(name) || "";
|
|
17943
|
+
}
|
|
17944
|
+
function xmlSpaceAttribute(node) {
|
|
17945
|
+
return node.getAttribute("xml:space") || node.getAttribute("space") || node.getAttributeNS?.("http://www.w3.org/XML/1998/namespace", "space") || "";
|
|
17946
|
+
}
|
|
17947
|
+
function isParagraphMarkRevision(node) {
|
|
17948
|
+
return localNameOf3(node.parentNode) === "rPr";
|
|
17949
|
+
}
|
|
17950
|
+
function parseOoxmlForValidation(oxml) {
|
|
17951
|
+
const attempt = (xml) => {
|
|
17952
|
+
const doc = parseXml(xml);
|
|
17953
|
+
const parseError = doc.getElementsByTagName("parsererror")[0];
|
|
17954
|
+
if (parseError) {
|
|
17955
|
+
throw new Error(parseError.textContent || "XML parse error");
|
|
17956
|
+
}
|
|
17957
|
+
return doc;
|
|
17958
|
+
};
|
|
17959
|
+
try {
|
|
17960
|
+
return { doc: attempt(oxml) };
|
|
17961
|
+
} catch {
|
|
17880
17962
|
try {
|
|
17881
|
-
|
|
17882
|
-
|
|
17883
|
-
|
|
17884
|
-
for (const abstractNum of abstractNums) {
|
|
17885
|
-
const id = parseIntegerAttribute(abstractNum, ["w:abstractNumId", "abstractNumId"]);
|
|
17886
|
-
if (id != null) usedAbstractNumIds.add(id);
|
|
17887
|
-
}
|
|
17888
|
-
for (const num of nums) {
|
|
17889
|
-
const id = parseIntegerAttribute(num, ["w:numId", "numId"]);
|
|
17890
|
-
if (id != null) usedNumIds.add(id);
|
|
17891
|
-
}
|
|
17892
|
-
} catch {
|
|
17963
|
+
return { doc: attempt(`<w:root xmlns:w="${NS_W}">${oxml}</w:root>`) };
|
|
17964
|
+
} catch (error2) {
|
|
17965
|
+
return { error: error2?.message || "XML parse error" };
|
|
17893
17966
|
}
|
|
17894
17967
|
}
|
|
17895
|
-
const maxUsedNumId = usedNumIds.size > 0 ? Math.max(...usedNumIds) : 0;
|
|
17896
|
-
const maxUsedAbstractNumId = usedAbstractNumIds.size > 0 ? Math.max(...usedAbstractNumIds) : 0;
|
|
17897
|
-
const baseNumId = Math.max(minId, maxUsedNumId + 1);
|
|
17898
|
-
const baseAbstractNumId = Math.max(minId, maxUsedAbstractNumId + 1);
|
|
17899
|
-
return {
|
|
17900
|
-
// Kept so list edits can recognise which existing definition a source list uses and continue it.
|
|
17901
|
-
sourceNumberingXml: String(numberingXml || ""),
|
|
17902
|
-
nextNumId: nextAvailableId(baseNumId, usedNumIds, maxPreferred),
|
|
17903
|
-
nextAbstractNumId: nextAvailableId(baseAbstractNumId, usedAbstractNumIds, maxPreferred),
|
|
17904
|
-
usedNumIds,
|
|
17905
|
-
usedAbstractNumIds,
|
|
17906
|
-
minId,
|
|
17907
|
-
maxPreferred
|
|
17908
|
-
};
|
|
17909
17968
|
}
|
|
17910
|
-
function
|
|
17911
|
-
|
|
17912
|
-
|
|
17913
|
-
|
|
17914
|
-
if (!
|
|
17915
|
-
|
|
17969
|
+
function validateRedlineOoxml(oxml) {
|
|
17970
|
+
const issues = [];
|
|
17971
|
+
const addIssue = (code, severity, message) => issues.push({ code, severity, message });
|
|
17972
|
+
const isDomNode = oxml && typeof oxml === "object" && (oxml.nodeType === 9 || oxml.nodeType === 1);
|
|
17973
|
+
if (!isDomNode && (typeof oxml !== "string" || oxml.trim() === "")) {
|
|
17974
|
+
addIssue("PARSE_ERROR", "error", "Input is not non-empty OOXML or a parsed XML DOM.");
|
|
17975
|
+
return { valid: false, issues };
|
|
17916
17976
|
}
|
|
17917
|
-
|
|
17918
|
-
|
|
17977
|
+
const { doc, error: error2 } = isDomNode ? { doc: oxml.nodeType === 9 ? oxml : oxml.ownerDocument } : parseOoxmlForValidation(oxml);
|
|
17978
|
+
if (!doc) {
|
|
17979
|
+
addIssue("PARSE_ERROR", "error", `OOXML does not parse as XML: ${error2}`);
|
|
17980
|
+
return { valid: false, issues };
|
|
17919
17981
|
}
|
|
17920
|
-
|
|
17921
|
-
|
|
17982
|
+
const insElements = elementsByLocalName(doc, "ins");
|
|
17983
|
+
const delElements = elementsByLocalName(doc, "del");
|
|
17984
|
+
const revisions = insElements.concat(delElements);
|
|
17985
|
+
for (const paragraph of elementsByLocalName(doc, "p")) {
|
|
17986
|
+
const nested = Array.from(paragraph.getElementsByTagName("*")).find((el) => el !== paragraph && localNameOf3(el) === "p");
|
|
17987
|
+
if (nested) {
|
|
17988
|
+
addIssue("NESTED_PARAGRAPH", "error", `<${paragraph.nodeName}> contains nested <${nested.nodeName}>.`);
|
|
17989
|
+
}
|
|
17922
17990
|
}
|
|
17923
|
-
|
|
17924
|
-
|
|
17991
|
+
for (const body of elementsByLocalName(doc, "body")) {
|
|
17992
|
+
const children = Array.from(body.childNodes || []).filter((child) => child.nodeType === 1);
|
|
17993
|
+
const sectPrIndexes = children.map((child, index) => localNameOf3(child) === "sectPr" ? index : -1).filter((index) => index >= 0);
|
|
17994
|
+
if (sectPrIndexes.length > 1) {
|
|
17995
|
+
addIssue("MULTIPLE_BODY_SECTPR", "error", "<w:body> contains multiple direct <w:sectPr> elements.");
|
|
17996
|
+
} else if (sectPrIndexes.length === 1 && sectPrIndexes[0] !== children.length - 1) {
|
|
17997
|
+
addIssue("SECTPR_NOT_LAST", "error", "<w:sectPr> is not the last element child of <w:body>.");
|
|
17998
|
+
}
|
|
17925
17999
|
}
|
|
17926
|
-
|
|
17927
|
-
|
|
17928
|
-
|
|
17929
|
-
|
|
17930
|
-
|
|
17931
|
-
|
|
17932
|
-
|
|
17933
|
-
|
|
17934
|
-
|
|
17935
|
-
|
|
17936
|
-
|
|
17937
|
-
|
|
17938
|
-
|
|
17939
|
-
} else {
|
|
17940
|
-
normalized.usedNumIds.add(id);
|
|
17941
|
-
normalized.nextNumId = nextAvailableId(id + 1, normalized.usedNumIds, normalized.maxPreferred);
|
|
18000
|
+
for (const revision of revisions) {
|
|
18001
|
+
const nested = Array.from(revision.getElementsByTagName("*")).filter((el) => el !== revision && ["ins", "del"].includes(localNameOf3(el)));
|
|
18002
|
+
const invalidNested = nested.find((candidate) => {
|
|
18003
|
+
if (localNameOf3(revision) !== "ins" || localNameOf3(candidate) !== "del") return true;
|
|
18004
|
+
return candidate.parentNode !== revision;
|
|
18005
|
+
});
|
|
18006
|
+
if (invalidNested) {
|
|
18007
|
+
addIssue(
|
|
18008
|
+
"NESTED_REVISION",
|
|
18009
|
+
"error",
|
|
18010
|
+
`<${revision.nodeName}> (w:id="${wordAttribute3(revision, "id")}") contains invalid nested <${invalidNested.nodeName}>.`
|
|
18011
|
+
);
|
|
18012
|
+
}
|
|
17942
18013
|
}
|
|
17943
|
-
|
|
17944
|
-
|
|
17945
|
-
|
|
17946
|
-
|
|
17947
|
-
|
|
17948
|
-
|
|
17949
|
-
|
|
17950
|
-
|
|
17951
|
-
|
|
17952
|
-
if (!doc || !doc.documentElement) return true;
|
|
17953
|
-
if (doc.documentElement.localName === "parsererror") return true;
|
|
17954
|
-
return doc.getElementsByTagName("parsererror").length > 0;
|
|
17955
|
-
}
|
|
17956
|
-
function isDirectWordChild(node, localName2) {
|
|
17957
|
-
return !!(node && node.nodeType === 1 && node.namespaceURI === WORD_MAIN_NS && node.localName === localName2);
|
|
17958
|
-
}
|
|
17959
|
-
function insertNumberingNodeInSchemaOrder(root, node, kind) {
|
|
17960
|
-
if (!root || !node) return;
|
|
17961
|
-
const directChildren = Array.from(root.childNodes || []).filter(
|
|
17962
|
-
(child) => child && child.nodeType === 1 && child.namespaceURI === WORD_MAIN_NS
|
|
17963
|
-
);
|
|
17964
|
-
let anchor = null;
|
|
17965
|
-
if (kind === "abstract") {
|
|
17966
|
-
anchor = directChildren.find(
|
|
17967
|
-
(child) => child.localName === "num" || child.localName === "numIdMacAtCleanup"
|
|
17968
|
-
) || null;
|
|
17969
|
-
} else {
|
|
17970
|
-
anchor = directChildren.find((child) => child.localName === "numIdMacAtCleanup") || null;
|
|
18014
|
+
for (const del of delElements) {
|
|
18015
|
+
const plainTextNodes = elementsByLocalName(del, "t");
|
|
18016
|
+
if (plainTextNodes.length > 0) {
|
|
18017
|
+
addIssue(
|
|
18018
|
+
"DEL_CONTAINS_T",
|
|
18019
|
+
"error",
|
|
18020
|
+
`<w:del> (w:id="${wordAttribute3(del, "id")}") contains <w:t>; deleted text must use <w:delText>.`
|
|
18021
|
+
);
|
|
18022
|
+
}
|
|
17971
18023
|
}
|
|
17972
|
-
|
|
17973
|
-
|
|
17974
|
-
|
|
17975
|
-
|
|
17976
|
-
|
|
17977
|
-
|
|
17978
|
-
|
|
18024
|
+
for (const revision of revisions) {
|
|
18025
|
+
const missing = [];
|
|
18026
|
+
if (!wordAttribute3(revision, "id")) missing.push("w:id");
|
|
18027
|
+
if (!wordAttribute3(revision, "author")) missing.push("w:author");
|
|
18028
|
+
if (!REVISION_DATE_PATTERN.test(wordAttribute3(revision, "date"))) missing.push("w:date");
|
|
18029
|
+
if (missing.length > 0) {
|
|
18030
|
+
addIssue(
|
|
18031
|
+
"MISSING_REVISION_METADATA",
|
|
18032
|
+
"error",
|
|
18033
|
+
`<${revision.nodeName}> is missing or has malformed ${missing.join(", ")}.`
|
|
18034
|
+
);
|
|
18035
|
+
}
|
|
17979
18036
|
}
|
|
17980
|
-
|
|
17981
|
-
|
|
17982
|
-
|
|
17983
|
-
|
|
17984
|
-
|
|
17985
|
-
|
|
17986
|
-
}
|
|
17987
|
-
function setElementId(element, preferredName, idValue) {
|
|
17988
|
-
element?.setAttribute?.(preferredName, String(idValue));
|
|
17989
|
-
}
|
|
17990
|
-
function setElementVal2(element, value) {
|
|
17991
|
-
element?.setAttribute?.("w:val", String(value));
|
|
17992
|
-
}
|
|
17993
|
-
function overwriteParagraphNumIds(paragraphNodes, targetNumId) {
|
|
17994
|
-
if (!Array.isArray(paragraphNodes) || targetNumId == null) return;
|
|
17995
|
-
for (const node of paragraphNodes) {
|
|
17996
|
-
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []);
|
|
17997
|
-
for (const numIdNode of numIdNodes) {
|
|
17998
|
-
setElementVal2(numIdNode, targetNumId);
|
|
18037
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
18038
|
+
for (const node of Array.from(doc.getElementsByTagName("*"))) {
|
|
18039
|
+
if (!REVISION_ID_ELEMENTS.has(localNameOf3(node))) continue;
|
|
18040
|
+
const id = wordAttribute3(node, "id");
|
|
18041
|
+
if (!id) continue;
|
|
18042
|
+
if (seenIds.has(id)) {
|
|
18043
|
+
addIssue("DUPLICATE_REVISION_ID", "error", `Revision id ${id} appears more than once.`);
|
|
17999
18044
|
}
|
|
18045
|
+
seenIds.add(id);
|
|
18000
18046
|
}
|
|
18001
|
-
|
|
18002
|
-
|
|
18003
|
-
|
|
18004
|
-
|
|
18005
|
-
|
|
18006
|
-
|
|
18007
|
-
|
|
18047
|
+
const textNodes = elementsByLocalName(doc, "t").concat(elementsByLocalName(doc, "delText"));
|
|
18048
|
+
for (const node of textNodes) {
|
|
18049
|
+
const text2 = node.textContent || "";
|
|
18050
|
+
if (/^\s|\s$/.test(text2) && xmlSpaceAttribute(node) !== "preserve") {
|
|
18051
|
+
addIssue(
|
|
18052
|
+
"MISSING_SPACE_PRESERVE",
|
|
18053
|
+
"error",
|
|
18054
|
+
`<${node.nodeName}> has boundary whitespace without xml:space="preserve".`
|
|
18055
|
+
);
|
|
18056
|
+
}
|
|
18057
|
+
if (text2 === "") {
|
|
18058
|
+
addIssue("EMPTY_TEXT_ELEMENT", "warning", `<${node.nodeName}> is empty.`);
|
|
18008
18059
|
}
|
|
18009
18060
|
}
|
|
18010
|
-
|
|
18011
|
-
|
|
18012
|
-
|
|
18013
|
-
|
|
18014
|
-
|
|
18015
|
-
|
|
18016
|
-
|
|
18017
|
-
|
|
18018
|
-
|
|
18019
|
-
|
|
18020
|
-
<w:lvl w:ilvl="${level}">
|
|
18021
|
-
<w:start w:val="1"/>
|
|
18022
|
-
<w:numFmt w:val="decimal"/>
|
|
18023
|
-
<w:lvlText w:val="${lvlText}"/>
|
|
18024
|
-
<w:lvlJc w:val="left"/>
|
|
18025
|
-
<w:pPr><w:ind w:left="${left}" w:hanging="360"/></w:pPr>
|
|
18026
|
-
</w:lvl>`;
|
|
18027
|
-
}).join("");
|
|
18028
|
-
return `
|
|
18029
|
-
<w:numbering xmlns:w="${WORD_MAIN_NS}">
|
|
18030
|
-
<w:abstractNum w:abstractNumId="${safeAbstractNumId}">
|
|
18031
|
-
<w:multiLevelType w:val="multilevel"/>
|
|
18032
|
-
${levelsXml}
|
|
18033
|
-
</w:abstractNum>
|
|
18034
|
-
<w:num w:numId="${safeNumId}">
|
|
18035
|
-
<w:abstractNumId w:val="${safeAbstractNumId}"/>
|
|
18036
|
-
<w:lvlOverride w:ilvl="0">
|
|
18037
|
-
<w:startOverride w:val="${safeStartAt}"/>
|
|
18038
|
-
</w:lvlOverride>
|
|
18039
|
-
</w:num>
|
|
18040
|
-
</w:numbering>`.trim();
|
|
18041
|
-
}
|
|
18042
|
-
function isTrackedDeletedParagraph(node) {
|
|
18043
|
-
if (!node || node.nodeType !== 1 || node.localName !== "p") return false;
|
|
18044
|
-
const pPr = Array.from(node.childNodes || []).find((child) => isDirectWordChild(child, "pPr"));
|
|
18045
|
-
const rPr = pPr && Array.from(pPr.childNodes || []).find((child) => isDirectWordChild(child, "rPr"));
|
|
18046
|
-
return !!rPr && Array.from(rPr.childNodes || []).some((child) => isDirectWordChild(child, "del"));
|
|
18047
|
-
}
|
|
18048
|
-
function paragraphNumIdNodes(node) {
|
|
18049
|
-
return Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []).filter((numIdNode) => numIdNode.parentNode?.localName === "numPr");
|
|
18050
|
-
}
|
|
18051
|
-
function levelFormats(abstractNum) {
|
|
18052
|
-
const formats = /* @__PURE__ */ new Map();
|
|
18053
|
-
for (const lvl of Array.from(abstractNum?.getElementsByTagNameNS?.("*", "lvl") || [])) {
|
|
18054
|
-
const ilvl = getElementId2(lvl, ["w:ilvl", "ilvl"]);
|
|
18055
|
-
const fmt = Array.from(lvl.getElementsByTagNameNS("*", "numFmt"))[0]?.getAttribute("w:val") || null;
|
|
18056
|
-
if (ilvl != null && fmt) formats.set(ilvl, fmt);
|
|
18061
|
+
for (const revision of revisions) {
|
|
18062
|
+
if (isParagraphMarkRevision(revision)) continue;
|
|
18063
|
+
const hasElementChild = Array.from(revision.childNodes || []).some((child) => child.nodeType === 1);
|
|
18064
|
+
if (!hasElementChild) {
|
|
18065
|
+
addIssue(
|
|
18066
|
+
"EMPTY_REVISION_WRAPPER",
|
|
18067
|
+
"warning",
|
|
18068
|
+
`<${revision.nodeName}> (w:id="${wordAttribute3(revision, "id")}") wraps no content.`
|
|
18069
|
+
);
|
|
18070
|
+
}
|
|
18057
18071
|
}
|
|
18058
|
-
|
|
18072
|
+
for (const resurrection of findForeignDeletedParagraphResurrections(doc)) {
|
|
18073
|
+
const ownerAuthor = resurrection.ownerAuthor || "unattributed";
|
|
18074
|
+
addIssue(
|
|
18075
|
+
"FOREIGN_PARAGRAPH_MARK_DELETION",
|
|
18076
|
+
"warning",
|
|
18077
|
+
`Paragraph deleted by ${ownerAuthor} also contains non-empty insertion content from another author; Accept/Reject lifecycle may not preserve the apparent restoration.`
|
|
18078
|
+
);
|
|
18079
|
+
}
|
|
18080
|
+
return { valid: !issues.some((issue) => issue.severity === "error"), issues };
|
|
18059
18081
|
}
|
|
18060
|
-
|
|
18061
|
-
|
|
18082
|
+
|
|
18083
|
+
// core/table-targeting.js
|
|
18084
|
+
function getDirectWordChildren(element, localName2) {
|
|
18085
|
+
if (!element) return [];
|
|
18086
|
+
return Array.from(element.childNodes || []).filter(
|
|
18087
|
+
(node) => node && node.nodeType === 1 && node.namespaceURI === WORD_MAIN_NS && node.localName === localName2
|
|
18088
|
+
);
|
|
18062
18089
|
}
|
|
18063
|
-
function
|
|
18064
|
-
|
|
18065
|
-
const abstractId = num && getElementId2(Array.from(num.getElementsByTagNameNS("*", "abstractNumId"))[0], ["w:val", "val"]);
|
|
18066
|
-
if (abstractId == null) return null;
|
|
18067
|
-
return Array.from(numberingDoc.getElementsByTagNameNS("*", "abstractNum")).find((candidate) => getElementId2(candidate, ["w:abstractNumId", "abstractNumId"]) === abstractId) || null;
|
|
18090
|
+
function escapeMarkdownCell(text2) {
|
|
18091
|
+
return String(text2 || "").replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
|
|
18068
18092
|
}
|
|
18069
|
-
function
|
|
18070
|
-
const
|
|
18071
|
-
|
|
18072
|
-
|
|
18073
|
-
|
|
18074
|
-
|
|
18075
|
-
|
|
18076
|
-
|
|
18077
|
-
|
|
18078
|
-
|
|
18079
|
-
|
|
18080
|
-
const
|
|
18081
|
-
|
|
18082
|
-
|
|
18083
|
-
|
|
18084
|
-
|
|
18085
|
-
|
|
18086
|
-
|
|
18087
|
-
|
|
18088
|
-
|
|
18089
|
-
|
|
18090
|
-
|
|
18091
|
-
|
|
18092
|
-
|
|
18093
|
-
|
|
18094
|
-
|
|
18095
|
-
|
|
18093
|
+
function extractTableMatrix(tableElement) {
|
|
18094
|
+
const rowElements = getDirectWordChildren(tableElement, "tr");
|
|
18095
|
+
const matrix = rowElements.map((row) => {
|
|
18096
|
+
const cellElements = getDirectWordChildren(row, "tc");
|
|
18097
|
+
return cellElements.map((cell) => {
|
|
18098
|
+
const paragraphs = getDirectWordChildren(cell, "p");
|
|
18099
|
+
if (paragraphs.length === 0) return normalizeWhitespaceForTargeting(getParagraphText(cell));
|
|
18100
|
+
const lines = paragraphs.map((p) => normalizeWhitespaceForTargeting(getParagraphText(p))).filter(Boolean);
|
|
18101
|
+
return lines.join("\n");
|
|
18102
|
+
});
|
|
18103
|
+
});
|
|
18104
|
+
const columnCount = matrix.reduce((max2, row) => Math.max(max2, row.length), 0);
|
|
18105
|
+
matrix.forEach((row) => {
|
|
18106
|
+
while (row.length < columnCount) row.push("");
|
|
18107
|
+
});
|
|
18108
|
+
return { matrix, rowElements, columnCount };
|
|
18109
|
+
}
|
|
18110
|
+
function tableMatrixToMarkdown(matrix, columnCount) {
|
|
18111
|
+
if (!Array.isArray(matrix) || matrix.length === 0 || columnCount <= 0) return null;
|
|
18112
|
+
const normalized = matrix.map((row) => {
|
|
18113
|
+
const copy = Array.isArray(row) ? row.slice(0, columnCount) : [];
|
|
18114
|
+
while (copy.length < columnCount) copy.push("");
|
|
18115
|
+
return copy;
|
|
18116
|
+
});
|
|
18117
|
+
const header = normalized[0];
|
|
18118
|
+
const separator = new Array(columnCount).fill("---");
|
|
18119
|
+
const bodyRows = normalized.slice(1);
|
|
18120
|
+
const toLine = (row) => `| ${row.map((cell) => escapeMarkdownCell(cell)).join(" | ")} |`;
|
|
18121
|
+
return [toLine(header), toLine(separator), ...bodyRows.map(toLine)].join("\n");
|
|
18122
|
+
}
|
|
18123
|
+
function isSymmetricLabelRow(rowValues) {
|
|
18124
|
+
if (!Array.isArray(rowValues) || rowValues.length < 2) return false;
|
|
18125
|
+
const normalized = rowValues.map((value) => normalizeWhitespaceForTargeting(value)).filter(Boolean);
|
|
18126
|
+
if (normalized.length < 2) return false;
|
|
18127
|
+
return normalized.every((value) => value === normalized[0]);
|
|
18128
|
+
}
|
|
18129
|
+
function isLikelyStructuredTableSourceParagraph(text2) {
|
|
18130
|
+
const normalized = String(text2 || "").trim();
|
|
18131
|
+
if (!normalized) return false;
|
|
18132
|
+
if (/^and$/i.test(normalized)) return true;
|
|
18133
|
+
if (/^\[.*\]$/.test(normalized)) return true;
|
|
18134
|
+
if (/^\(.*\)$/.test(normalized)) return true;
|
|
18135
|
+
if (/:\s*$/.test(normalized)) return true;
|
|
18136
|
+
if (normalized.length <= 90 && !/[.!?]$/.test(normalized) && /[:\[\]()]/.test(normalized)) return true;
|
|
18137
|
+
if (/^[\[(]/.test(normalized)) return true;
|
|
18138
|
+
return false;
|
|
18139
|
+
}
|
|
18140
|
+
function inferTableReplacementParagraphBlock(startParagraph, options = {}) {
|
|
18141
|
+
const maxScan = Number.isInteger(options?.maxScan) && options.maxScan > 0 ? options.maxScan : 10;
|
|
18142
|
+
const paragraphTextGetter = typeof options?.getParagraphText === "function" ? options.getParagraphText : getParagraphText;
|
|
18143
|
+
if (!startParagraph || !startParagraph.parentNode) return null;
|
|
18144
|
+
const block = [startParagraph];
|
|
18145
|
+
let cursor = startParagraph.nextSibling;
|
|
18146
|
+
let scanned = 0;
|
|
18147
|
+
while (cursor && scanned < maxScan) {
|
|
18148
|
+
scanned += 1;
|
|
18149
|
+
const nextCursor = cursor.nextSibling;
|
|
18150
|
+
if (cursor.nodeType !== 1 || cursor.namespaceURI !== WORD_MAIN_NS || cursor.localName !== "p") {
|
|
18151
|
+
cursor = nextCursor;
|
|
18152
|
+
continue;
|
|
18096
18153
|
}
|
|
18097
|
-
|
|
18098
|
-
|
|
18099
|
-
|
|
18100
|
-
|
|
18101
|
-
|
|
18102
|
-
if (!payloadFormats.has(numId)) {
|
|
18103
|
-
const abstract = abstractNumForNumId(payloadDoc, numId);
|
|
18104
|
-
if (!abstract) return null;
|
|
18105
|
-
payloadFormats.set(numId, levelFormats(abstract));
|
|
18154
|
+
const text2 = String(paragraphTextGetter(cursor) || "").trim();
|
|
18155
|
+
if (!text2) {
|
|
18156
|
+
if (block.length > 1) break;
|
|
18157
|
+
cursor = nextCursor;
|
|
18158
|
+
continue;
|
|
18106
18159
|
}
|
|
18107
|
-
|
|
18108
|
-
|
|
18109
|
-
|
|
18110
|
-
}
|
|
18111
|
-
const clonedNodes = replacementNodes.map((node) => node?.cloneNode ? node.cloneNode(true) : node);
|
|
18112
|
-
for (const node of clonedNodes) {
|
|
18113
|
-
if (isTrackedDeletedParagraph(node)) continue;
|
|
18114
|
-
for (const numIdNode of paragraphNumIdNodes(node)) setElementVal2(numIdNode, sourceNumId);
|
|
18160
|
+
if (!isLikelyStructuredTableSourceParagraph(text2)) break;
|
|
18161
|
+
block.push(cursor);
|
|
18162
|
+
cursor = nextCursor;
|
|
18115
18163
|
}
|
|
18116
|
-
return
|
|
18164
|
+
return block.length > 1 ? block : null;
|
|
18117
18165
|
}
|
|
18118
|
-
function
|
|
18119
|
-
const
|
|
18120
|
-
|
|
18121
|
-
|
|
18122
|
-
|
|
18123
|
-
|
|
18124
|
-
|
|
18166
|
+
function synthesizeTableMarkdownFromMultilineCellEdit(targetParagraph, modifiedText, options = {}) {
|
|
18167
|
+
const onInfo = typeof options.onInfo === "function" ? options.onInfo : () => {
|
|
18168
|
+
};
|
|
18169
|
+
const onWarn = typeof options.onWarn === "function" ? options.onWarn : () => {
|
|
18170
|
+
};
|
|
18171
|
+
const rawModified = String(modifiedText || "");
|
|
18172
|
+
if (!rawModified.includes("\n")) return null;
|
|
18173
|
+
if (isMarkdownTableText(rawModified)) return null;
|
|
18174
|
+
const lines = rawModified.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean);
|
|
18175
|
+
if (lines.length < 2) return null;
|
|
18176
|
+
const tableElement = options.tableElement || findContainingWordElement(targetParagraph, "tbl");
|
|
18177
|
+
const rowElement = findContainingWordElement(targetParagraph, "tr");
|
|
18178
|
+
const cellElement = findContainingWordElement(targetParagraph, "tc");
|
|
18179
|
+
if (!tableElement || !rowElement || !cellElement) return null;
|
|
18180
|
+
const currentParagraphText = normalizeWhitespaceForTargeting(
|
|
18181
|
+
options.currentParagraphText || getParagraphText(targetParagraph)
|
|
18182
|
+
);
|
|
18183
|
+
const firstLine = normalizeWhitespaceForTargeting(lines[0]);
|
|
18184
|
+
if (currentParagraphText && firstLine && firstLine !== currentParagraphText) {
|
|
18185
|
+
onWarn("[Table] Multiline cell text did not anchor to original cell text; skipping table-row synthesis heuristic.");
|
|
18186
|
+
return null;
|
|
18125
18187
|
}
|
|
18126
|
-
const
|
|
18127
|
-
|
|
18128
|
-
const
|
|
18129
|
-
|
|
18130
|
-
|
|
18131
|
-
|
|
18132
|
-
|
|
18133
|
-
|
|
18134
|
-
|
|
18135
|
-
|
|
18188
|
+
const { matrix, rowElements, columnCount } = extractTableMatrix(tableElement);
|
|
18189
|
+
if (matrix.length === 0 || columnCount === 0) return null;
|
|
18190
|
+
const rowIndex = rowElements.indexOf(rowElement);
|
|
18191
|
+
const cellElements = getDirectWordChildren(rowElement, "tc");
|
|
18192
|
+
const colIndex = cellElements.indexOf(cellElement);
|
|
18193
|
+
if (rowIndex < 0 || colIndex < 0 || colIndex >= columnCount) return null;
|
|
18194
|
+
matrix[rowIndex][colIndex] = lines[0];
|
|
18195
|
+
const mirrorAcrossColumns = isSymmetricLabelRow(matrix[rowIndex]);
|
|
18196
|
+
if (mirrorAcrossColumns) {
|
|
18197
|
+
onInfo("[Table] Symmetric row detected; mirroring inserted row values across columns.");
|
|
18136
18198
|
}
|
|
18137
|
-
|
|
18138
|
-
|
|
18139
|
-
const
|
|
18140
|
-
if (
|
|
18141
|
-
|
|
18142
|
-
|
|
18143
|
-
|
|
18144
|
-
|
|
18145
|
-
|
|
18146
|
-
|
|
18147
|
-
|
|
18148
|
-
|
|
18149
|
-
setElementVal2(abstractNumIdNode, abstractNumMap.get(oldAbsRef));
|
|
18199
|
+
for (let i2 = 1; i2 < lines.length; i2++) {
|
|
18200
|
+
const insertIndex = rowIndex + i2;
|
|
18201
|
+
const extraValue = lines[i2];
|
|
18202
|
+
if (insertIndex < matrix.length && !normalizeWhitespaceForTargeting(matrix[insertIndex][colIndex])) {
|
|
18203
|
+
if (mirrorAcrossColumns) {
|
|
18204
|
+
for (let col = 0; col < columnCount; col++) {
|
|
18205
|
+
if (!normalizeWhitespaceForTargeting(matrix[insertIndex][col])) {
|
|
18206
|
+
matrix[insertIndex][col] = extraValue;
|
|
18207
|
+
}
|
|
18208
|
+
}
|
|
18209
|
+
} else {
|
|
18210
|
+
matrix[insertIndex][colIndex] = extraValue;
|
|
18150
18211
|
}
|
|
18151
|
-
}
|
|
18152
|
-
|
|
18153
|
-
|
|
18154
|
-
|
|
18155
|
-
|
|
18156
|
-
|
|
18157
|
-
for (const numIdNode of numIdNodes) {
|
|
18158
|
-
const oldNumRef = getElementId2(numIdNode, ["w:val", "val"]);
|
|
18159
|
-
if (oldNumRef != null && numIdMap.has(oldNumRef)) {
|
|
18160
|
-
setElementVal2(numIdNode, numIdMap.get(oldNumRef));
|
|
18212
|
+
} else {
|
|
18213
|
+
const newRow = new Array(columnCount).fill("");
|
|
18214
|
+
if (mirrorAcrossColumns) {
|
|
18215
|
+
for (let col = 0; col < columnCount; col++) newRow[col] = extraValue;
|
|
18216
|
+
} else {
|
|
18217
|
+
newRow[colIndex] = extraValue;
|
|
18161
18218
|
}
|
|
18219
|
+
matrix.splice(Math.min(insertIndex, matrix.length), 0, newRow);
|
|
18162
18220
|
}
|
|
18163
18221
|
}
|
|
18164
|
-
|
|
18165
|
-
|
|
18166
|
-
|
|
18167
|
-
|
|
18168
|
-
}
|
|
18169
|
-
function mergeNumberingXmlBySchemaOrder(existingNumberingXml, incomingNumberingXml) {
|
|
18170
|
-
const existingText = String(existingNumberingXml || "");
|
|
18171
|
-
const incomingText = String(incomingNumberingXml || "");
|
|
18172
|
-
if (!incomingText.trim()) return existingText;
|
|
18173
|
-
if (!existingText.trim()) return incomingText;
|
|
18174
|
-
try {
|
|
18175
|
-
const existingDoc = parseOoxml(existingText);
|
|
18176
|
-
const incomingDoc = parseOoxml(incomingText);
|
|
18177
|
-
if (hasXmlParseError(existingDoc) || hasXmlParseError(incomingDoc)) {
|
|
18178
|
-
return existingText;
|
|
18179
|
-
}
|
|
18180
|
-
const existingRoot = existingDoc.documentElement;
|
|
18181
|
-
const incomingRoot = incomingDoc.documentElement;
|
|
18182
|
-
if (!existingRoot || !incomingRoot) return existingText;
|
|
18183
|
-
const existingAbstractIds = new Set(
|
|
18184
|
-
Array.from(existingRoot.childNodes || []).filter((node) => isDirectWordChild(node, "abstractNum")).map((node) => parseIntegerAttribute(node, ["w:abstractNumId", "abstractNumId"])).filter((id) => id != null)
|
|
18185
|
-
);
|
|
18186
|
-
const existingNumIds = new Set(
|
|
18187
|
-
Array.from(existingRoot.childNodes || []).filter((node) => isDirectWordChild(node, "num")).map((node) => parseIntegerAttribute(node, ["w:numId", "numId"])).filter((id) => id != null)
|
|
18188
|
-
);
|
|
18189
|
-
for (const incomingNode of Array.from(incomingRoot.childNodes || [])) {
|
|
18190
|
-
if (!isDirectWordChild(incomingNode, "abstractNum")) continue;
|
|
18191
|
-
const incomingId = parseIntegerAttribute(incomingNode, ["w:abstractNumId", "abstractNumId"]);
|
|
18192
|
-
if (incomingId == null || existingAbstractIds.has(incomingId)) continue;
|
|
18193
|
-
insertNumberingNodeInSchemaOrder(existingRoot, existingDoc.importNode(incomingNode, true), "abstract");
|
|
18194
|
-
existingAbstractIds.add(incomingId);
|
|
18195
|
-
}
|
|
18196
|
-
for (const incomingNode of Array.from(incomingRoot.childNodes || [])) {
|
|
18197
|
-
if (!isDirectWordChild(incomingNode, "num")) continue;
|
|
18198
|
-
const incomingId = parseIntegerAttribute(incomingNode, ["w:numId", "numId"]);
|
|
18199
|
-
if (incomingId == null || existingNumIds.has(incomingId)) continue;
|
|
18200
|
-
insertNumberingNodeInSchemaOrder(existingRoot, existingDoc.importNode(incomingNode, true), "num");
|
|
18201
|
-
existingNumIds.add(incomingId);
|
|
18202
|
-
}
|
|
18203
|
-
return serializeOoxml(existingDoc);
|
|
18204
|
-
} catch {
|
|
18205
|
-
return existingText;
|
|
18206
|
-
}
|
|
18222
|
+
const markdown = tableMatrixToMarkdown(matrix, columnCount);
|
|
18223
|
+
if (!markdown) return null;
|
|
18224
|
+
onInfo("[Table] Synthesized full markdown table from multiline cell edit for table-scope reconciliation.");
|
|
18225
|
+
return markdown;
|
|
18207
18226
|
}
|
|
18208
18227
|
|
|
18209
18228
|
// pipeline/ingestion-export.js
|
|
@@ -21853,7 +21872,7 @@ function validateDocumentOperation(operation) {
|
|
|
21853
21872
|
}
|
|
21854
21873
|
};
|
|
21855
21874
|
}
|
|
21856
|
-
if (target.revisionView !== "accepted" || target.captureRef || target.occurrence != null || normalized.captureKey || normalized.targetEndDescriptor || normalized.targetEndRef != null
|
|
21875
|
+
if (target.revisionView !== "accepted" || target.captureRef || target.occurrence != null || normalized.captureKey || normalized.targetEndDescriptor || normalized.targetEndRef != null) {
|
|
21857
21876
|
return {
|
|
21858
21877
|
valid: false,
|
|
21859
21878
|
error: {
|
|
@@ -21875,6 +21894,15 @@ function validateDocumentOperation(operation) {
|
|
|
21875
21894
|
}
|
|
21876
21895
|
};
|
|
21877
21896
|
}
|
|
21897
|
+
if (replacementValidation.replacements.some((replacement) => /\r|\n/.test(replacement.find))) {
|
|
21898
|
+
return {
|
|
21899
|
+
valid: false,
|
|
21900
|
+
error: {
|
|
21901
|
+
code: "INVALID_OPERATION",
|
|
21902
|
+
message: 'Localized replacements v1 cannot span a line break; use one replacement per line or a full "modified" paragraph.'
|
|
21903
|
+
}
|
|
21904
|
+
};
|
|
21905
|
+
}
|
|
21878
21906
|
normalized.replacements = replacementValidation.replacements;
|
|
21879
21907
|
}
|
|
21880
21908
|
}
|
|
@@ -22069,6 +22097,12 @@ function validateDocumentOperation(operation) {
|
|
|
22069
22097
|
error: { code: "INVALID_OPERATION", message: 'Format operations require a non-empty "textToFormat" field.' }
|
|
22070
22098
|
};
|
|
22071
22099
|
}
|
|
22100
|
+
if (normalized.textOccurrence != null && (!Number.isInteger(normalized.textOccurrence) || normalized.textOccurrence < 1)) {
|
|
22101
|
+
return {
|
|
22102
|
+
valid: false,
|
|
22103
|
+
error: { code: "INVALID_OPERATION", message: "textOccurrence must be a positive integer when provided." }
|
|
22104
|
+
};
|
|
22105
|
+
}
|
|
22072
22106
|
if (!isRecord(normalized.properties)) {
|
|
22073
22107
|
return {
|
|
22074
22108
|
valid: false,
|
|
@@ -22525,7 +22559,7 @@ function compileOperationBatch(xmlDoc, operations = [], options = {}) {
|
|
|
22525
22559
|
};
|
|
22526
22560
|
const compiled = { ...sourceOperation };
|
|
22527
22561
|
const dynamicOccurrence = operation.targetDescriptor?.occurrence != null && !operation.targetDescriptor?.paragraphId && !operation.targetDescriptor?.fingerprint;
|
|
22528
|
-
const compositeTextTarget = typeof operation.targetDescriptor?.text === "string" && /\r|\n/.test(operation.targetDescriptor.text) && !operation.targetEndDescriptor;
|
|
22562
|
+
const compositeTextTarget = typeof operation.targetDescriptor?.text === "string" && /\r|\n/.test(operation.targetDescriptor.text) && !operation.targetEndDescriptor && !(Array.isArray(operation.replacements) && (operation.targetDescriptor.paragraphId || operation.targetDescriptor.fingerprint));
|
|
22529
22563
|
if (dynamicOccurrence || compositeTextTarget) binding.dynamic = true;
|
|
22530
22564
|
if (validation.valid && operation.operationKind !== "comment_reply" && operation.operationKind !== "comment_resolve" && !operation.targetDescriptor?.captureRef && !binding.dynamic) {
|
|
22531
22565
|
try {
|
|
@@ -22863,6 +22897,17 @@ function deriveSingleParagraphPlainAdjacencyInsertion(currentParagraphText, modi
|
|
|
22863
22897
|
}
|
|
22864
22898
|
return null;
|
|
22865
22899
|
}
|
|
22900
|
+
function deriveParagraphThenTableAppend(modifiedText) {
|
|
22901
|
+
const lines = String(modifiedText || "").split(/\r?\n/g).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
|
|
22902
|
+
if (lines.length < 3) return null;
|
|
22903
|
+
const tableStart = lines.findIndex((line) => line.trim().startsWith("|"));
|
|
22904
|
+
if (tableStart !== 1) return null;
|
|
22905
|
+
const tableLines = lines.slice(1);
|
|
22906
|
+
if (!tableLines.every((line) => line.trim().startsWith("|"))) return null;
|
|
22907
|
+
const separator = tableLines[1].replace(/\s+/g, "");
|
|
22908
|
+
if (!/^\|:?-{3,}:?(\|:?-{3,}:?)+\|?$/.test(separator)) return null;
|
|
22909
|
+
return { paragraphMarkdown: lines[0].trim(), tableText: tableLines.join("\n") };
|
|
22910
|
+
}
|
|
22866
22911
|
|
|
22867
22912
|
// services/capture-engine.js
|
|
22868
22913
|
function createCaptureError(code, message) {
|
|
@@ -24648,6 +24693,81 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
24648
24693
|
numberingXml: null
|
|
24649
24694
|
};
|
|
24650
24695
|
}
|
|
24696
|
+
const tableAppendCandidate = !useTableScope && !hasExplicitRangeScope && !targetListInfo && !containingTable && !targetEndRef ? deriveParagraphThenTableAppend(effectiveModifiedText) : null;
|
|
24697
|
+
const tableAppendParagraph = tableAppendCandidate ? preprocessMarkdown(tableAppendCandidate.paragraphMarkdown) : null;
|
|
24698
|
+
if (tableAppendCandidate && normalizeWhitespaceForTargeting(tableAppendParagraph.cleanText) === normalizeWhitespaceForTargeting(currentParagraphText)) {
|
|
24699
|
+
const tableData = parseTable(tableAppendCandidate.tableText);
|
|
24700
|
+
if (tableData.headers.length > 0 || tableData.rows.length > 0) {
|
|
24701
|
+
onInfo("[Table] Applying retained-paragraph table append heuristic.");
|
|
24702
|
+
const parent2 = targetParagraph.parentNode;
|
|
24703
|
+
if (!parent2) throw new Error("Target paragraph has no parent for table append");
|
|
24704
|
+
const followingNode = targetParagraph.nextSibling;
|
|
24705
|
+
if (tableAppendParagraph.formatHints.length > 0) {
|
|
24706
|
+
const formatted = await applyRedlineToOxml2(
|
|
24707
|
+
serializer.serializeToString(targetParagraph),
|
|
24708
|
+
currentParagraphText || targetText,
|
|
24709
|
+
tableAppendCandidate.paragraphMarkdown,
|
|
24710
|
+
{
|
|
24711
|
+
author,
|
|
24712
|
+
generateRedlines,
|
|
24713
|
+
existingRevisions: options.existingRevisions || "merge-same-author",
|
|
24714
|
+
structuredContent: false,
|
|
24715
|
+
_revisionIdAllocator: revisionIdAllocator
|
|
24716
|
+
}
|
|
24717
|
+
);
|
|
24718
|
+
if (formatted?.status === "error") {
|
|
24719
|
+
return {
|
|
24720
|
+
documentXml,
|
|
24721
|
+
hasChanges: false,
|
|
24722
|
+
numberingXml: null,
|
|
24723
|
+
status: "error",
|
|
24724
|
+
error: formatted.error
|
|
24725
|
+
};
|
|
24726
|
+
}
|
|
24727
|
+
if (formatted?.hasChanges) {
|
|
24728
|
+
if (typeof formatted.oxml !== "string") {
|
|
24729
|
+
throw new Error("Reconciliation engine did not return OOXML for a changed paragraph format");
|
|
24730
|
+
}
|
|
24731
|
+
for (const node of extractReplacementNodes(formatted.oxml).replacementNodes) {
|
|
24732
|
+
const imported = xmlDoc.importNode(node, true);
|
|
24733
|
+
ensureParagraphIdsOnImportedNode(imported, operationSession);
|
|
24734
|
+
options?._mutationLiveNodes?.push(parent2.insertBefore(imported, targetParagraph));
|
|
24735
|
+
}
|
|
24736
|
+
options?._mutationRemovedNodes?.push(targetParagraph);
|
|
24737
|
+
parent2.removeChild(targetParagraph);
|
|
24738
|
+
}
|
|
24739
|
+
}
|
|
24740
|
+
const tableOoxml = generateTableOoxml(tableData, {
|
|
24741
|
+
generateRedlines,
|
|
24742
|
+
author,
|
|
24743
|
+
revisionIdAllocator,
|
|
24744
|
+
trackAsBlock: true
|
|
24745
|
+
});
|
|
24746
|
+
for (const node of extractReplacementNodes(tableOoxml).replacementNodes) {
|
|
24747
|
+
options?._mutationLiveNodes?.push(parent2.insertBefore(xmlDoc.importNode(node, true), followingNode));
|
|
24748
|
+
}
|
|
24749
|
+
const endsContainer = !followingNode || followingNode.nodeType === 1 && followingNode.namespaceURI === NS_W9 && followingNode.localName === "sectPr";
|
|
24750
|
+
if (endsContainer) {
|
|
24751
|
+
const trailing = createWordElement(xmlDoc, "w:p");
|
|
24752
|
+
const trailingPPr = clonePropertiesWithoutRevisionHistory(getDirectWordChild4(targetParagraph, "pPr"));
|
|
24753
|
+
if (trailingPPr) {
|
|
24754
|
+
for (const child of Array.from(trailingPPr.childNodes)) {
|
|
24755
|
+
if (child.nodeType === 1 && ["sectPr", "numPr"].includes(child.localName)) trailingPPr.removeChild(child);
|
|
24756
|
+
}
|
|
24757
|
+
trailing.appendChild(trailingPPr);
|
|
24758
|
+
}
|
|
24759
|
+
if (generateRedlines) markParagraphMarkInserted(xmlDoc, trailing, author);
|
|
24760
|
+
ensureParagraphIdsOnImportedNode(trailing, operationSession);
|
|
24761
|
+
options?._mutationLiveNodes?.push(parent2.insertBefore(trailing, followingNode));
|
|
24762
|
+
}
|
|
24763
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
24764
|
+
return {
|
|
24765
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
24766
|
+
hasChanges: true,
|
|
24767
|
+
numberingXml: null
|
|
24768
|
+
};
|
|
24769
|
+
}
|
|
24770
|
+
}
|
|
24651
24771
|
const plainAdjacencyInsertionCandidate = !bypassSingleParagraphHeuristics && !useTableScope && !hasExplicitRangeScope && !targetListInfo ? deriveSingleParagraphPlainAdjacencyInsertion(currentParagraphText, effectiveModifiedText) : null;
|
|
24652
24772
|
if (plainAdjacencyInsertionCandidate) {
|
|
24653
24773
|
onInfo(
|
|
@@ -24773,6 +24893,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
24773
24893
|
explicitStructuredContent: options.explicitStructuredContent === true,
|
|
24774
24894
|
pairReplacements: options.pairReplacements === true,
|
|
24775
24895
|
insertionAffinity: options.insertionAffinity || null,
|
|
24896
|
+
literalTextEdit: options.literalTextEdit === true,
|
|
24776
24897
|
_revisionIdAllocator: revisionIdAllocator,
|
|
24777
24898
|
_isolatedTableCell: useTableScope
|
|
24778
24899
|
});
|
|
@@ -25060,7 +25181,8 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
|
|
|
25060
25181
|
spanRanges.push({ ...entry, start, end });
|
|
25061
25182
|
}
|
|
25062
25183
|
const targetDescriptor = options.targetDescriptor;
|
|
25063
|
-
const
|
|
25184
|
+
const explicitTextOccurrence = Number.isInteger(options.textOccurrence) && options.textOccurrence > 0 ? options.textOccurrence : null;
|
|
25185
|
+
const occurrence = explicitTextOccurrence ?? targetDescriptor?.occurrence ?? null;
|
|
25064
25186
|
let matchStart = -1;
|
|
25065
25187
|
if (occurrence != null && occurrence > 0) {
|
|
25066
25188
|
let count = 0;
|
|
@@ -25078,6 +25200,17 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
|
|
|
25078
25200
|
} else {
|
|
25079
25201
|
matchStart = fullParaText.indexOf(textToFormat);
|
|
25080
25202
|
}
|
|
25203
|
+
if (matchStart === -1 && explicitTextOccurrence != null && fullParaText.includes(textToFormat)) {
|
|
25204
|
+
return {
|
|
25205
|
+
documentXml,
|
|
25206
|
+
hasChanges: false,
|
|
25207
|
+
status: "error",
|
|
25208
|
+
error: {
|
|
25209
|
+
code: "PATCH_SOURCE_NOT_FOUND",
|
|
25210
|
+
message: `Occurrence ${explicitTextOccurrence} of text to format was not found in target paragraph: "${textToFormat}".`
|
|
25211
|
+
}
|
|
25212
|
+
};
|
|
25213
|
+
}
|
|
25081
25214
|
if (matchStart === -1) {
|
|
25082
25215
|
return {
|
|
25083
25216
|
documentXml,
|
|
@@ -25360,9 +25493,11 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
|
|
|
25360
25493
|
...operation.existingRevisions ? { existingRevisions: operation.existingRevisions } : {},
|
|
25361
25494
|
structuredContent: typeof operation.structuredContent === "boolean" ? operation.structuredContent : options.structuredContent !== false,
|
|
25362
25495
|
explicitStructuredContent: operation.structuredContent === true,
|
|
25496
|
+
...operation._localizedReplacementCompilation && operation.structuredContent !== true ? { literalTextEdit: true } : {},
|
|
25363
25497
|
pairReplacements: typeof operation.pairReplacements === "boolean" ? operation.pairReplacements : options.pairReplacements !== false,
|
|
25364
25498
|
...operation.insertionAffinity ? { insertionAffinity: operation.insertionAffinity } : {},
|
|
25365
25499
|
...operation.formattingRevisionPolicy ? { formattingRevisionPolicy: operation.formattingRevisionPolicy } : {},
|
|
25500
|
+
...operation.operationKind === "format" && Number.isInteger(operation.textOccurrence) ? { textOccurrence: operation.textOccurrence } : {},
|
|
25366
25501
|
targetDescriptor: operation.targetDescriptor,
|
|
25367
25502
|
targetEndDescriptor: operation.targetEndDescriptor,
|
|
25368
25503
|
_compiledSourceId: operation._compiledSourceId || null,
|
|
@@ -25986,6 +26121,9 @@ async function applyOperationsToDocumentXml(documentXml, operations, author, run
|
|
|
25986
26121
|
const scheduled = dependencyPlan.scheduled;
|
|
25987
26122
|
const atomic = options.atomic === true;
|
|
25988
26123
|
const continueOnError = options.continueOnError !== false;
|
|
26124
|
+
if (runtimeContext && typeof runtimeContext === "object" && !runtimeContext.numberingIdState && typeof runtimeContext.numberingXml === "string" && runtimeContext.numberingXml.trim()) {
|
|
26125
|
+
runtimeContext.numberingIdState = createDynamicNumberingIdState(runtimeContext.numberingXml);
|
|
26126
|
+
}
|
|
25989
26127
|
const context = cloneBatchRuntimeContext(runtimeContext);
|
|
25990
26128
|
if (!(context.targetRefSnapshot instanceof Map)) {
|
|
25991
26129
|
context.targetRefSnapshot = session.initialTargetReferenceSnapshot;
|
|
@@ -28411,8 +28549,32 @@ function openDocx(input) {
|
|
|
28411
28549
|
}
|
|
28412
28550
|
|
|
28413
28551
|
// index.js
|
|
28552
|
+
function remapGeneratedNumberingForSource(result, options) {
|
|
28553
|
+
const sourceNumbering = typeof options?.numberingXml === "string" && options.numberingXml.trim() ? options.numberingXml : null;
|
|
28554
|
+
const state = options?.numberingIdState && typeof options.numberingIdState === "object" ? options.numberingIdState : sourceNumbering ? createDynamicNumberingIdState(sourceNumbering) : null;
|
|
28555
|
+
if (!state || !result?.hasChanges || typeof result.oxml !== "string" || !result.oxml.includes("/word/numbering.xml")) {
|
|
28556
|
+
return result;
|
|
28557
|
+
}
|
|
28558
|
+
const pkgDoc = parseOoxml(result.oxml);
|
|
28559
|
+
const parts = Array.from(pkgDoc.getElementsByTagNameNS("*", "part"));
|
|
28560
|
+
const payloadOf = (name) => {
|
|
28561
|
+
const part = parts.find((candidate) => (candidate.getAttribute("pkg:name") || candidate.getAttribute("name")) === name);
|
|
28562
|
+
const xmlData = part?.getElementsByTagNameNS("*", "xmlData")[0];
|
|
28563
|
+
return xmlData ? Array.from(xmlData.childNodes).find((node) => node.nodeType === 1) || null : null;
|
|
28564
|
+
};
|
|
28565
|
+
const numberingNode = payloadOf("/word/numbering.xml");
|
|
28566
|
+
const body = payloadOf("/word/document.xml")?.getElementsByTagNameNS("*", "body")[0];
|
|
28567
|
+
if (!numberingNode || !body) return result;
|
|
28568
|
+
const bodyNodes = Array.from(body.childNodes).filter((node) => node.nodeType === 1 && node.localName !== "sectPr");
|
|
28569
|
+
const remapped = remapNumberingPayloadForDocument(serializeOoxml(numberingNode), bodyNodes, state);
|
|
28570
|
+
const newNumbering = pkgDoc.importNode(parseOoxml(remapped.numberingXml).documentElement, true);
|
|
28571
|
+
numberingNode.parentNode.replaceChild(newNumbering, numberingNode);
|
|
28572
|
+
bodyNodes.forEach((node, index) => body.replaceChild(remapped.replacementNodes[index], node));
|
|
28573
|
+
return { ...result, oxml: serializeOoxml(pkgDoc) };
|
|
28574
|
+
}
|
|
28414
28575
|
async function applyRedlineToOxml3(oxml, originalText, modifiedText, options = {}) {
|
|
28415
|
-
const
|
|
28576
|
+
const rawResult = await applyRedlineToOxml(oxml, originalText, modifiedText, options);
|
|
28577
|
+
const result = remapGeneratedNumberingForSource(rawResult, options);
|
|
28416
28578
|
if (result?.useNativeApi && typeof result?.oxml !== "string") {
|
|
28417
28579
|
const existingWarnings = Array.isArray(result?.warnings) ? result.warnings : [];
|
|
28418
28580
|
return withOoxmlSourceType({
|