@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 bundle v0.8.
|
|
1
|
+
// @ansonlai/docx-redline-js standalone 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;
|
|
@@ -9733,7 +9733,14 @@ function buildRunXmlWithHints(item, formatHints, options = {}) {
|
|
|
9733
9733
|
function buildSimpleRun(text2, rPrXml) {
|
|
9734
9734
|
if (!text2) return "";
|
|
9735
9735
|
const rPr = rPrXml || "";
|
|
9736
|
-
return `<w:r>${rPr}
|
|
9736
|
+
return `<w:r>${rPr}${buildRunContent(text2, "w:t")}</w:r>`;
|
|
9737
|
+
}
|
|
9738
|
+
function buildRunContent(text2, textTag) {
|
|
9739
|
+
return String(text2).split(/(\r\n|[\n\r\v\t])/).map((part) => {
|
|
9740
|
+
if (part === " ") return "<w:tab/>";
|
|
9741
|
+
if (part === "\n" || part === "\r" || part === "\r\n" || part === "\v") return "<w:br/>";
|
|
9742
|
+
return part ? `<${textTag} xml:space="preserve">${escapeXml(part)}</${textTag}>` : "";
|
|
9743
|
+
}).join("");
|
|
9737
9744
|
}
|
|
9738
9745
|
function buildDeletionXml(item, options = {}) {
|
|
9739
9746
|
const metadata = createRevisionMetadata(
|
|
@@ -9746,7 +9753,7 @@ function buildDeletionXml(item, options = {}) {
|
|
|
9746
9753
|
if (font) {
|
|
9747
9754
|
rPr = applyFont(rPr, font);
|
|
9748
9755
|
}
|
|
9749
|
-
return `<w:del w:id="${metadata.id}" w:author="${escapeXml(metadata.author)}" w:date="${metadata.date}"><w:r>${rPr}
|
|
9756
|
+
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>`;
|
|
9750
9757
|
}
|
|
9751
9758
|
function buildInsertionXml(item, formatHints, options = {}) {
|
|
9752
9759
|
const metadata = createRevisionMetadata(
|
|
@@ -10087,7 +10094,7 @@ function generateTableOoxml(tableData, options = {}) {
|
|
|
10087
10094
|
revisionIdAllocator = null,
|
|
10088
10095
|
trackAsBlock = false
|
|
10089
10096
|
} = options;
|
|
10090
|
-
const
|
|
10097
|
+
const trackRows = generateRedlines && trackAsBlock;
|
|
10091
10098
|
const numCols = tableData.headers?.length || (tableData.rows?.[0]?.length || 1);
|
|
10092
10099
|
const tblPr = `
|
|
10093
10100
|
<w:tblPr>
|
|
@@ -10115,14 +10122,16 @@ function generateTableOoxml(tableData, options = {}) {
|
|
|
10115
10122
|
const cellText = row[c] || "";
|
|
10116
10123
|
const { cleanText, formatHints } = preprocessMarkdown(cellText);
|
|
10117
10124
|
const runModel = [{
|
|
10118
|
-
kind: generateRedlines
|
|
10125
|
+
kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
|
|
10119
10126
|
text: cleanText,
|
|
10120
10127
|
rPrXml: isHeaderRow ? "<w:rPr><w:b/></w:rPr>" : "",
|
|
10121
10128
|
author,
|
|
10122
10129
|
startOffset: 0,
|
|
10123
10130
|
endOffset: cleanText.length
|
|
10124
10131
|
}];
|
|
10125
|
-
const
|
|
10132
|
+
const cellMarkMeta = trackRows ? createRevisionMetadata(author, revisionIdAllocator) : null;
|
|
10133
|
+
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;
|
|
10134
|
+
const runsOoxml = serializeToOoxml(runModel, cellPPr, formatHints, {
|
|
10126
10135
|
author,
|
|
10127
10136
|
generateRedlines,
|
|
10128
10137
|
revisionIdAllocator
|
|
@@ -10131,13 +10140,11 @@ function generateTableOoxml(tableData, options = {}) {
|
|
|
10131
10140
|
cellsXml += `<w:tc>${tcPr}${runsOoxml}</w:tc>`;
|
|
10132
10141
|
}
|
|
10133
10142
|
const trPr = isHeaderRow ? "<w:trPr><w:tblHeader/><w:cantSplit/></w:trPr>" : "<w:trPr><w:cantSplit/></w:trPr>";
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
if (tableInsertMeta) {
|
|
10138
|
-
tableXml = `<w:ins w:id="${tableInsertMeta.id}" w:author="${escapeXml(tableInsertMeta.author)}" w:date="${tableInsertMeta.date}">${tableXml}</w:ins>`;
|
|
10143
|
+
const rowMeta = trackRows ? createRevisionMetadata(author, revisionIdAllocator) : null;
|
|
10144
|
+
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;
|
|
10145
|
+
rowsXml += `<w:tr>${trackedTrPr}${cellsXml}</w:tr>`;
|
|
10139
10146
|
}
|
|
10140
|
-
return
|
|
10147
|
+
return `<w:tbl>${tblPr}${tblGrid}${rowsXml}</w:tbl>`;
|
|
10141
10148
|
}
|
|
10142
10149
|
function diffTablesWithVirtualGrid(oldGrid, newTableData) {
|
|
10143
10150
|
const operations = [];
|
|
@@ -15061,6 +15068,11 @@ function rejectTrackedChangesInOoxml(oxml, options = {}) {
|
|
|
15061
15068
|
}
|
|
15062
15069
|
if (removeNode(insNode)) rejectedCount += 1;
|
|
15063
15070
|
}
|
|
15071
|
+
for (const tableNode of getWordElementsByLocalName(xmlDoc, "tbl")) {
|
|
15072
|
+
if (!tableNode.parentNode) continue;
|
|
15073
|
+
const hasRow = Array.from(tableNode.childNodes || []).some((child) => isWordElement5(child, "tr"));
|
|
15074
|
+
if (!hasRow && removeNode(tableNode)) rejectedCount += 1;
|
|
15075
|
+
}
|
|
15064
15076
|
for (const delNode of getWordElementsByLocalName(xmlDoc, "del")) {
|
|
15065
15077
|
if (!delNode.parentNode || !authorMatchesNode(delNode, filter)) continue;
|
|
15066
15078
|
if (isParagraphMarkRevisionMarker(delNode)) {
|
|
@@ -15690,7 +15702,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
15690
15702
|
operationWarnings.push("Input was sanitized; pass sanitizeInput: false to disable.");
|
|
15691
15703
|
}
|
|
15692
15704
|
let structuredAnalysis = null;
|
|
15693
|
-
if (options.structuredContent !== false) {
|
|
15705
|
+
if (options.structuredContent !== false && options.literalTextEdit !== true) {
|
|
15694
15706
|
structuredAnalysis = analyzeStructuredContent(sanitizedText);
|
|
15695
15707
|
if (!structuredAnalysis.valid) {
|
|
15696
15708
|
if (options.explicitStructuredContent === true) {
|
|
@@ -15709,13 +15721,16 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
15709
15721
|
}
|
|
15710
15722
|
}
|
|
15711
15723
|
const { cleanText: cleanModifiedText, formatHints } = preprocessMarkdown(sanitizedText);
|
|
15712
|
-
const hasTextChanges = existingRevisionsPolicy === "slice-cross-author" ? cleanModifiedText !== originalText : cleanModifiedText.trim() !== originalText.trim();
|
|
15713
|
-
const hasFormatHints = formatHints.length > 0;
|
|
15714
15724
|
const { existingFormatHints, textSpans, paragraphs } = extractFormattingFromOoxml(xmlDoc);
|
|
15725
|
+
const hasLiteralTextChanges = existingRevisionsPolicy === "slice-cross-author" ? cleanModifiedText !== originalText : cleanModifiedText.trim() !== originalText.trim();
|
|
15726
|
+
const hasListStructureChange = !hasLiteralTextChanges && options.explicitStructuredContent === true && isListTargetLoose(cleanModifiedText) && !sourceMatchesListShape(paragraphs, cleanModifiedText);
|
|
15727
|
+
const hasTextChanges = hasLiteralTextChanges || hasListStructureChange;
|
|
15728
|
+
const hasFormatHints = formatHints.length > 0;
|
|
15715
15729
|
const hasExistingFormatting = existingFormatHints.length > 0;
|
|
15716
|
-
const
|
|
15730
|
+
const { textSpans: targetSpans } = buildTextSpansFromParagraphs(paragraphs);
|
|
15731
|
+
const visibleText = targetSpans.map((span) => textSpanVisibleText(span)).join("");
|
|
15717
15732
|
const targetFound = originalText.includes("\n") || originalText.includes("\r") ? originalText.split(/\r?\n/).map(normalizeTargetText).filter(Boolean).every((line) => paragraphs.some((paragraph) => {
|
|
15718
|
-
const paragraphText =
|
|
15733
|
+
const paragraphText = targetSpans.filter((span) => span.paragraph === paragraph).map(textSpanVisibleText).join("");
|
|
15719
15734
|
return normalizeTargetText(paragraphText).includes(line);
|
|
15720
15735
|
})) : visibleText.includes(originalText.trim()) || visibleText.replace(/[\t\n\u2011]/g, "").includes(originalText.trim().replace(/[\t\n\u2011]/g, "")) || normalizeTargetText(visibleText).includes(normalizeTargetText(originalText));
|
|
15721
15736
|
if (hasTextChanges && typeof originalText === "string" && originalText.trim() && !targetFound) {
|
|
@@ -15829,7 +15844,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
15829
15844
|
const tables = getElementsByTagNSOrTag(xmlDoc, NS_W, "tbl");
|
|
15830
15845
|
const hasTables = tables.length > 0;
|
|
15831
15846
|
const isMarkdownTable = /^\|.+\|/.test(cleanModifiedText.trim()) && cleanModifiedText.includes("\n");
|
|
15832
|
-
const isTargetList = isListTargetLoose(cleanModifiedText);
|
|
15847
|
+
const isTargetList = options.literalTextEdit !== true && isListTargetLoose(cleanModifiedText);
|
|
15833
15848
|
const isStructuredContent = options.structuredContent !== false && structuredAnalysis?.requiresStructuredContent === true;
|
|
15834
15849
|
const tableCellContext = initialTableCellContext;
|
|
15835
15850
|
const usesSurgicalTextMode = hasTables || existingRevisionsPolicy === "slice-cross-author";
|
|
@@ -15944,6 +15959,10 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
15944
15959
|
throw caught;
|
|
15945
15960
|
}
|
|
15946
15961
|
}
|
|
15962
|
+
function sourceMatchesListShape(paragraphs, listText) {
|
|
15963
|
+
const itemCount = String(listText).split(/\r?\n/).filter((line) => line.trim()).length;
|
|
15964
|
+
return paragraphs.length === itemCount && paragraphs.every((paragraph) => getElementsByTagNSOrTag(paragraph, NS_W, "numPr").length > 0);
|
|
15965
|
+
}
|
|
15947
15966
|
function normalizeTargetText(text2) {
|
|
15948
15967
|
return String(text2 || "").replace(/[\t\n\u2011]/g, " ").replace(/\s+/g, " ").trim();
|
|
15949
15968
|
}
|
|
@@ -17413,659 +17432,659 @@ async function executeSingleLineListStructuralFallback(plan, options = {}) {
|
|
|
17413
17432
|
};
|
|
17414
17433
|
}
|
|
17415
17434
|
|
|
17416
|
-
//
|
|
17417
|
-
|
|
17418
|
-
|
|
17419
|
-
|
|
17420
|
-
|
|
17421
|
-
|
|
17422
|
-
|
|
17423
|
-
|
|
17424
|
-
}
|
|
17425
|
-
|
|
17426
|
-
return node.getAttribute(`w:${name}`) || node.getAttribute(name) || "";
|
|
17427
|
-
}
|
|
17428
|
-
function xmlSpaceAttribute(node) {
|
|
17429
|
-
return node.getAttribute("xml:space") || node.getAttribute("space") || node.getAttributeNS?.("http://www.w3.org/XML/1998/namespace", "space") || "";
|
|
17430
|
-
}
|
|
17431
|
-
function isParagraphMarkRevision(node) {
|
|
17432
|
-
return localNameOf3(node.parentNode) === "rPr";
|
|
17435
|
+
// services/numbering-helpers.js
|
|
17436
|
+
function parseIntegerAttribute(element, names) {
|
|
17437
|
+
if (!element || !Array.isArray(names)) return null;
|
|
17438
|
+
for (const name of names) {
|
|
17439
|
+
const raw = element.getAttribute(name);
|
|
17440
|
+
if (raw == null || raw === "") continue;
|
|
17441
|
+
const parsed = Number.parseInt(String(raw), 10);
|
|
17442
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
17443
|
+
}
|
|
17444
|
+
return null;
|
|
17433
17445
|
}
|
|
17434
|
-
function
|
|
17435
|
-
|
|
17436
|
-
|
|
17437
|
-
|
|
17438
|
-
|
|
17439
|
-
|
|
17446
|
+
function nextAvailableId(startId, occupiedIds, maxPreferred = null) {
|
|
17447
|
+
let candidate = Number.isInteger(startId) && startId > 0 ? startId : 1;
|
|
17448
|
+
const occupied = occupiedIds instanceof Set ? occupiedIds : /* @__PURE__ */ new Set();
|
|
17449
|
+
while (occupied.has(candidate)) {
|
|
17450
|
+
candidate += 1;
|
|
17451
|
+
}
|
|
17452
|
+
if (Number.isInteger(maxPreferred) && maxPreferred > 0 && candidate > maxPreferred) {
|
|
17453
|
+
for (let probe = 1; probe <= maxPreferred; probe += 1) {
|
|
17454
|
+
if (!occupied.has(probe)) return probe;
|
|
17440
17455
|
}
|
|
17441
|
-
|
|
17442
|
-
|
|
17443
|
-
|
|
17444
|
-
|
|
17445
|
-
|
|
17456
|
+
}
|
|
17457
|
+
return candidate;
|
|
17458
|
+
}
|
|
17459
|
+
function createDynamicNumberingIdState(numberingXml, options = {}) {
|
|
17460
|
+
const minId = Number.isInteger(options?.minId) && options.minId > 0 ? options.minId : 1;
|
|
17461
|
+
const maxPreferred = Number.isInteger(options?.maxPreferred) && options.maxPreferred >= minId ? options.maxPreferred : 32767;
|
|
17462
|
+
const usedNumIds = /* @__PURE__ */ new Set();
|
|
17463
|
+
const usedAbstractNumIds = /* @__PURE__ */ new Set();
|
|
17464
|
+
if (String(numberingXml || "").trim()) {
|
|
17446
17465
|
try {
|
|
17447
|
-
|
|
17448
|
-
|
|
17449
|
-
|
|
17466
|
+
const numberingDoc = parseOoxml(numberingXml);
|
|
17467
|
+
const abstractNums = Array.from(numberingDoc.getElementsByTagNameNS("*", "abstractNum"));
|
|
17468
|
+
const nums = Array.from(numberingDoc.getElementsByTagNameNS("*", "num"));
|
|
17469
|
+
for (const abstractNum of abstractNums) {
|
|
17470
|
+
const id = parseIntegerAttribute(abstractNum, ["w:abstractNumId", "abstractNumId"]);
|
|
17471
|
+
if (id != null) usedAbstractNumIds.add(id);
|
|
17472
|
+
}
|
|
17473
|
+
for (const num of nums) {
|
|
17474
|
+
const id = parseIntegerAttribute(num, ["w:numId", "numId"]);
|
|
17475
|
+
if (id != null) usedNumIds.add(id);
|
|
17476
|
+
}
|
|
17477
|
+
} catch {
|
|
17450
17478
|
}
|
|
17451
17479
|
}
|
|
17480
|
+
const maxUsedNumId = usedNumIds.size > 0 ? Math.max(...usedNumIds) : 0;
|
|
17481
|
+
const maxUsedAbstractNumId = usedAbstractNumIds.size > 0 ? Math.max(...usedAbstractNumIds) : 0;
|
|
17482
|
+
const baseNumId = Math.max(minId, maxUsedNumId + 1);
|
|
17483
|
+
const baseAbstractNumId = Math.max(minId, maxUsedAbstractNumId + 1);
|
|
17484
|
+
return {
|
|
17485
|
+
// Kept so list edits can recognise which existing definition a source list uses and continue it.
|
|
17486
|
+
sourceNumberingXml: String(numberingXml || ""),
|
|
17487
|
+
nextNumId: nextAvailableId(baseNumId, usedNumIds, maxPreferred),
|
|
17488
|
+
nextAbstractNumId: nextAvailableId(baseAbstractNumId, usedAbstractNumIds, maxPreferred),
|
|
17489
|
+
usedNumIds,
|
|
17490
|
+
usedAbstractNumIds,
|
|
17491
|
+
minId,
|
|
17492
|
+
maxPreferred
|
|
17493
|
+
};
|
|
17452
17494
|
}
|
|
17453
|
-
function
|
|
17454
|
-
|
|
17455
|
-
|
|
17456
|
-
|
|
17457
|
-
if (!
|
|
17458
|
-
|
|
17459
|
-
return { valid: false, issues };
|
|
17495
|
+
function normalizeNumberingIdState(state) {
|
|
17496
|
+
if (!state || typeof state !== "object") return null;
|
|
17497
|
+
if (!(state.usedNumIds instanceof Set)) state.usedNumIds = /* @__PURE__ */ new Set();
|
|
17498
|
+
if (!(state.usedAbstractNumIds instanceof Set)) state.usedAbstractNumIds = /* @__PURE__ */ new Set();
|
|
17499
|
+
if (!Number.isInteger(state.minId) || state.minId < 1) {
|
|
17500
|
+
state.minId = 1;
|
|
17460
17501
|
}
|
|
17461
|
-
|
|
17462
|
-
|
|
17463
|
-
addIssue("PARSE_ERROR", "error", `OOXML does not parse as XML: ${error2}`);
|
|
17464
|
-
return { valid: false, issues };
|
|
17502
|
+
if (!Number.isInteger(state.maxPreferred) || state.maxPreferred < state.minId) {
|
|
17503
|
+
state.maxPreferred = 32767;
|
|
17465
17504
|
}
|
|
17466
|
-
|
|
17467
|
-
|
|
17468
|
-
const revisions = insElements.concat(delElements);
|
|
17469
|
-
for (const paragraph of elementsByLocalName(doc, "p")) {
|
|
17470
|
-
const nested = Array.from(paragraph.getElementsByTagName("*")).find((el) => el !== paragraph && localNameOf3(el) === "p");
|
|
17471
|
-
if (nested) {
|
|
17472
|
-
addIssue("NESTED_PARAGRAPH", "error", `<${paragraph.nodeName}> contains nested <${nested.nodeName}>.`);
|
|
17473
|
-
}
|
|
17505
|
+
if (!Number.isInteger(state.nextNumId) || state.nextNumId < state.minId) {
|
|
17506
|
+
state.nextNumId = state.minId;
|
|
17474
17507
|
}
|
|
17475
|
-
|
|
17476
|
-
|
|
17477
|
-
const sectPrIndexes = children.map((child, index) => localNameOf3(child) === "sectPr" ? index : -1).filter((index) => index >= 0);
|
|
17478
|
-
if (sectPrIndexes.length > 1) {
|
|
17479
|
-
addIssue("MULTIPLE_BODY_SECTPR", "error", "<w:body> contains multiple direct <w:sectPr> elements.");
|
|
17480
|
-
} else if (sectPrIndexes.length === 1 && sectPrIndexes[0] !== children.length - 1) {
|
|
17481
|
-
addIssue("SECTPR_NOT_LAST", "error", "<w:sectPr> is not the last element child of <w:body>.");
|
|
17482
|
-
}
|
|
17508
|
+
if (!Number.isInteger(state.nextAbstractNumId) || state.nextAbstractNumId < state.minId) {
|
|
17509
|
+
state.nextAbstractNumId = state.minId;
|
|
17483
17510
|
}
|
|
17484
|
-
|
|
17485
|
-
|
|
17486
|
-
|
|
17487
|
-
|
|
17488
|
-
|
|
17489
|
-
|
|
17490
|
-
|
|
17491
|
-
|
|
17492
|
-
|
|
17493
|
-
|
|
17494
|
-
|
|
17495
|
-
|
|
17496
|
-
|
|
17511
|
+
state.nextNumId = nextAvailableId(state.nextNumId, state.usedNumIds, state.maxPreferred);
|
|
17512
|
+
state.nextAbstractNumId = nextAvailableId(state.nextAbstractNumId, state.usedAbstractNumIds, state.maxPreferred);
|
|
17513
|
+
return state;
|
|
17514
|
+
}
|
|
17515
|
+
function reserveNextNumberingId(state, kind = "num") {
|
|
17516
|
+
const normalized = normalizeNumberingIdState(state);
|
|
17517
|
+
if (!normalized) return null;
|
|
17518
|
+
const useAbstract = kind === "abstract";
|
|
17519
|
+
const id = useAbstract ? normalized.nextAbstractNumId : normalized.nextNumId;
|
|
17520
|
+
if (!Number.isInteger(id) || id < 1) return null;
|
|
17521
|
+
if (useAbstract) {
|
|
17522
|
+
normalized.usedAbstractNumIds.add(id);
|
|
17523
|
+
normalized.nextAbstractNumId = nextAvailableId(id + 1, normalized.usedAbstractNumIds, normalized.maxPreferred);
|
|
17524
|
+
} else {
|
|
17525
|
+
normalized.usedNumIds.add(id);
|
|
17526
|
+
normalized.nextNumId = nextAvailableId(id + 1, normalized.usedNumIds, normalized.maxPreferred);
|
|
17497
17527
|
}
|
|
17498
|
-
|
|
17499
|
-
|
|
17500
|
-
|
|
17501
|
-
|
|
17502
|
-
|
|
17503
|
-
|
|
17504
|
-
|
|
17505
|
-
|
|
17506
|
-
|
|
17528
|
+
return id;
|
|
17529
|
+
}
|
|
17530
|
+
function reserveNextNumberingIdPair(state) {
|
|
17531
|
+
const numId = reserveNextNumberingId(state, "num");
|
|
17532
|
+
const abstractNumId = reserveNextNumberingId(state, "abstract");
|
|
17533
|
+
if (numId == null || abstractNumId == null) return null;
|
|
17534
|
+
return { numId, abstractNumId };
|
|
17535
|
+
}
|
|
17536
|
+
function hasXmlParseError(doc) {
|
|
17537
|
+
if (!doc || !doc.documentElement) return true;
|
|
17538
|
+
if (doc.documentElement.localName === "parsererror") return true;
|
|
17539
|
+
return doc.getElementsByTagName("parsererror").length > 0;
|
|
17540
|
+
}
|
|
17541
|
+
function isDirectWordChild(node, localName2) {
|
|
17542
|
+
return !!(node && node.nodeType === 1 && node.namespaceURI === WORD_MAIN_NS && node.localName === localName2);
|
|
17543
|
+
}
|
|
17544
|
+
function insertNumberingNodeInSchemaOrder(root, node, kind) {
|
|
17545
|
+
if (!root || !node) return;
|
|
17546
|
+
const directChildren = Array.from(root.childNodes || []).filter(
|
|
17547
|
+
(child) => child && child.nodeType === 1 && child.namespaceURI === WORD_MAIN_NS
|
|
17548
|
+
);
|
|
17549
|
+
let anchor = null;
|
|
17550
|
+
if (kind === "abstract") {
|
|
17551
|
+
anchor = directChildren.find(
|
|
17552
|
+
(child) => child.localName === "num" || child.localName === "numIdMacAtCleanup"
|
|
17553
|
+
) || null;
|
|
17554
|
+
} else {
|
|
17555
|
+
anchor = directChildren.find((child) => child.localName === "numIdMacAtCleanup") || null;
|
|
17507
17556
|
}
|
|
17508
|
-
|
|
17509
|
-
|
|
17510
|
-
|
|
17511
|
-
|
|
17512
|
-
|
|
17513
|
-
|
|
17514
|
-
|
|
17515
|
-
|
|
17516
|
-
|
|
17517
|
-
|
|
17518
|
-
|
|
17557
|
+
if (anchor) root.insertBefore(node, anchor);
|
|
17558
|
+
else root.appendChild(node);
|
|
17559
|
+
}
|
|
17560
|
+
function getAttributeFirst2(element, names) {
|
|
17561
|
+
for (const name of names || []) {
|
|
17562
|
+
const value = element?.getAttribute?.(name);
|
|
17563
|
+
if (value != null && value !== "") return value;
|
|
17564
|
+
}
|
|
17565
|
+
return null;
|
|
17566
|
+
}
|
|
17567
|
+
function getElementId2(element, names) {
|
|
17568
|
+
const raw = getAttributeFirst2(element, names);
|
|
17569
|
+
const parsed = Number.parseInt(String(raw || ""), 10);
|
|
17570
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
17571
|
+
}
|
|
17572
|
+
function setElementId(element, preferredName, idValue) {
|
|
17573
|
+
element?.setAttribute?.(preferredName, String(idValue));
|
|
17574
|
+
}
|
|
17575
|
+
function setElementVal2(element, value) {
|
|
17576
|
+
element?.setAttribute?.("w:val", String(value));
|
|
17577
|
+
}
|
|
17578
|
+
function overwriteParagraphNumIds(paragraphNodes, targetNumId) {
|
|
17579
|
+
if (!Array.isArray(paragraphNodes) || targetNumId == null) return;
|
|
17580
|
+
for (const node of paragraphNodes) {
|
|
17581
|
+
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []);
|
|
17582
|
+
for (const numIdNode of numIdNodes) {
|
|
17583
|
+
setElementVal2(numIdNode, targetNumId);
|
|
17519
17584
|
}
|
|
17520
17585
|
}
|
|
17521
|
-
|
|
17522
|
-
|
|
17523
|
-
|
|
17524
|
-
const
|
|
17525
|
-
|
|
17526
|
-
|
|
17527
|
-
|
|
17586
|
+
}
|
|
17587
|
+
function extractFirstParagraphNumId(paragraphNodes) {
|
|
17588
|
+
for (const node of paragraphNodes || []) {
|
|
17589
|
+
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []);
|
|
17590
|
+
for (const numIdNode of numIdNodes) {
|
|
17591
|
+
const numId = getElementId2(numIdNode, ["w:val", "val"]);
|
|
17592
|
+
if (numId != null) return String(numId);
|
|
17528
17593
|
}
|
|
17529
|
-
seenIds.add(id);
|
|
17530
17594
|
}
|
|
17531
|
-
|
|
17532
|
-
for (const node of textNodes) {
|
|
17533
|
-
const text2 = node.textContent || "";
|
|
17534
|
-
if (/^\s|\s$/.test(text2) && xmlSpaceAttribute(node) !== "preserve") {
|
|
17535
|
-
addIssue(
|
|
17536
|
-
"MISSING_SPACE_PRESERVE",
|
|
17537
|
-
"error",
|
|
17538
|
-
`<${node.nodeName}> has boundary whitespace without xml:space="preserve".`
|
|
17539
|
-
);
|
|
17540
|
-
}
|
|
17541
|
-
if (text2 === "") {
|
|
17542
|
-
addIssue("EMPTY_TEXT_ELEMENT", "warning", `<${node.nodeName}> is empty.`);
|
|
17543
|
-
}
|
|
17544
|
-
}
|
|
17545
|
-
for (const revision of revisions) {
|
|
17546
|
-
if (isParagraphMarkRevision(revision)) continue;
|
|
17547
|
-
const hasElementChild = Array.from(revision.childNodes || []).some((child) => child.nodeType === 1);
|
|
17548
|
-
if (!hasElementChild) {
|
|
17549
|
-
addIssue(
|
|
17550
|
-
"EMPTY_REVISION_WRAPPER",
|
|
17551
|
-
"warning",
|
|
17552
|
-
`<${revision.nodeName}> (w:id="${wordAttribute3(revision, "id")}") wraps no content.`
|
|
17553
|
-
);
|
|
17554
|
-
}
|
|
17555
|
-
}
|
|
17556
|
-
for (const resurrection of findForeignDeletedParagraphResurrections(doc)) {
|
|
17557
|
-
const ownerAuthor = resurrection.ownerAuthor || "unattributed";
|
|
17558
|
-
addIssue(
|
|
17559
|
-
"FOREIGN_PARAGRAPH_MARK_DELETION",
|
|
17560
|
-
"warning",
|
|
17561
|
-
`Paragraph deleted by ${ownerAuthor} also contains non-empty insertion content from another author; Accept/Reject lifecycle may not preserve the apparent restoration.`
|
|
17562
|
-
);
|
|
17563
|
-
}
|
|
17564
|
-
return { valid: !issues.some((issue) => issue.severity === "error"), issues };
|
|
17595
|
+
return null;
|
|
17565
17596
|
}
|
|
17566
|
-
|
|
17567
|
-
|
|
17568
|
-
|
|
17569
|
-
|
|
17570
|
-
|
|
17571
|
-
|
|
17572
|
-
|
|
17597
|
+
function buildExplicitDecimalMultilevelNumberingXml(numId, abstractNumId, startAt) {
|
|
17598
|
+
const safeNumId = String(numId);
|
|
17599
|
+
const safeAbstractNumId = String(abstractNumId);
|
|
17600
|
+
const safeStartAt = Number.isInteger(startAt) && startAt > 0 ? startAt : 1;
|
|
17601
|
+
const levelsXml = Array.from({ length: 9 }, (_, level) => {
|
|
17602
|
+
const lvlText = Array.from({ length: level + 1 }, (_2, i2) => `%${i2 + 1}`).join(".") + ".";
|
|
17603
|
+
const left = 720 * (level + 1);
|
|
17604
|
+
return `
|
|
17605
|
+
<w:lvl w:ilvl="${level}">
|
|
17606
|
+
<w:start w:val="1"/>
|
|
17607
|
+
<w:numFmt w:val="decimal"/>
|
|
17608
|
+
<w:lvlText w:val="${lvlText}"/>
|
|
17609
|
+
<w:lvlJc w:val="left"/>
|
|
17610
|
+
<w:pPr><w:ind w:left="${left}" w:hanging="360"/></w:pPr>
|
|
17611
|
+
</w:lvl>`;
|
|
17612
|
+
}).join("");
|
|
17613
|
+
return `
|
|
17614
|
+
<w:numbering xmlns:w="${WORD_MAIN_NS}">
|
|
17615
|
+
<w:abstractNum w:abstractNumId="${safeAbstractNumId}">
|
|
17616
|
+
<w:multiLevelType w:val="multilevel"/>
|
|
17617
|
+
${levelsXml}
|
|
17618
|
+
</w:abstractNum>
|
|
17619
|
+
<w:num w:numId="${safeNumId}">
|
|
17620
|
+
<w:abstractNumId w:val="${safeAbstractNumId}"/>
|
|
17621
|
+
<w:lvlOverride w:ilvl="0">
|
|
17622
|
+
<w:startOverride w:val="${safeStartAt}"/>
|
|
17623
|
+
</w:lvlOverride>
|
|
17624
|
+
</w:num>
|
|
17625
|
+
</w:numbering>`.trim();
|
|
17573
17626
|
}
|
|
17574
|
-
function
|
|
17575
|
-
|
|
17627
|
+
function isTrackedDeletedParagraph(node) {
|
|
17628
|
+
if (!node || node.nodeType !== 1 || node.localName !== "p") return false;
|
|
17629
|
+
const pPr = Array.from(node.childNodes || []).find((child) => isDirectWordChild(child, "pPr"));
|
|
17630
|
+
const rPr = pPr && Array.from(pPr.childNodes || []).find((child) => isDirectWordChild(child, "rPr"));
|
|
17631
|
+
return !!rPr && Array.from(rPr.childNodes || []).some((child) => isDirectWordChild(child, "del"));
|
|
17576
17632
|
}
|
|
17577
|
-
function
|
|
17578
|
-
|
|
17579
|
-
const matrix = rowElements.map((row) => {
|
|
17580
|
-
const cellElements = getDirectWordChildren(row, "tc");
|
|
17581
|
-
return cellElements.map((cell) => {
|
|
17582
|
-
const paragraphs = getDirectWordChildren(cell, "p");
|
|
17583
|
-
if (paragraphs.length === 0) return normalizeWhitespaceForTargeting(getParagraphText(cell));
|
|
17584
|
-
const lines = paragraphs.map((p) => normalizeWhitespaceForTargeting(getParagraphText(p))).filter(Boolean);
|
|
17585
|
-
return lines.join("\n");
|
|
17586
|
-
});
|
|
17587
|
-
});
|
|
17588
|
-
const columnCount = matrix.reduce((max2, row) => Math.max(max2, row.length), 0);
|
|
17589
|
-
matrix.forEach((row) => {
|
|
17590
|
-
while (row.length < columnCount) row.push("");
|
|
17591
|
-
});
|
|
17592
|
-
return { matrix, rowElements, columnCount };
|
|
17633
|
+
function paragraphNumIdNodes(node) {
|
|
17634
|
+
return Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []).filter((numIdNode) => numIdNode.parentNode?.localName === "numPr");
|
|
17593
17635
|
}
|
|
17594
|
-
function
|
|
17595
|
-
|
|
17596
|
-
const
|
|
17597
|
-
const
|
|
17598
|
-
|
|
17599
|
-
|
|
17600
|
-
}
|
|
17601
|
-
|
|
17602
|
-
const separator = new Array(columnCount).fill("---");
|
|
17603
|
-
const bodyRows = normalized.slice(1);
|
|
17604
|
-
const toLine = (row) => `| ${row.map((cell) => escapeMarkdownCell(cell)).join(" | ")} |`;
|
|
17605
|
-
return [toLine(header), toLine(separator), ...bodyRows.map(toLine)].join("\n");
|
|
17636
|
+
function levelFormats(abstractNum) {
|
|
17637
|
+
const formats = /* @__PURE__ */ new Map();
|
|
17638
|
+
for (const lvl of Array.from(abstractNum?.getElementsByTagNameNS?.("*", "lvl") || [])) {
|
|
17639
|
+
const ilvl = getElementId2(lvl, ["w:ilvl", "ilvl"]);
|
|
17640
|
+
const fmt = Array.from(lvl.getElementsByTagNameNS("*", "numFmt"))[0]?.getAttribute("w:val") || null;
|
|
17641
|
+
if (ilvl != null && fmt) formats.set(ilvl, fmt);
|
|
17642
|
+
}
|
|
17643
|
+
return formats;
|
|
17606
17644
|
}
|
|
17607
|
-
function
|
|
17608
|
-
|
|
17609
|
-
const normalized = rowValues.map((value) => normalizeWhitespaceForTargeting(value)).filter(Boolean);
|
|
17610
|
-
if (normalized.length < 2) return false;
|
|
17611
|
-
return normalized.every((value) => value === normalized[0]);
|
|
17645
|
+
function normalizeListFormat(format) {
|
|
17646
|
+
return format === "bullet" || format === "circle" || format === "square" ? "bullet" : format;
|
|
17612
17647
|
}
|
|
17613
|
-
function
|
|
17614
|
-
const
|
|
17615
|
-
|
|
17616
|
-
if (
|
|
17617
|
-
|
|
17618
|
-
if (/^\(.*\)$/.test(normalized)) return true;
|
|
17619
|
-
if (/:\s*$/.test(normalized)) return true;
|
|
17620
|
-
if (normalized.length <= 90 && !/[.!?]$/.test(normalized) && /[:\[\]()]/.test(normalized)) return true;
|
|
17621
|
-
if (/^[\[(]/.test(normalized)) return true;
|
|
17622
|
-
return false;
|
|
17648
|
+
function abstractNumForNumId(numberingDoc, numId) {
|
|
17649
|
+
const num = Array.from(numberingDoc.getElementsByTagNameNS("*", "num")).find((candidate) => getElementId2(candidate, ["w:numId", "numId"]) === numId);
|
|
17650
|
+
const abstractId = num && getElementId2(Array.from(num.getElementsByTagNameNS("*", "abstractNumId"))[0], ["w:val", "val"]);
|
|
17651
|
+
if (abstractId == null) return null;
|
|
17652
|
+
return Array.from(numberingDoc.getElementsByTagNameNS("*", "abstractNum")).find((candidate) => getElementId2(candidate, ["w:abstractNumId", "abstractNumId"]) === abstractId) || null;
|
|
17623
17653
|
}
|
|
17624
|
-
function
|
|
17625
|
-
const
|
|
17626
|
-
|
|
17627
|
-
|
|
17628
|
-
const
|
|
17629
|
-
|
|
17630
|
-
|
|
17631
|
-
|
|
17632
|
-
scanned += 1;
|
|
17633
|
-
const nextCursor = cursor.nextSibling;
|
|
17634
|
-
if (cursor.nodeType !== 1 || cursor.namespaceURI !== WORD_MAIN_NS || cursor.localName !== "p") {
|
|
17635
|
-
cursor = nextCursor;
|
|
17636
|
-
continue;
|
|
17654
|
+
function reuseSourceListNumbering(numberingXml, replacementNodes, sourceParagraphs, numberingIdState) {
|
|
17655
|
+
const sourceXml = numberingIdState?.sourceNumberingXml;
|
|
17656
|
+
if (!numberingXml || !String(sourceXml || "").trim() || !Array.isArray(replacementNodes)) return null;
|
|
17657
|
+
const sourceNumIds = /* @__PURE__ */ new Set();
|
|
17658
|
+
for (const paragraph of sourceParagraphs || []) {
|
|
17659
|
+
for (const numIdNode of paragraphNumIdNodes(paragraph)) {
|
|
17660
|
+
const id = getElementId2(numIdNode, ["w:val", "val"]);
|
|
17661
|
+
if (id != null && id !== 0) sourceNumIds.add(id);
|
|
17637
17662
|
}
|
|
17638
|
-
|
|
17639
|
-
|
|
17640
|
-
|
|
17641
|
-
|
|
17642
|
-
|
|
17663
|
+
}
|
|
17664
|
+
if (sourceNumIds.size !== 1) return null;
|
|
17665
|
+
const [sourceNumId] = sourceNumIds;
|
|
17666
|
+
const sourceDoc = parseOoxml(sourceXml);
|
|
17667
|
+
const payloadDoc = parseOoxml(numberingXml);
|
|
17668
|
+
if (hasXmlParseError(sourceDoc) || hasXmlParseError(payloadDoc)) return null;
|
|
17669
|
+
const sourceAbstract = abstractNumForNumId(sourceDoc, sourceNumId);
|
|
17670
|
+
if (!sourceAbstract) return null;
|
|
17671
|
+
const sourceFormats = levelFormats(sourceAbstract);
|
|
17672
|
+
const newParagraphNumIds = [];
|
|
17673
|
+
for (const node of replacementNodes) {
|
|
17674
|
+
if (isTrackedDeletedParagraph(node)) continue;
|
|
17675
|
+
for (const numIdNode of paragraphNumIdNodes(node)) {
|
|
17676
|
+
const ilvlNode = Array.from(numIdNode.parentNode.childNodes || []).find((child) => child.localName === "ilvl");
|
|
17677
|
+
newParagraphNumIds.push({
|
|
17678
|
+
numId: getElementId2(numIdNode, ["w:val", "val"]),
|
|
17679
|
+
ilvl: ilvlNode ? getElementId2(ilvlNode, ["w:val", "val"]) ?? 0 : 0
|
|
17680
|
+
});
|
|
17643
17681
|
}
|
|
17644
|
-
if (!isLikelyStructuredTableSourceParagraph(text2)) break;
|
|
17645
|
-
block.push(cursor);
|
|
17646
|
-
cursor = nextCursor;
|
|
17647
17682
|
}
|
|
17648
|
-
|
|
17683
|
+
if (newParagraphNumIds.length === 0) return null;
|
|
17684
|
+
const payloadFormats = /* @__PURE__ */ new Map();
|
|
17685
|
+
for (const { numId, ilvl } of newParagraphNumIds) {
|
|
17686
|
+
if (numId == null) return null;
|
|
17687
|
+
if (!payloadFormats.has(numId)) {
|
|
17688
|
+
const abstract = abstractNumForNumId(payloadDoc, numId);
|
|
17689
|
+
if (!abstract) return null;
|
|
17690
|
+
payloadFormats.set(numId, levelFormats(abstract));
|
|
17691
|
+
}
|
|
17692
|
+
const generated = payloadFormats.get(numId).get(ilvl);
|
|
17693
|
+
const existing = sourceFormats.get(ilvl);
|
|
17694
|
+
if (!generated || !existing || normalizeListFormat(generated) !== normalizeListFormat(existing)) return null;
|
|
17695
|
+
}
|
|
17696
|
+
const clonedNodes = replacementNodes.map((node) => node?.cloneNode ? node.cloneNode(true) : node);
|
|
17697
|
+
for (const node of clonedNodes) {
|
|
17698
|
+
if (isTrackedDeletedParagraph(node)) continue;
|
|
17699
|
+
for (const numIdNode of paragraphNumIdNodes(node)) setElementVal2(numIdNode, sourceNumId);
|
|
17700
|
+
}
|
|
17701
|
+
return { numId: sourceNumId, replacementNodes: clonedNodes };
|
|
17649
17702
|
}
|
|
17650
|
-
function
|
|
17651
|
-
const
|
|
17652
|
-
|
|
17653
|
-
|
|
17654
|
-
|
|
17655
|
-
|
|
17656
|
-
|
|
17657
|
-
if (isMarkdownTableText(rawModified)) return null;
|
|
17658
|
-
const lines = rawModified.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean);
|
|
17659
|
-
if (lines.length < 2) return null;
|
|
17660
|
-
const tableElement = options.tableElement || findContainingWordElement(targetParagraph, "tbl");
|
|
17661
|
-
const rowElement = findContainingWordElement(targetParagraph, "tr");
|
|
17662
|
-
const cellElement = findContainingWordElement(targetParagraph, "tc");
|
|
17663
|
-
if (!tableElement || !rowElement || !cellElement) return null;
|
|
17664
|
-
const currentParagraphText = normalizeWhitespaceForTargeting(
|
|
17665
|
-
options.currentParagraphText || getParagraphText(targetParagraph)
|
|
17666
|
-
);
|
|
17667
|
-
const firstLine = normalizeWhitespaceForTargeting(lines[0]);
|
|
17668
|
-
if (currentParagraphText && firstLine && firstLine !== currentParagraphText) {
|
|
17669
|
-
onWarn("[Table] Multiline cell text did not anchor to original cell text; skipping table-row synthesis heuristic.");
|
|
17670
|
-
return null;
|
|
17703
|
+
function remapNumberingPayloadForDocument(numberingXml, replacementNodes, numberingIdState) {
|
|
17704
|
+
const numberingDoc = parseOoxml(numberingXml);
|
|
17705
|
+
if (hasXmlParseError(numberingDoc)) {
|
|
17706
|
+
return {
|
|
17707
|
+
numberingXml: String(numberingXml || ""),
|
|
17708
|
+
replacementNodes: Array.isArray(replacementNodes) ? replacementNodes.map((node) => node?.cloneNode ? node.cloneNode(true) : node) : []
|
|
17709
|
+
};
|
|
17671
17710
|
}
|
|
17672
|
-
const
|
|
17673
|
-
|
|
17674
|
-
const
|
|
17675
|
-
const
|
|
17676
|
-
|
|
17677
|
-
|
|
17678
|
-
|
|
17679
|
-
|
|
17680
|
-
|
|
17681
|
-
|
|
17711
|
+
const abstractNumMap = /* @__PURE__ */ new Map();
|
|
17712
|
+
const numIdMap = /* @__PURE__ */ new Map();
|
|
17713
|
+
const abstractNums = Array.from(numberingDoc.getElementsByTagNameNS("*", "abstractNum"));
|
|
17714
|
+
for (const abstractNum of abstractNums) {
|
|
17715
|
+
const oldId = getElementId2(abstractNum, ["w:abstractNumId", "abstractNumId"]);
|
|
17716
|
+
if (oldId == null) continue;
|
|
17717
|
+
const newId = reserveNextNumberingId(numberingIdState, "abstract");
|
|
17718
|
+
if (newId == null) continue;
|
|
17719
|
+
abstractNumMap.set(oldId, newId);
|
|
17720
|
+
setElementId(abstractNum, "w:abstractNumId", newId);
|
|
17682
17721
|
}
|
|
17683
|
-
|
|
17684
|
-
|
|
17685
|
-
const
|
|
17686
|
-
if (
|
|
17687
|
-
|
|
17688
|
-
|
|
17689
|
-
|
|
17690
|
-
|
|
17691
|
-
|
|
17692
|
-
|
|
17693
|
-
|
|
17694
|
-
|
|
17695
|
-
|
|
17696
|
-
} else {
|
|
17697
|
-
const newRow = new Array(columnCount).fill("");
|
|
17698
|
-
if (mirrorAcrossColumns) {
|
|
17699
|
-
for (let col = 0; col < columnCount; col++) newRow[col] = extraValue;
|
|
17700
|
-
} else {
|
|
17701
|
-
newRow[colIndex] = extraValue;
|
|
17722
|
+
const nums = Array.from(numberingDoc.getElementsByTagNameNS("*", "num"));
|
|
17723
|
+
for (const num of nums) {
|
|
17724
|
+
const oldNumId = getElementId2(num, ["w:numId", "numId"]);
|
|
17725
|
+
if (oldNumId == null) continue;
|
|
17726
|
+
const newNumId = reserveNextNumberingId(numberingIdState, "num");
|
|
17727
|
+
if (newNumId == null) continue;
|
|
17728
|
+
numIdMap.set(oldNumId, newNumId);
|
|
17729
|
+
setElementId(num, "w:numId", newNumId);
|
|
17730
|
+
const abstractNumIdNode = Array.from(num.getElementsByTagNameNS("*", "abstractNumId"))[0] || null;
|
|
17731
|
+
if (abstractNumIdNode) {
|
|
17732
|
+
const oldAbsRef = getElementId2(abstractNumIdNode, ["w:val", "val"]);
|
|
17733
|
+
if (oldAbsRef != null && abstractNumMap.has(oldAbsRef)) {
|
|
17734
|
+
setElementVal2(abstractNumIdNode, abstractNumMap.get(oldAbsRef));
|
|
17702
17735
|
}
|
|
17703
|
-
matrix.splice(Math.min(insertIndex, matrix.length), 0, newRow);
|
|
17704
17736
|
}
|
|
17705
17737
|
}
|
|
17706
|
-
const
|
|
17707
|
-
|
|
17708
|
-
|
|
17709
|
-
|
|
17710
|
-
|
|
17711
|
-
|
|
17712
|
-
|
|
17713
|
-
|
|
17714
|
-
|
|
17715
|
-
|
|
17716
|
-
const raw = element.getAttribute(name);
|
|
17717
|
-
if (raw == null || raw === "") continue;
|
|
17718
|
-
const parsed = Number.parseInt(String(raw), 10);
|
|
17719
|
-
if (Number.isFinite(parsed)) return parsed;
|
|
17738
|
+
const clonedNodes = Array.isArray(replacementNodes) ? replacementNodes.map((node) => node?.cloneNode ? node.cloneNode(true) : node) : [];
|
|
17739
|
+
for (const node of clonedNodes) {
|
|
17740
|
+
if (isTrackedDeletedParagraph(node)) continue;
|
|
17741
|
+
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []);
|
|
17742
|
+
for (const numIdNode of numIdNodes) {
|
|
17743
|
+
const oldNumRef = getElementId2(numIdNode, ["w:val", "val"]);
|
|
17744
|
+
if (oldNumRef != null && numIdMap.has(oldNumRef)) {
|
|
17745
|
+
setElementVal2(numIdNode, numIdMap.get(oldNumRef));
|
|
17746
|
+
}
|
|
17747
|
+
}
|
|
17720
17748
|
}
|
|
17721
|
-
return
|
|
17749
|
+
return {
|
|
17750
|
+
numberingXml: serializeOoxml(numberingDoc),
|
|
17751
|
+
replacementNodes: clonedNodes
|
|
17752
|
+
};
|
|
17722
17753
|
}
|
|
17723
|
-
function
|
|
17724
|
-
|
|
17725
|
-
const
|
|
17726
|
-
|
|
17727
|
-
|
|
17728
|
-
|
|
17729
|
-
|
|
17730
|
-
|
|
17731
|
-
|
|
17754
|
+
function mergeNumberingXmlBySchemaOrder(existingNumberingXml, incomingNumberingXml) {
|
|
17755
|
+
const existingText = String(existingNumberingXml || "");
|
|
17756
|
+
const incomingText = String(incomingNumberingXml || "");
|
|
17757
|
+
if (!incomingText.trim()) return existingText;
|
|
17758
|
+
if (!existingText.trim()) return incomingText;
|
|
17759
|
+
try {
|
|
17760
|
+
const existingDoc = parseOoxml(existingText);
|
|
17761
|
+
const incomingDoc = parseOoxml(incomingText);
|
|
17762
|
+
if (hasXmlParseError(existingDoc) || hasXmlParseError(incomingDoc)) {
|
|
17763
|
+
return existingText;
|
|
17732
17764
|
}
|
|
17765
|
+
const existingRoot = existingDoc.documentElement;
|
|
17766
|
+
const incomingRoot = incomingDoc.documentElement;
|
|
17767
|
+
if (!existingRoot || !incomingRoot) return existingText;
|
|
17768
|
+
const existingAbstractIds = new Set(
|
|
17769
|
+
Array.from(existingRoot.childNodes || []).filter((node) => isDirectWordChild(node, "abstractNum")).map((node) => parseIntegerAttribute(node, ["w:abstractNumId", "abstractNumId"])).filter((id) => id != null)
|
|
17770
|
+
);
|
|
17771
|
+
const existingNumIds = new Set(
|
|
17772
|
+
Array.from(existingRoot.childNodes || []).filter((node) => isDirectWordChild(node, "num")).map((node) => parseIntegerAttribute(node, ["w:numId", "numId"])).filter((id) => id != null)
|
|
17773
|
+
);
|
|
17774
|
+
for (const incomingNode of Array.from(incomingRoot.childNodes || [])) {
|
|
17775
|
+
if (!isDirectWordChild(incomingNode, "abstractNum")) continue;
|
|
17776
|
+
const incomingId = parseIntegerAttribute(incomingNode, ["w:abstractNumId", "abstractNumId"]);
|
|
17777
|
+
if (incomingId == null || existingAbstractIds.has(incomingId)) continue;
|
|
17778
|
+
insertNumberingNodeInSchemaOrder(existingRoot, existingDoc.importNode(incomingNode, true), "abstract");
|
|
17779
|
+
existingAbstractIds.add(incomingId);
|
|
17780
|
+
}
|
|
17781
|
+
for (const incomingNode of Array.from(incomingRoot.childNodes || [])) {
|
|
17782
|
+
if (!isDirectWordChild(incomingNode, "num")) continue;
|
|
17783
|
+
const incomingId = parseIntegerAttribute(incomingNode, ["w:numId", "numId"]);
|
|
17784
|
+
if (incomingId == null || existingNumIds.has(incomingId)) continue;
|
|
17785
|
+
insertNumberingNodeInSchemaOrder(existingRoot, existingDoc.importNode(incomingNode, true), "num");
|
|
17786
|
+
existingNumIds.add(incomingId);
|
|
17787
|
+
}
|
|
17788
|
+
return serializeOoxml(existingDoc);
|
|
17789
|
+
} catch {
|
|
17790
|
+
return existingText;
|
|
17733
17791
|
}
|
|
17734
|
-
return candidate;
|
|
17735
17792
|
}
|
|
17736
|
-
|
|
17737
|
-
|
|
17738
|
-
|
|
17739
|
-
|
|
17740
|
-
|
|
17741
|
-
|
|
17793
|
+
|
|
17794
|
+
// core/redline-validation.js
|
|
17795
|
+
var REVISION_ID_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "rPrChange", "pPrChange"]);
|
|
17796
|
+
var REVISION_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}T/;
|
|
17797
|
+
function localNameOf3(node) {
|
|
17798
|
+
return String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
|
|
17799
|
+
}
|
|
17800
|
+
function elementsByLocalName(root, name) {
|
|
17801
|
+
return Array.from(root.getElementsByTagName("*")).filter((el) => localNameOf3(el) === name);
|
|
17802
|
+
}
|
|
17803
|
+
function wordAttribute3(node, name) {
|
|
17804
|
+
return node.getAttribute(`w:${name}`) || node.getAttribute(name) || "";
|
|
17805
|
+
}
|
|
17806
|
+
function xmlSpaceAttribute(node) {
|
|
17807
|
+
return node.getAttribute("xml:space") || node.getAttribute("space") || node.getAttributeNS?.("http://www.w3.org/XML/1998/namespace", "space") || "";
|
|
17808
|
+
}
|
|
17809
|
+
function isParagraphMarkRevision(node) {
|
|
17810
|
+
return localNameOf3(node.parentNode) === "rPr";
|
|
17811
|
+
}
|
|
17812
|
+
function parseOoxmlForValidation(oxml) {
|
|
17813
|
+
const attempt = (xml) => {
|
|
17814
|
+
const doc = parseXml(xml);
|
|
17815
|
+
const parseError = doc.getElementsByTagName("parsererror")[0];
|
|
17816
|
+
if (parseError) {
|
|
17817
|
+
throw new Error(parseError.textContent || "XML parse error");
|
|
17818
|
+
}
|
|
17819
|
+
return doc;
|
|
17820
|
+
};
|
|
17821
|
+
try {
|
|
17822
|
+
return { doc: attempt(oxml) };
|
|
17823
|
+
} catch {
|
|
17742
17824
|
try {
|
|
17743
|
-
|
|
17744
|
-
|
|
17745
|
-
|
|
17746
|
-
for (const abstractNum of abstractNums) {
|
|
17747
|
-
const id = parseIntegerAttribute(abstractNum, ["w:abstractNumId", "abstractNumId"]);
|
|
17748
|
-
if (id != null) usedAbstractNumIds.add(id);
|
|
17749
|
-
}
|
|
17750
|
-
for (const num of nums) {
|
|
17751
|
-
const id = parseIntegerAttribute(num, ["w:numId", "numId"]);
|
|
17752
|
-
if (id != null) usedNumIds.add(id);
|
|
17753
|
-
}
|
|
17754
|
-
} catch {
|
|
17825
|
+
return { doc: attempt(`<w:root xmlns:w="${NS_W}">${oxml}</w:root>`) };
|
|
17826
|
+
} catch (error2) {
|
|
17827
|
+
return { error: error2?.message || "XML parse error" };
|
|
17755
17828
|
}
|
|
17756
17829
|
}
|
|
17757
|
-
const maxUsedNumId = usedNumIds.size > 0 ? Math.max(...usedNumIds) : 0;
|
|
17758
|
-
const maxUsedAbstractNumId = usedAbstractNumIds.size > 0 ? Math.max(...usedAbstractNumIds) : 0;
|
|
17759
|
-
const baseNumId = Math.max(minId, maxUsedNumId + 1);
|
|
17760
|
-
const baseAbstractNumId = Math.max(minId, maxUsedAbstractNumId + 1);
|
|
17761
|
-
return {
|
|
17762
|
-
// Kept so list edits can recognise which existing definition a source list uses and continue it.
|
|
17763
|
-
sourceNumberingXml: String(numberingXml || ""),
|
|
17764
|
-
nextNumId: nextAvailableId(baseNumId, usedNumIds, maxPreferred),
|
|
17765
|
-
nextAbstractNumId: nextAvailableId(baseAbstractNumId, usedAbstractNumIds, maxPreferred),
|
|
17766
|
-
usedNumIds,
|
|
17767
|
-
usedAbstractNumIds,
|
|
17768
|
-
minId,
|
|
17769
|
-
maxPreferred
|
|
17770
|
-
};
|
|
17771
17830
|
}
|
|
17772
|
-
function
|
|
17773
|
-
|
|
17774
|
-
|
|
17775
|
-
|
|
17776
|
-
if (!
|
|
17777
|
-
|
|
17831
|
+
function validateRedlineOoxml(oxml) {
|
|
17832
|
+
const issues = [];
|
|
17833
|
+
const addIssue = (code, severity, message) => issues.push({ code, severity, message });
|
|
17834
|
+
const isDomNode = oxml && typeof oxml === "object" && (oxml.nodeType === 9 || oxml.nodeType === 1);
|
|
17835
|
+
if (!isDomNode && (typeof oxml !== "string" || oxml.trim() === "")) {
|
|
17836
|
+
addIssue("PARSE_ERROR", "error", "Input is not non-empty OOXML or a parsed XML DOM.");
|
|
17837
|
+
return { valid: false, issues };
|
|
17778
17838
|
}
|
|
17779
|
-
|
|
17780
|
-
|
|
17839
|
+
const { doc, error: error2 } = isDomNode ? { doc: oxml.nodeType === 9 ? oxml : oxml.ownerDocument } : parseOoxmlForValidation(oxml);
|
|
17840
|
+
if (!doc) {
|
|
17841
|
+
addIssue("PARSE_ERROR", "error", `OOXML does not parse as XML: ${error2}`);
|
|
17842
|
+
return { valid: false, issues };
|
|
17781
17843
|
}
|
|
17782
|
-
|
|
17783
|
-
|
|
17844
|
+
const insElements = elementsByLocalName(doc, "ins");
|
|
17845
|
+
const delElements = elementsByLocalName(doc, "del");
|
|
17846
|
+
const revisions = insElements.concat(delElements);
|
|
17847
|
+
for (const paragraph of elementsByLocalName(doc, "p")) {
|
|
17848
|
+
const nested = Array.from(paragraph.getElementsByTagName("*")).find((el) => el !== paragraph && localNameOf3(el) === "p");
|
|
17849
|
+
if (nested) {
|
|
17850
|
+
addIssue("NESTED_PARAGRAPH", "error", `<${paragraph.nodeName}> contains nested <${nested.nodeName}>.`);
|
|
17851
|
+
}
|
|
17784
17852
|
}
|
|
17785
|
-
|
|
17786
|
-
|
|
17853
|
+
for (const body of elementsByLocalName(doc, "body")) {
|
|
17854
|
+
const children = Array.from(body.childNodes || []).filter((child) => child.nodeType === 1);
|
|
17855
|
+
const sectPrIndexes = children.map((child, index) => localNameOf3(child) === "sectPr" ? index : -1).filter((index) => index >= 0);
|
|
17856
|
+
if (sectPrIndexes.length > 1) {
|
|
17857
|
+
addIssue("MULTIPLE_BODY_SECTPR", "error", "<w:body> contains multiple direct <w:sectPr> elements.");
|
|
17858
|
+
} else if (sectPrIndexes.length === 1 && sectPrIndexes[0] !== children.length - 1) {
|
|
17859
|
+
addIssue("SECTPR_NOT_LAST", "error", "<w:sectPr> is not the last element child of <w:body>.");
|
|
17860
|
+
}
|
|
17787
17861
|
}
|
|
17788
|
-
|
|
17789
|
-
|
|
17790
|
-
|
|
17791
|
-
|
|
17792
|
-
|
|
17793
|
-
|
|
17794
|
-
|
|
17795
|
-
|
|
17796
|
-
|
|
17797
|
-
|
|
17798
|
-
|
|
17799
|
-
|
|
17800
|
-
|
|
17801
|
-
} else {
|
|
17802
|
-
normalized.usedNumIds.add(id);
|
|
17803
|
-
normalized.nextNumId = nextAvailableId(id + 1, normalized.usedNumIds, normalized.maxPreferred);
|
|
17862
|
+
for (const revision of revisions) {
|
|
17863
|
+
const nested = Array.from(revision.getElementsByTagName("*")).filter((el) => el !== revision && ["ins", "del"].includes(localNameOf3(el)));
|
|
17864
|
+
const invalidNested = nested.find((candidate) => {
|
|
17865
|
+
if (localNameOf3(revision) !== "ins" || localNameOf3(candidate) !== "del") return true;
|
|
17866
|
+
return candidate.parentNode !== revision;
|
|
17867
|
+
});
|
|
17868
|
+
if (invalidNested) {
|
|
17869
|
+
addIssue(
|
|
17870
|
+
"NESTED_REVISION",
|
|
17871
|
+
"error",
|
|
17872
|
+
`<${revision.nodeName}> (w:id="${wordAttribute3(revision, "id")}") contains invalid nested <${invalidNested.nodeName}>.`
|
|
17873
|
+
);
|
|
17874
|
+
}
|
|
17804
17875
|
}
|
|
17805
|
-
|
|
17806
|
-
|
|
17807
|
-
|
|
17808
|
-
|
|
17809
|
-
|
|
17810
|
-
|
|
17811
|
-
|
|
17812
|
-
|
|
17813
|
-
|
|
17814
|
-
if (!doc || !doc.documentElement) return true;
|
|
17815
|
-
if (doc.documentElement.localName === "parsererror") return true;
|
|
17816
|
-
return doc.getElementsByTagName("parsererror").length > 0;
|
|
17817
|
-
}
|
|
17818
|
-
function isDirectWordChild(node, localName2) {
|
|
17819
|
-
return !!(node && node.nodeType === 1 && node.namespaceURI === WORD_MAIN_NS && node.localName === localName2);
|
|
17820
|
-
}
|
|
17821
|
-
function insertNumberingNodeInSchemaOrder(root, node, kind) {
|
|
17822
|
-
if (!root || !node) return;
|
|
17823
|
-
const directChildren = Array.from(root.childNodes || []).filter(
|
|
17824
|
-
(child) => child && child.nodeType === 1 && child.namespaceURI === WORD_MAIN_NS
|
|
17825
|
-
);
|
|
17826
|
-
let anchor = null;
|
|
17827
|
-
if (kind === "abstract") {
|
|
17828
|
-
anchor = directChildren.find(
|
|
17829
|
-
(child) => child.localName === "num" || child.localName === "numIdMacAtCleanup"
|
|
17830
|
-
) || null;
|
|
17831
|
-
} else {
|
|
17832
|
-
anchor = directChildren.find((child) => child.localName === "numIdMacAtCleanup") || null;
|
|
17876
|
+
for (const del of delElements) {
|
|
17877
|
+
const plainTextNodes = elementsByLocalName(del, "t");
|
|
17878
|
+
if (plainTextNodes.length > 0) {
|
|
17879
|
+
addIssue(
|
|
17880
|
+
"DEL_CONTAINS_T",
|
|
17881
|
+
"error",
|
|
17882
|
+
`<w:del> (w:id="${wordAttribute3(del, "id")}") contains <w:t>; deleted text must use <w:delText>.`
|
|
17883
|
+
);
|
|
17884
|
+
}
|
|
17833
17885
|
}
|
|
17834
|
-
|
|
17835
|
-
|
|
17836
|
-
|
|
17837
|
-
|
|
17838
|
-
|
|
17839
|
-
|
|
17840
|
-
|
|
17886
|
+
for (const revision of revisions) {
|
|
17887
|
+
const missing = [];
|
|
17888
|
+
if (!wordAttribute3(revision, "id")) missing.push("w:id");
|
|
17889
|
+
if (!wordAttribute3(revision, "author")) missing.push("w:author");
|
|
17890
|
+
if (!REVISION_DATE_PATTERN.test(wordAttribute3(revision, "date"))) missing.push("w:date");
|
|
17891
|
+
if (missing.length > 0) {
|
|
17892
|
+
addIssue(
|
|
17893
|
+
"MISSING_REVISION_METADATA",
|
|
17894
|
+
"error",
|
|
17895
|
+
`<${revision.nodeName}> is missing or has malformed ${missing.join(", ")}.`
|
|
17896
|
+
);
|
|
17897
|
+
}
|
|
17841
17898
|
}
|
|
17842
|
-
|
|
17843
|
-
|
|
17844
|
-
|
|
17845
|
-
|
|
17846
|
-
|
|
17847
|
-
|
|
17848
|
-
}
|
|
17849
|
-
function setElementId(element, preferredName, idValue) {
|
|
17850
|
-
element?.setAttribute?.(preferredName, String(idValue));
|
|
17851
|
-
}
|
|
17852
|
-
function setElementVal2(element, value) {
|
|
17853
|
-
element?.setAttribute?.("w:val", String(value));
|
|
17854
|
-
}
|
|
17855
|
-
function overwriteParagraphNumIds(paragraphNodes, targetNumId) {
|
|
17856
|
-
if (!Array.isArray(paragraphNodes) || targetNumId == null) return;
|
|
17857
|
-
for (const node of paragraphNodes) {
|
|
17858
|
-
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []);
|
|
17859
|
-
for (const numIdNode of numIdNodes) {
|
|
17860
|
-
setElementVal2(numIdNode, targetNumId);
|
|
17899
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
17900
|
+
for (const node of Array.from(doc.getElementsByTagName("*"))) {
|
|
17901
|
+
if (!REVISION_ID_ELEMENTS.has(localNameOf3(node))) continue;
|
|
17902
|
+
const id = wordAttribute3(node, "id");
|
|
17903
|
+
if (!id) continue;
|
|
17904
|
+
if (seenIds.has(id)) {
|
|
17905
|
+
addIssue("DUPLICATE_REVISION_ID", "error", `Revision id ${id} appears more than once.`);
|
|
17861
17906
|
}
|
|
17907
|
+
seenIds.add(id);
|
|
17862
17908
|
}
|
|
17863
|
-
|
|
17864
|
-
|
|
17865
|
-
|
|
17866
|
-
|
|
17867
|
-
|
|
17868
|
-
|
|
17869
|
-
|
|
17909
|
+
const textNodes = elementsByLocalName(doc, "t").concat(elementsByLocalName(doc, "delText"));
|
|
17910
|
+
for (const node of textNodes) {
|
|
17911
|
+
const text2 = node.textContent || "";
|
|
17912
|
+
if (/^\s|\s$/.test(text2) && xmlSpaceAttribute(node) !== "preserve") {
|
|
17913
|
+
addIssue(
|
|
17914
|
+
"MISSING_SPACE_PRESERVE",
|
|
17915
|
+
"error",
|
|
17916
|
+
`<${node.nodeName}> has boundary whitespace without xml:space="preserve".`
|
|
17917
|
+
);
|
|
17918
|
+
}
|
|
17919
|
+
if (text2 === "") {
|
|
17920
|
+
addIssue("EMPTY_TEXT_ELEMENT", "warning", `<${node.nodeName}> is empty.`);
|
|
17870
17921
|
}
|
|
17871
17922
|
}
|
|
17872
|
-
|
|
17873
|
-
|
|
17874
|
-
|
|
17875
|
-
|
|
17876
|
-
|
|
17877
|
-
|
|
17878
|
-
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
|
|
17882
|
-
<w:lvl w:ilvl="${level}">
|
|
17883
|
-
<w:start w:val="1"/>
|
|
17884
|
-
<w:numFmt w:val="decimal"/>
|
|
17885
|
-
<w:lvlText w:val="${lvlText}"/>
|
|
17886
|
-
<w:lvlJc w:val="left"/>
|
|
17887
|
-
<w:pPr><w:ind w:left="${left}" w:hanging="360"/></w:pPr>
|
|
17888
|
-
</w:lvl>`;
|
|
17889
|
-
}).join("");
|
|
17890
|
-
return `
|
|
17891
|
-
<w:numbering xmlns:w="${WORD_MAIN_NS}">
|
|
17892
|
-
<w:abstractNum w:abstractNumId="${safeAbstractNumId}">
|
|
17893
|
-
<w:multiLevelType w:val="multilevel"/>
|
|
17894
|
-
${levelsXml}
|
|
17895
|
-
</w:abstractNum>
|
|
17896
|
-
<w:num w:numId="${safeNumId}">
|
|
17897
|
-
<w:abstractNumId w:val="${safeAbstractNumId}"/>
|
|
17898
|
-
<w:lvlOverride w:ilvl="0">
|
|
17899
|
-
<w:startOverride w:val="${safeStartAt}"/>
|
|
17900
|
-
</w:lvlOverride>
|
|
17901
|
-
</w:num>
|
|
17902
|
-
</w:numbering>`.trim();
|
|
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);
|
|
17923
|
+
for (const revision of revisions) {
|
|
17924
|
+
if (isParagraphMarkRevision(revision)) continue;
|
|
17925
|
+
const hasElementChild = Array.from(revision.childNodes || []).some((child) => child.nodeType === 1);
|
|
17926
|
+
if (!hasElementChild) {
|
|
17927
|
+
addIssue(
|
|
17928
|
+
"EMPTY_REVISION_WRAPPER",
|
|
17929
|
+
"warning",
|
|
17930
|
+
`<${revision.nodeName}> (w:id="${wordAttribute3(revision, "id")}") wraps no content.`
|
|
17931
|
+
);
|
|
17932
|
+
}
|
|
17919
17933
|
}
|
|
17920
|
-
|
|
17934
|
+
for (const resurrection of findForeignDeletedParagraphResurrections(doc)) {
|
|
17935
|
+
const ownerAuthor = resurrection.ownerAuthor || "unattributed";
|
|
17936
|
+
addIssue(
|
|
17937
|
+
"FOREIGN_PARAGRAPH_MARK_DELETION",
|
|
17938
|
+
"warning",
|
|
17939
|
+
`Paragraph deleted by ${ownerAuthor} also contains non-empty insertion content from another author; Accept/Reject lifecycle may not preserve the apparent restoration.`
|
|
17940
|
+
);
|
|
17941
|
+
}
|
|
17942
|
+
return { valid: !issues.some((issue) => issue.severity === "error"), issues };
|
|
17921
17943
|
}
|
|
17922
|
-
|
|
17923
|
-
|
|
17944
|
+
|
|
17945
|
+
// core/table-targeting.js
|
|
17946
|
+
function getDirectWordChildren(element, localName2) {
|
|
17947
|
+
if (!element) return [];
|
|
17948
|
+
return Array.from(element.childNodes || []).filter(
|
|
17949
|
+
(node) => node && node.nodeType === 1 && node.namespaceURI === WORD_MAIN_NS && node.localName === localName2
|
|
17950
|
+
);
|
|
17924
17951
|
}
|
|
17925
|
-
function
|
|
17926
|
-
|
|
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;
|
|
17952
|
+
function escapeMarkdownCell(text2) {
|
|
17953
|
+
return String(text2 || "").replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
|
|
17930
17954
|
}
|
|
17931
|
-
function
|
|
17932
|
-
const
|
|
17933
|
-
|
|
17934
|
-
|
|
17935
|
-
|
|
17936
|
-
|
|
17937
|
-
|
|
17938
|
-
|
|
17939
|
-
|
|
17940
|
-
|
|
17941
|
-
|
|
17942
|
-
const
|
|
17943
|
-
|
|
17944
|
-
|
|
17945
|
-
|
|
17946
|
-
|
|
17947
|
-
|
|
17948
|
-
|
|
17949
|
-
|
|
17950
|
-
|
|
17951
|
-
|
|
17952
|
-
|
|
17953
|
-
|
|
17954
|
-
|
|
17955
|
-
|
|
17956
|
-
|
|
17957
|
-
|
|
17955
|
+
function extractTableMatrix(tableElement) {
|
|
17956
|
+
const rowElements = getDirectWordChildren(tableElement, "tr");
|
|
17957
|
+
const matrix = rowElements.map((row) => {
|
|
17958
|
+
const cellElements = getDirectWordChildren(row, "tc");
|
|
17959
|
+
return cellElements.map((cell) => {
|
|
17960
|
+
const paragraphs = getDirectWordChildren(cell, "p");
|
|
17961
|
+
if (paragraphs.length === 0) return normalizeWhitespaceForTargeting(getParagraphText(cell));
|
|
17962
|
+
const lines = paragraphs.map((p) => normalizeWhitespaceForTargeting(getParagraphText(p))).filter(Boolean);
|
|
17963
|
+
return lines.join("\n");
|
|
17964
|
+
});
|
|
17965
|
+
});
|
|
17966
|
+
const columnCount = matrix.reduce((max2, row) => Math.max(max2, row.length), 0);
|
|
17967
|
+
matrix.forEach((row) => {
|
|
17968
|
+
while (row.length < columnCount) row.push("");
|
|
17969
|
+
});
|
|
17970
|
+
return { matrix, rowElements, columnCount };
|
|
17971
|
+
}
|
|
17972
|
+
function tableMatrixToMarkdown(matrix, columnCount) {
|
|
17973
|
+
if (!Array.isArray(matrix) || matrix.length === 0 || columnCount <= 0) return null;
|
|
17974
|
+
const normalized = matrix.map((row) => {
|
|
17975
|
+
const copy = Array.isArray(row) ? row.slice(0, columnCount) : [];
|
|
17976
|
+
while (copy.length < columnCount) copy.push("");
|
|
17977
|
+
return copy;
|
|
17978
|
+
});
|
|
17979
|
+
const header = normalized[0];
|
|
17980
|
+
const separator = new Array(columnCount).fill("---");
|
|
17981
|
+
const bodyRows = normalized.slice(1);
|
|
17982
|
+
const toLine = (row) => `| ${row.map((cell) => escapeMarkdownCell(cell)).join(" | ")} |`;
|
|
17983
|
+
return [toLine(header), toLine(separator), ...bodyRows.map(toLine)].join("\n");
|
|
17984
|
+
}
|
|
17985
|
+
function isSymmetricLabelRow(rowValues) {
|
|
17986
|
+
if (!Array.isArray(rowValues) || rowValues.length < 2) return false;
|
|
17987
|
+
const normalized = rowValues.map((value) => normalizeWhitespaceForTargeting(value)).filter(Boolean);
|
|
17988
|
+
if (normalized.length < 2) return false;
|
|
17989
|
+
return normalized.every((value) => value === normalized[0]);
|
|
17990
|
+
}
|
|
17991
|
+
function isLikelyStructuredTableSourceParagraph(text2) {
|
|
17992
|
+
const normalized = String(text2 || "").trim();
|
|
17993
|
+
if (!normalized) return false;
|
|
17994
|
+
if (/^and$/i.test(normalized)) return true;
|
|
17995
|
+
if (/^\[.*\]$/.test(normalized)) return true;
|
|
17996
|
+
if (/^\(.*\)$/.test(normalized)) return true;
|
|
17997
|
+
if (/:\s*$/.test(normalized)) return true;
|
|
17998
|
+
if (normalized.length <= 90 && !/[.!?]$/.test(normalized) && /[:\[\]()]/.test(normalized)) return true;
|
|
17999
|
+
if (/^[\[(]/.test(normalized)) return true;
|
|
18000
|
+
return false;
|
|
18001
|
+
}
|
|
18002
|
+
function inferTableReplacementParagraphBlock(startParagraph, options = {}) {
|
|
18003
|
+
const maxScan = Number.isInteger(options?.maxScan) && options.maxScan > 0 ? options.maxScan : 10;
|
|
18004
|
+
const paragraphTextGetter = typeof options?.getParagraphText === "function" ? options.getParagraphText : getParagraphText;
|
|
18005
|
+
if (!startParagraph || !startParagraph.parentNode) return null;
|
|
18006
|
+
const block = [startParagraph];
|
|
18007
|
+
let cursor = startParagraph.nextSibling;
|
|
18008
|
+
let scanned = 0;
|
|
18009
|
+
while (cursor && scanned < maxScan) {
|
|
18010
|
+
scanned += 1;
|
|
18011
|
+
const nextCursor = cursor.nextSibling;
|
|
18012
|
+
if (cursor.nodeType !== 1 || cursor.namespaceURI !== WORD_MAIN_NS || cursor.localName !== "p") {
|
|
18013
|
+
cursor = nextCursor;
|
|
18014
|
+
continue;
|
|
17958
18015
|
}
|
|
17959
|
-
|
|
17960
|
-
|
|
17961
|
-
|
|
17962
|
-
|
|
17963
|
-
|
|
17964
|
-
if (!payloadFormats.has(numId)) {
|
|
17965
|
-
const abstract = abstractNumForNumId(payloadDoc, numId);
|
|
17966
|
-
if (!abstract) return null;
|
|
17967
|
-
payloadFormats.set(numId, levelFormats(abstract));
|
|
18016
|
+
const text2 = String(paragraphTextGetter(cursor) || "").trim();
|
|
18017
|
+
if (!text2) {
|
|
18018
|
+
if (block.length > 1) break;
|
|
18019
|
+
cursor = nextCursor;
|
|
18020
|
+
continue;
|
|
17968
18021
|
}
|
|
17969
|
-
|
|
17970
|
-
|
|
17971
|
-
|
|
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);
|
|
18022
|
+
if (!isLikelyStructuredTableSourceParagraph(text2)) break;
|
|
18023
|
+
block.push(cursor);
|
|
18024
|
+
cursor = nextCursor;
|
|
17977
18025
|
}
|
|
17978
|
-
return
|
|
18026
|
+
return block.length > 1 ? block : null;
|
|
17979
18027
|
}
|
|
17980
|
-
function
|
|
17981
|
-
const
|
|
17982
|
-
|
|
17983
|
-
|
|
17984
|
-
|
|
17985
|
-
|
|
17986
|
-
|
|
18028
|
+
function synthesizeTableMarkdownFromMultilineCellEdit(targetParagraph, modifiedText, options = {}) {
|
|
18029
|
+
const onInfo = typeof options.onInfo === "function" ? options.onInfo : () => {
|
|
18030
|
+
};
|
|
18031
|
+
const onWarn = typeof options.onWarn === "function" ? options.onWarn : () => {
|
|
18032
|
+
};
|
|
18033
|
+
const rawModified = String(modifiedText || "");
|
|
18034
|
+
if (!rawModified.includes("\n")) return null;
|
|
18035
|
+
if (isMarkdownTableText(rawModified)) return null;
|
|
18036
|
+
const lines = rawModified.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean);
|
|
18037
|
+
if (lines.length < 2) return null;
|
|
18038
|
+
const tableElement = options.tableElement || findContainingWordElement(targetParagraph, "tbl");
|
|
18039
|
+
const rowElement = findContainingWordElement(targetParagraph, "tr");
|
|
18040
|
+
const cellElement = findContainingWordElement(targetParagraph, "tc");
|
|
18041
|
+
if (!tableElement || !rowElement || !cellElement) return null;
|
|
18042
|
+
const currentParagraphText = normalizeWhitespaceForTargeting(
|
|
18043
|
+
options.currentParagraphText || getParagraphText(targetParagraph)
|
|
18044
|
+
);
|
|
18045
|
+
const firstLine = normalizeWhitespaceForTargeting(lines[0]);
|
|
18046
|
+
if (currentParagraphText && firstLine && firstLine !== currentParagraphText) {
|
|
18047
|
+
onWarn("[Table] Multiline cell text did not anchor to original cell text; skipping table-row synthesis heuristic.");
|
|
18048
|
+
return null;
|
|
17987
18049
|
}
|
|
17988
|
-
const
|
|
17989
|
-
|
|
17990
|
-
const
|
|
17991
|
-
|
|
17992
|
-
|
|
17993
|
-
|
|
17994
|
-
|
|
17995
|
-
|
|
17996
|
-
|
|
17997
|
-
|
|
18050
|
+
const { matrix, rowElements, columnCount } = extractTableMatrix(tableElement);
|
|
18051
|
+
if (matrix.length === 0 || columnCount === 0) return null;
|
|
18052
|
+
const rowIndex = rowElements.indexOf(rowElement);
|
|
18053
|
+
const cellElements = getDirectWordChildren(rowElement, "tc");
|
|
18054
|
+
const colIndex = cellElements.indexOf(cellElement);
|
|
18055
|
+
if (rowIndex < 0 || colIndex < 0 || colIndex >= columnCount) return null;
|
|
18056
|
+
matrix[rowIndex][colIndex] = lines[0];
|
|
18057
|
+
const mirrorAcrossColumns = isSymmetricLabelRow(matrix[rowIndex]);
|
|
18058
|
+
if (mirrorAcrossColumns) {
|
|
18059
|
+
onInfo("[Table] Symmetric row detected; mirroring inserted row values across columns.");
|
|
17998
18060
|
}
|
|
17999
|
-
|
|
18000
|
-
|
|
18001
|
-
const
|
|
18002
|
-
if (
|
|
18003
|
-
|
|
18004
|
-
|
|
18005
|
-
|
|
18006
|
-
|
|
18007
|
-
|
|
18008
|
-
|
|
18009
|
-
|
|
18010
|
-
|
|
18011
|
-
setElementVal2(abstractNumIdNode, abstractNumMap.get(oldAbsRef));
|
|
18061
|
+
for (let i2 = 1; i2 < lines.length; i2++) {
|
|
18062
|
+
const insertIndex = rowIndex + i2;
|
|
18063
|
+
const extraValue = lines[i2];
|
|
18064
|
+
if (insertIndex < matrix.length && !normalizeWhitespaceForTargeting(matrix[insertIndex][colIndex])) {
|
|
18065
|
+
if (mirrorAcrossColumns) {
|
|
18066
|
+
for (let col = 0; col < columnCount; col++) {
|
|
18067
|
+
if (!normalizeWhitespaceForTargeting(matrix[insertIndex][col])) {
|
|
18068
|
+
matrix[insertIndex][col] = extraValue;
|
|
18069
|
+
}
|
|
18070
|
+
}
|
|
18071
|
+
} else {
|
|
18072
|
+
matrix[insertIndex][colIndex] = extraValue;
|
|
18012
18073
|
}
|
|
18013
|
-
}
|
|
18014
|
-
|
|
18015
|
-
|
|
18016
|
-
|
|
18017
|
-
|
|
18018
|
-
|
|
18019
|
-
for (const numIdNode of numIdNodes) {
|
|
18020
|
-
const oldNumRef = getElementId2(numIdNode, ["w:val", "val"]);
|
|
18021
|
-
if (oldNumRef != null && numIdMap.has(oldNumRef)) {
|
|
18022
|
-
setElementVal2(numIdNode, numIdMap.get(oldNumRef));
|
|
18074
|
+
} else {
|
|
18075
|
+
const newRow = new Array(columnCount).fill("");
|
|
18076
|
+
if (mirrorAcrossColumns) {
|
|
18077
|
+
for (let col = 0; col < columnCount; col++) newRow[col] = extraValue;
|
|
18078
|
+
} else {
|
|
18079
|
+
newRow[colIndex] = extraValue;
|
|
18023
18080
|
}
|
|
18081
|
+
matrix.splice(Math.min(insertIndex, matrix.length), 0, newRow);
|
|
18024
18082
|
}
|
|
18025
18083
|
}
|
|
18026
|
-
|
|
18027
|
-
|
|
18028
|
-
|
|
18029
|
-
|
|
18030
|
-
}
|
|
18031
|
-
function mergeNumberingXmlBySchemaOrder(existingNumberingXml, incomingNumberingXml) {
|
|
18032
|
-
const existingText = String(existingNumberingXml || "");
|
|
18033
|
-
const incomingText = String(incomingNumberingXml || "");
|
|
18034
|
-
if (!incomingText.trim()) return existingText;
|
|
18035
|
-
if (!existingText.trim()) return incomingText;
|
|
18036
|
-
try {
|
|
18037
|
-
const existingDoc = parseOoxml(existingText);
|
|
18038
|
-
const incomingDoc = parseOoxml(incomingText);
|
|
18039
|
-
if (hasXmlParseError(existingDoc) || hasXmlParseError(incomingDoc)) {
|
|
18040
|
-
return existingText;
|
|
18041
|
-
}
|
|
18042
|
-
const existingRoot = existingDoc.documentElement;
|
|
18043
|
-
const incomingRoot = incomingDoc.documentElement;
|
|
18044
|
-
if (!existingRoot || !incomingRoot) return existingText;
|
|
18045
|
-
const existingAbstractIds = new Set(
|
|
18046
|
-
Array.from(existingRoot.childNodes || []).filter((node) => isDirectWordChild(node, "abstractNum")).map((node) => parseIntegerAttribute(node, ["w:abstractNumId", "abstractNumId"])).filter((id) => id != null)
|
|
18047
|
-
);
|
|
18048
|
-
const existingNumIds = new Set(
|
|
18049
|
-
Array.from(existingRoot.childNodes || []).filter((node) => isDirectWordChild(node, "num")).map((node) => parseIntegerAttribute(node, ["w:numId", "numId"])).filter((id) => id != null)
|
|
18050
|
-
);
|
|
18051
|
-
for (const incomingNode of Array.from(incomingRoot.childNodes || [])) {
|
|
18052
|
-
if (!isDirectWordChild(incomingNode, "abstractNum")) continue;
|
|
18053
|
-
const incomingId = parseIntegerAttribute(incomingNode, ["w:abstractNumId", "abstractNumId"]);
|
|
18054
|
-
if (incomingId == null || existingAbstractIds.has(incomingId)) continue;
|
|
18055
|
-
insertNumberingNodeInSchemaOrder(existingRoot, existingDoc.importNode(incomingNode, true), "abstract");
|
|
18056
|
-
existingAbstractIds.add(incomingId);
|
|
18057
|
-
}
|
|
18058
|
-
for (const incomingNode of Array.from(incomingRoot.childNodes || [])) {
|
|
18059
|
-
if (!isDirectWordChild(incomingNode, "num")) continue;
|
|
18060
|
-
const incomingId = parseIntegerAttribute(incomingNode, ["w:numId", "numId"]);
|
|
18061
|
-
if (incomingId == null || existingNumIds.has(incomingId)) continue;
|
|
18062
|
-
insertNumberingNodeInSchemaOrder(existingRoot, existingDoc.importNode(incomingNode, true), "num");
|
|
18063
|
-
existingNumIds.add(incomingId);
|
|
18064
|
-
}
|
|
18065
|
-
return serializeOoxml(existingDoc);
|
|
18066
|
-
} catch {
|
|
18067
|
-
return existingText;
|
|
18068
|
-
}
|
|
18084
|
+
const markdown = tableMatrixToMarkdown(matrix, columnCount);
|
|
18085
|
+
if (!markdown) return null;
|
|
18086
|
+
onInfo("[Table] Synthesized full markdown table from multiline cell edit for table-scope reconciliation.");
|
|
18087
|
+
return markdown;
|
|
18069
18088
|
}
|
|
18070
18089
|
|
|
18071
18090
|
// pipeline/ingestion-export.js
|
|
@@ -21715,7 +21734,7 @@ function validateDocumentOperation(operation) {
|
|
|
21715
21734
|
}
|
|
21716
21735
|
};
|
|
21717
21736
|
}
|
|
21718
|
-
if (target.revisionView !== "accepted" || target.captureRef || target.occurrence != null || normalized.captureKey || normalized.targetEndDescriptor || normalized.targetEndRef != null
|
|
21737
|
+
if (target.revisionView !== "accepted" || target.captureRef || target.occurrence != null || normalized.captureKey || normalized.targetEndDescriptor || normalized.targetEndRef != null) {
|
|
21719
21738
|
return {
|
|
21720
21739
|
valid: false,
|
|
21721
21740
|
error: {
|
|
@@ -21737,6 +21756,15 @@ function validateDocumentOperation(operation) {
|
|
|
21737
21756
|
}
|
|
21738
21757
|
};
|
|
21739
21758
|
}
|
|
21759
|
+
if (replacementValidation.replacements.some((replacement) => /\r|\n/.test(replacement.find))) {
|
|
21760
|
+
return {
|
|
21761
|
+
valid: false,
|
|
21762
|
+
error: {
|
|
21763
|
+
code: "INVALID_OPERATION",
|
|
21764
|
+
message: 'Localized replacements v1 cannot span a line break; use one replacement per line or a full "modified" paragraph.'
|
|
21765
|
+
}
|
|
21766
|
+
};
|
|
21767
|
+
}
|
|
21740
21768
|
normalized.replacements = replacementValidation.replacements;
|
|
21741
21769
|
}
|
|
21742
21770
|
}
|
|
@@ -21931,6 +21959,12 @@ function validateDocumentOperation(operation) {
|
|
|
21931
21959
|
error: { code: "INVALID_OPERATION", message: 'Format operations require a non-empty "textToFormat" field.' }
|
|
21932
21960
|
};
|
|
21933
21961
|
}
|
|
21962
|
+
if (normalized.textOccurrence != null && (!Number.isInteger(normalized.textOccurrence) || normalized.textOccurrence < 1)) {
|
|
21963
|
+
return {
|
|
21964
|
+
valid: false,
|
|
21965
|
+
error: { code: "INVALID_OPERATION", message: "textOccurrence must be a positive integer when provided." }
|
|
21966
|
+
};
|
|
21967
|
+
}
|
|
21934
21968
|
if (!isRecord(normalized.properties)) {
|
|
21935
21969
|
return {
|
|
21936
21970
|
valid: false,
|
|
@@ -22387,7 +22421,7 @@ function compileOperationBatch(xmlDoc, operations = [], options = {}) {
|
|
|
22387
22421
|
};
|
|
22388
22422
|
const compiled = { ...sourceOperation };
|
|
22389
22423
|
const dynamicOccurrence = operation.targetDescriptor?.occurrence != null && !operation.targetDescriptor?.paragraphId && !operation.targetDescriptor?.fingerprint;
|
|
22390
|
-
const compositeTextTarget = typeof operation.targetDescriptor?.text === "string" && /\r|\n/.test(operation.targetDescriptor.text) && !operation.targetEndDescriptor;
|
|
22424
|
+
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));
|
|
22391
22425
|
if (dynamicOccurrence || compositeTextTarget) binding.dynamic = true;
|
|
22392
22426
|
if (validation.valid && operation.operationKind !== "comment_reply" && operation.operationKind !== "comment_resolve" && !operation.targetDescriptor?.captureRef && !binding.dynamic) {
|
|
22393
22427
|
try {
|
|
@@ -22725,6 +22759,17 @@ function deriveSingleParagraphPlainAdjacencyInsertion(currentParagraphText, modi
|
|
|
22725
22759
|
}
|
|
22726
22760
|
return null;
|
|
22727
22761
|
}
|
|
22762
|
+
function deriveParagraphThenTableAppend(modifiedText) {
|
|
22763
|
+
const lines = String(modifiedText || "").split(/\r?\n/g).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
|
|
22764
|
+
if (lines.length < 3) return null;
|
|
22765
|
+
const tableStart = lines.findIndex((line) => line.trim().startsWith("|"));
|
|
22766
|
+
if (tableStart !== 1) return null;
|
|
22767
|
+
const tableLines = lines.slice(1);
|
|
22768
|
+
if (!tableLines.every((line) => line.trim().startsWith("|"))) return null;
|
|
22769
|
+
const separator = tableLines[1].replace(/\s+/g, "");
|
|
22770
|
+
if (!/^\|:?-{3,}:?(\|:?-{3,}:?)+\|?$/.test(separator)) return null;
|
|
22771
|
+
return { paragraphMarkdown: lines[0].trim(), tableText: tableLines.join("\n") };
|
|
22772
|
+
}
|
|
22728
22773
|
|
|
22729
22774
|
// services/capture-engine.js
|
|
22730
22775
|
function createCaptureError(code, message) {
|
|
@@ -24510,6 +24555,81 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
24510
24555
|
numberingXml: null
|
|
24511
24556
|
};
|
|
24512
24557
|
}
|
|
24558
|
+
const tableAppendCandidate = !useTableScope && !hasExplicitRangeScope && !targetListInfo && !containingTable && !targetEndRef ? deriveParagraphThenTableAppend(effectiveModifiedText) : null;
|
|
24559
|
+
const tableAppendParagraph = tableAppendCandidate ? preprocessMarkdown(tableAppendCandidate.paragraphMarkdown) : null;
|
|
24560
|
+
if (tableAppendCandidate && normalizeWhitespaceForTargeting(tableAppendParagraph.cleanText) === normalizeWhitespaceForTargeting(currentParagraphText)) {
|
|
24561
|
+
const tableData = parseTable(tableAppendCandidate.tableText);
|
|
24562
|
+
if (tableData.headers.length > 0 || tableData.rows.length > 0) {
|
|
24563
|
+
onInfo("[Table] Applying retained-paragraph table append heuristic.");
|
|
24564
|
+
const parent2 = targetParagraph.parentNode;
|
|
24565
|
+
if (!parent2) throw new Error("Target paragraph has no parent for table append");
|
|
24566
|
+
const followingNode = targetParagraph.nextSibling;
|
|
24567
|
+
if (tableAppendParagraph.formatHints.length > 0) {
|
|
24568
|
+
const formatted = await applyRedlineToOxml2(
|
|
24569
|
+
serializer.serializeToString(targetParagraph),
|
|
24570
|
+
currentParagraphText || targetText,
|
|
24571
|
+
tableAppendCandidate.paragraphMarkdown,
|
|
24572
|
+
{
|
|
24573
|
+
author,
|
|
24574
|
+
generateRedlines,
|
|
24575
|
+
existingRevisions: options.existingRevisions || "merge-same-author",
|
|
24576
|
+
structuredContent: false,
|
|
24577
|
+
_revisionIdAllocator: revisionIdAllocator
|
|
24578
|
+
}
|
|
24579
|
+
);
|
|
24580
|
+
if (formatted?.status === "error") {
|
|
24581
|
+
return {
|
|
24582
|
+
documentXml,
|
|
24583
|
+
hasChanges: false,
|
|
24584
|
+
numberingXml: null,
|
|
24585
|
+
status: "error",
|
|
24586
|
+
error: formatted.error
|
|
24587
|
+
};
|
|
24588
|
+
}
|
|
24589
|
+
if (formatted?.hasChanges) {
|
|
24590
|
+
if (typeof formatted.oxml !== "string") {
|
|
24591
|
+
throw new Error("Reconciliation engine did not return OOXML for a changed paragraph format");
|
|
24592
|
+
}
|
|
24593
|
+
for (const node of extractReplacementNodes(formatted.oxml).replacementNodes) {
|
|
24594
|
+
const imported = xmlDoc.importNode(node, true);
|
|
24595
|
+
ensureParagraphIdsOnImportedNode(imported, operationSession);
|
|
24596
|
+
options?._mutationLiveNodes?.push(parent2.insertBefore(imported, targetParagraph));
|
|
24597
|
+
}
|
|
24598
|
+
options?._mutationRemovedNodes?.push(targetParagraph);
|
|
24599
|
+
parent2.removeChild(targetParagraph);
|
|
24600
|
+
}
|
|
24601
|
+
}
|
|
24602
|
+
const tableOoxml = generateTableOoxml(tableData, {
|
|
24603
|
+
generateRedlines,
|
|
24604
|
+
author,
|
|
24605
|
+
revisionIdAllocator,
|
|
24606
|
+
trackAsBlock: true
|
|
24607
|
+
});
|
|
24608
|
+
for (const node of extractReplacementNodes(tableOoxml).replacementNodes) {
|
|
24609
|
+
options?._mutationLiveNodes?.push(parent2.insertBefore(xmlDoc.importNode(node, true), followingNode));
|
|
24610
|
+
}
|
|
24611
|
+
const endsContainer = !followingNode || followingNode.nodeType === 1 && followingNode.namespaceURI === NS_W9 && followingNode.localName === "sectPr";
|
|
24612
|
+
if (endsContainer) {
|
|
24613
|
+
const trailing = createWordElement(xmlDoc, "w:p");
|
|
24614
|
+
const trailingPPr = clonePropertiesWithoutRevisionHistory(getDirectWordChild4(targetParagraph, "pPr"));
|
|
24615
|
+
if (trailingPPr) {
|
|
24616
|
+
for (const child of Array.from(trailingPPr.childNodes)) {
|
|
24617
|
+
if (child.nodeType === 1 && ["sectPr", "numPr"].includes(child.localName)) trailingPPr.removeChild(child);
|
|
24618
|
+
}
|
|
24619
|
+
trailing.appendChild(trailingPPr);
|
|
24620
|
+
}
|
|
24621
|
+
if (generateRedlines) markParagraphMarkInserted(xmlDoc, trailing, author);
|
|
24622
|
+
ensureParagraphIdsOnImportedNode(trailing, operationSession);
|
|
24623
|
+
options?._mutationLiveNodes?.push(parent2.insertBefore(trailing, followingNode));
|
|
24624
|
+
}
|
|
24625
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
24626
|
+
return {
|
|
24627
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
24628
|
+
hasChanges: true,
|
|
24629
|
+
numberingXml: null
|
|
24630
|
+
};
|
|
24631
|
+
}
|
|
24632
|
+
}
|
|
24513
24633
|
const plainAdjacencyInsertionCandidate = !bypassSingleParagraphHeuristics && !useTableScope && !hasExplicitRangeScope && !targetListInfo ? deriveSingleParagraphPlainAdjacencyInsertion(currentParagraphText, effectiveModifiedText) : null;
|
|
24514
24634
|
if (plainAdjacencyInsertionCandidate) {
|
|
24515
24635
|
onInfo(
|
|
@@ -24635,6 +24755,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
24635
24755
|
explicitStructuredContent: options.explicitStructuredContent === true,
|
|
24636
24756
|
pairReplacements: options.pairReplacements === true,
|
|
24637
24757
|
insertionAffinity: options.insertionAffinity || null,
|
|
24758
|
+
literalTextEdit: options.literalTextEdit === true,
|
|
24638
24759
|
_revisionIdAllocator: revisionIdAllocator,
|
|
24639
24760
|
_isolatedTableCell: useTableScope
|
|
24640
24761
|
});
|
|
@@ -24922,7 +25043,8 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
|
|
|
24922
25043
|
spanRanges.push({ ...entry, start, end });
|
|
24923
25044
|
}
|
|
24924
25045
|
const targetDescriptor = options.targetDescriptor;
|
|
24925
|
-
const
|
|
25046
|
+
const explicitTextOccurrence = Number.isInteger(options.textOccurrence) && options.textOccurrence > 0 ? options.textOccurrence : null;
|
|
25047
|
+
const occurrence = explicitTextOccurrence ?? targetDescriptor?.occurrence ?? null;
|
|
24926
25048
|
let matchStart = -1;
|
|
24927
25049
|
if (occurrence != null && occurrence > 0) {
|
|
24928
25050
|
let count = 0;
|
|
@@ -24940,6 +25062,17 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
|
|
|
24940
25062
|
} else {
|
|
24941
25063
|
matchStart = fullParaText.indexOf(textToFormat);
|
|
24942
25064
|
}
|
|
25065
|
+
if (matchStart === -1 && explicitTextOccurrence != null && fullParaText.includes(textToFormat)) {
|
|
25066
|
+
return {
|
|
25067
|
+
documentXml,
|
|
25068
|
+
hasChanges: false,
|
|
25069
|
+
status: "error",
|
|
25070
|
+
error: {
|
|
25071
|
+
code: "PATCH_SOURCE_NOT_FOUND",
|
|
25072
|
+
message: `Occurrence ${explicitTextOccurrence} of text to format was not found in target paragraph: "${textToFormat}".`
|
|
25073
|
+
}
|
|
25074
|
+
};
|
|
25075
|
+
}
|
|
24943
25076
|
if (matchStart === -1) {
|
|
24944
25077
|
return {
|
|
24945
25078
|
documentXml,
|
|
@@ -25222,9 +25355,11 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
|
|
|
25222
25355
|
...operation.existingRevisions ? { existingRevisions: operation.existingRevisions } : {},
|
|
25223
25356
|
structuredContent: typeof operation.structuredContent === "boolean" ? operation.structuredContent : options.structuredContent !== false,
|
|
25224
25357
|
explicitStructuredContent: operation.structuredContent === true,
|
|
25358
|
+
...operation._localizedReplacementCompilation && operation.structuredContent !== true ? { literalTextEdit: true } : {},
|
|
25225
25359
|
pairReplacements: typeof operation.pairReplacements === "boolean" ? operation.pairReplacements : options.pairReplacements !== false,
|
|
25226
25360
|
...operation.insertionAffinity ? { insertionAffinity: operation.insertionAffinity } : {},
|
|
25227
25361
|
...operation.formattingRevisionPolicy ? { formattingRevisionPolicy: operation.formattingRevisionPolicy } : {},
|
|
25362
|
+
...operation.operationKind === "format" && Number.isInteger(operation.textOccurrence) ? { textOccurrence: operation.textOccurrence } : {},
|
|
25228
25363
|
targetDescriptor: operation.targetDescriptor,
|
|
25229
25364
|
targetEndDescriptor: operation.targetEndDescriptor,
|
|
25230
25365
|
_compiledSourceId: operation._compiledSourceId || null,
|
|
@@ -25848,6 +25983,9 @@ async function applyOperationsToDocumentXml(documentXml, operations, author, run
|
|
|
25848
25983
|
const scheduled = dependencyPlan.scheduled;
|
|
25849
25984
|
const atomic = options.atomic === true;
|
|
25850
25985
|
const continueOnError = options.continueOnError !== false;
|
|
25986
|
+
if (runtimeContext && typeof runtimeContext === "object" && !runtimeContext.numberingIdState && typeof runtimeContext.numberingXml === "string" && runtimeContext.numberingXml.trim()) {
|
|
25987
|
+
runtimeContext.numberingIdState = createDynamicNumberingIdState(runtimeContext.numberingXml);
|
|
25988
|
+
}
|
|
25851
25989
|
const context = cloneBatchRuntimeContext(runtimeContext);
|
|
25852
25990
|
if (!(context.targetRefSnapshot instanceof Map)) {
|
|
25853
25991
|
context.targetRefSnapshot = session.initialTargetReferenceSnapshot;
|
|
@@ -28273,8 +28411,32 @@ function openDocx(input) {
|
|
|
28273
28411
|
}
|
|
28274
28412
|
|
|
28275
28413
|
// index.js
|
|
28414
|
+
function remapGeneratedNumberingForSource(result, options) {
|
|
28415
|
+
const sourceNumbering = typeof options?.numberingXml === "string" && options.numberingXml.trim() ? options.numberingXml : null;
|
|
28416
|
+
const state = options?.numberingIdState && typeof options.numberingIdState === "object" ? options.numberingIdState : sourceNumbering ? createDynamicNumberingIdState(sourceNumbering) : null;
|
|
28417
|
+
if (!state || !result?.hasChanges || typeof result.oxml !== "string" || !result.oxml.includes("/word/numbering.xml")) {
|
|
28418
|
+
return result;
|
|
28419
|
+
}
|
|
28420
|
+
const pkgDoc = parseOoxml(result.oxml);
|
|
28421
|
+
const parts = Array.from(pkgDoc.getElementsByTagNameNS("*", "part"));
|
|
28422
|
+
const payloadOf = (name) => {
|
|
28423
|
+
const part = parts.find((candidate) => (candidate.getAttribute("pkg:name") || candidate.getAttribute("name")) === name);
|
|
28424
|
+
const xmlData = part?.getElementsByTagNameNS("*", "xmlData")[0];
|
|
28425
|
+
return xmlData ? Array.from(xmlData.childNodes).find((node) => node.nodeType === 1) || null : null;
|
|
28426
|
+
};
|
|
28427
|
+
const numberingNode = payloadOf("/word/numbering.xml");
|
|
28428
|
+
const body = payloadOf("/word/document.xml")?.getElementsByTagNameNS("*", "body")[0];
|
|
28429
|
+
if (!numberingNode || !body) return result;
|
|
28430
|
+
const bodyNodes = Array.from(body.childNodes).filter((node) => node.nodeType === 1 && node.localName !== "sectPr");
|
|
28431
|
+
const remapped = remapNumberingPayloadForDocument(serializeOoxml(numberingNode), bodyNodes, state);
|
|
28432
|
+
const newNumbering = pkgDoc.importNode(parseOoxml(remapped.numberingXml).documentElement, true);
|
|
28433
|
+
numberingNode.parentNode.replaceChild(newNumbering, numberingNode);
|
|
28434
|
+
bodyNodes.forEach((node, index) => body.replaceChild(remapped.replacementNodes[index], node));
|
|
28435
|
+
return { ...result, oxml: serializeOoxml(pkgDoc) };
|
|
28436
|
+
}
|
|
28276
28437
|
async function applyRedlineToOxml3(oxml, originalText, modifiedText, options = {}) {
|
|
28277
|
-
const
|
|
28438
|
+
const rawResult = await applyRedlineToOxml(oxml, originalText, modifiedText, options);
|
|
28439
|
+
const result = remapGeneratedNumberingForSource(rawResult, options);
|
|
28278
28440
|
if (result?.useNativeApi && typeof result?.oxml !== "string") {
|
|
28279
28441
|
const existingWarnings = Array.isArray(result?.warnings) ? result.warnings : [];
|
|
28280
28442
|
return withOoxmlSourceType({
|