@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 v0.8.
|
|
1
|
+
// @ansonlai/docx-redline-js 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;
|
|
@@ -3254,7 +3254,14 @@ function buildRunXmlWithHints(item, formatHints, options = {}) {
|
|
|
3254
3254
|
function buildSimpleRun(text2, rPrXml) {
|
|
3255
3255
|
if (!text2) return "";
|
|
3256
3256
|
const rPr = rPrXml || "";
|
|
3257
|
-
return `<w:r>${rPr}
|
|
3257
|
+
return `<w:r>${rPr}${buildRunContent(text2, "w:t")}</w:r>`;
|
|
3258
|
+
}
|
|
3259
|
+
function buildRunContent(text2, textTag) {
|
|
3260
|
+
return String(text2).split(/(\r\n|[\n\r\v\t])/).map((part) => {
|
|
3261
|
+
if (part === " ") return "<w:tab/>";
|
|
3262
|
+
if (part === "\n" || part === "\r" || part === "\r\n" || part === "\v") return "<w:br/>";
|
|
3263
|
+
return part ? `<${textTag} xml:space="preserve">${escapeXml(part)}</${textTag}>` : "";
|
|
3264
|
+
}).join("");
|
|
3258
3265
|
}
|
|
3259
3266
|
function buildDeletionXml(item, options = {}) {
|
|
3260
3267
|
const metadata = createRevisionMetadata(
|
|
@@ -3267,7 +3274,7 @@ function buildDeletionXml(item, options = {}) {
|
|
|
3267
3274
|
if (font) {
|
|
3268
3275
|
rPr = applyFont(rPr, font);
|
|
3269
3276
|
}
|
|
3270
|
-
return `<w:del w:id="${metadata.id}" w:author="${escapeXml(metadata.author)}" w:date="${metadata.date}"><w:r>${rPr}
|
|
3277
|
+
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>`;
|
|
3271
3278
|
}
|
|
3272
3279
|
function buildInsertionXml(item, formatHints, options = {}) {
|
|
3273
3280
|
const metadata = createRevisionMetadata(
|
|
@@ -3608,7 +3615,7 @@ function generateTableOoxml(tableData, options = {}) {
|
|
|
3608
3615
|
revisionIdAllocator = null,
|
|
3609
3616
|
trackAsBlock = false
|
|
3610
3617
|
} = options;
|
|
3611
|
-
const
|
|
3618
|
+
const trackRows = generateRedlines && trackAsBlock;
|
|
3612
3619
|
const numCols = tableData.headers?.length || (tableData.rows?.[0]?.length || 1);
|
|
3613
3620
|
const tblPr = `
|
|
3614
3621
|
<w:tblPr>
|
|
@@ -3636,14 +3643,16 @@ function generateTableOoxml(tableData, options = {}) {
|
|
|
3636
3643
|
const cellText = row[c] || "";
|
|
3637
3644
|
const { cleanText, formatHints } = preprocessMarkdown(cellText);
|
|
3638
3645
|
const runModel = [{
|
|
3639
|
-
kind: generateRedlines
|
|
3646
|
+
kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
|
|
3640
3647
|
text: cleanText,
|
|
3641
3648
|
rPrXml: isHeaderRow ? "<w:rPr><w:b/></w:rPr>" : "",
|
|
3642
3649
|
author,
|
|
3643
3650
|
startOffset: 0,
|
|
3644
3651
|
endOffset: cleanText.length
|
|
3645
3652
|
}];
|
|
3646
|
-
const
|
|
3653
|
+
const cellMarkMeta = trackRows ? createRevisionMetadata(author, revisionIdAllocator) : null;
|
|
3654
|
+
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;
|
|
3655
|
+
const runsOoxml = serializeToOoxml(runModel, cellPPr, formatHints, {
|
|
3647
3656
|
author,
|
|
3648
3657
|
generateRedlines,
|
|
3649
3658
|
revisionIdAllocator
|
|
@@ -3652,13 +3661,11 @@ function generateTableOoxml(tableData, options = {}) {
|
|
|
3652
3661
|
cellsXml += `<w:tc>${tcPr}${runsOoxml}</w:tc>`;
|
|
3653
3662
|
}
|
|
3654
3663
|
const trPr = isHeaderRow ? "<w:trPr><w:tblHeader/><w:cantSplit/></w:trPr>" : "<w:trPr><w:cantSplit/></w:trPr>";
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
if (tableInsertMeta) {
|
|
3659
|
-
tableXml = `<w:ins w:id="${tableInsertMeta.id}" w:author="${escapeXml(tableInsertMeta.author)}" w:date="${tableInsertMeta.date}">${tableXml}</w:ins>`;
|
|
3664
|
+
const rowMeta = trackRows ? createRevisionMetadata(author, revisionIdAllocator) : null;
|
|
3665
|
+
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;
|
|
3666
|
+
rowsXml += `<w:tr>${trackedTrPr}${cellsXml}</w:tr>`;
|
|
3660
3667
|
}
|
|
3661
|
-
return
|
|
3668
|
+
return `<w:tbl>${tblPr}${tblGrid}${rowsXml}</w:tbl>`;
|
|
3662
3669
|
}
|
|
3663
3670
|
function diffTablesWithVirtualGrid(oldGrid, newTableData) {
|
|
3664
3671
|
const operations = [];
|
|
@@ -8582,6 +8589,11 @@ function rejectTrackedChangesInOoxml(oxml, options = {}) {
|
|
|
8582
8589
|
}
|
|
8583
8590
|
if (removeNode(insNode)) rejectedCount += 1;
|
|
8584
8591
|
}
|
|
8592
|
+
for (const tableNode of getWordElementsByLocalName(xmlDoc, "tbl")) {
|
|
8593
|
+
if (!tableNode.parentNode) continue;
|
|
8594
|
+
const hasRow = Array.from(tableNode.childNodes || []).some((child) => isWordElement5(child, "tr"));
|
|
8595
|
+
if (!hasRow && removeNode(tableNode)) rejectedCount += 1;
|
|
8596
|
+
}
|
|
8585
8597
|
for (const delNode of getWordElementsByLocalName(xmlDoc, "del")) {
|
|
8586
8598
|
if (!delNode.parentNode || !authorMatchesNode(delNode, filter)) continue;
|
|
8587
8599
|
if (isParagraphMarkRevisionMarker(delNode)) {
|
|
@@ -9211,7 +9223,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
9211
9223
|
operationWarnings.push("Input was sanitized; pass sanitizeInput: false to disable.");
|
|
9212
9224
|
}
|
|
9213
9225
|
let structuredAnalysis = null;
|
|
9214
|
-
if (options.structuredContent !== false) {
|
|
9226
|
+
if (options.structuredContent !== false && options.literalTextEdit !== true) {
|
|
9215
9227
|
structuredAnalysis = analyzeStructuredContent(sanitizedText);
|
|
9216
9228
|
if (!structuredAnalysis.valid) {
|
|
9217
9229
|
if (options.explicitStructuredContent === true) {
|
|
@@ -9230,13 +9242,16 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
9230
9242
|
}
|
|
9231
9243
|
}
|
|
9232
9244
|
const { cleanText: cleanModifiedText, formatHints } = preprocessMarkdown(sanitizedText);
|
|
9233
|
-
const hasTextChanges = existingRevisionsPolicy === "slice-cross-author" ? cleanModifiedText !== originalText : cleanModifiedText.trim() !== originalText.trim();
|
|
9234
|
-
const hasFormatHints = formatHints.length > 0;
|
|
9235
9245
|
const { existingFormatHints, textSpans, paragraphs } = extractFormattingFromOoxml(xmlDoc);
|
|
9246
|
+
const hasLiteralTextChanges = existingRevisionsPolicy === "slice-cross-author" ? cleanModifiedText !== originalText : cleanModifiedText.trim() !== originalText.trim();
|
|
9247
|
+
const hasListStructureChange = !hasLiteralTextChanges && options.explicitStructuredContent === true && isListTargetLoose(cleanModifiedText) && !sourceMatchesListShape(paragraphs, cleanModifiedText);
|
|
9248
|
+
const hasTextChanges = hasLiteralTextChanges || hasListStructureChange;
|
|
9249
|
+
const hasFormatHints = formatHints.length > 0;
|
|
9236
9250
|
const hasExistingFormatting = existingFormatHints.length > 0;
|
|
9237
|
-
const
|
|
9251
|
+
const { textSpans: targetSpans } = buildTextSpansFromParagraphs(paragraphs);
|
|
9252
|
+
const visibleText = targetSpans.map((span) => textSpanVisibleText(span)).join("");
|
|
9238
9253
|
const targetFound = originalText.includes("\n") || originalText.includes("\r") ? originalText.split(/\r?\n/).map(normalizeTargetText).filter(Boolean).every((line) => paragraphs.some((paragraph) => {
|
|
9239
|
-
const paragraphText =
|
|
9254
|
+
const paragraphText = targetSpans.filter((span) => span.paragraph === paragraph).map(textSpanVisibleText).join("");
|
|
9240
9255
|
return normalizeTargetText(paragraphText).includes(line);
|
|
9241
9256
|
})) : visibleText.includes(originalText.trim()) || visibleText.replace(/[\t\n\u2011]/g, "").includes(originalText.trim().replace(/[\t\n\u2011]/g, "")) || normalizeTargetText(visibleText).includes(normalizeTargetText(originalText));
|
|
9242
9257
|
if (hasTextChanges && typeof originalText === "string" && originalText.trim() && !targetFound) {
|
|
@@ -9350,7 +9365,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
9350
9365
|
const tables = getElementsByTagNSOrTag(xmlDoc, NS_W, "tbl");
|
|
9351
9366
|
const hasTables = tables.length > 0;
|
|
9352
9367
|
const isMarkdownTable = /^\|.+\|/.test(cleanModifiedText.trim()) && cleanModifiedText.includes("\n");
|
|
9353
|
-
const isTargetList = isListTargetLoose(cleanModifiedText);
|
|
9368
|
+
const isTargetList = options.literalTextEdit !== true && isListTargetLoose(cleanModifiedText);
|
|
9354
9369
|
const isStructuredContent = options.structuredContent !== false && structuredAnalysis?.requiresStructuredContent === true;
|
|
9355
9370
|
const tableCellContext = initialTableCellContext;
|
|
9356
9371
|
const usesSurgicalTextMode = hasTables || existingRevisionsPolicy === "slice-cross-author";
|
|
@@ -9465,6 +9480,10 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
9465
9480
|
throw caught;
|
|
9466
9481
|
}
|
|
9467
9482
|
}
|
|
9483
|
+
function sourceMatchesListShape(paragraphs, listText) {
|
|
9484
|
+
const itemCount = String(listText).split(/\r?\n/).filter((line) => line.trim()).length;
|
|
9485
|
+
return paragraphs.length === itemCount && paragraphs.every((paragraph) => getElementsByTagNSOrTag(paragraph, NS_W, "numPr").length > 0);
|
|
9486
|
+
}
|
|
9468
9487
|
function normalizeTargetText(text2) {
|
|
9469
9488
|
return String(text2 || "").replace(/[\t\n\u2011]/g, " ").replace(/\s+/g, " ").trim();
|
|
9470
9489
|
}
|
|
@@ -10934,659 +10953,659 @@ async function executeSingleLineListStructuralFallback(plan, options = {}) {
|
|
|
10934
10953
|
};
|
|
10935
10954
|
}
|
|
10936
10955
|
|
|
10937
|
-
//
|
|
10938
|
-
|
|
10939
|
-
|
|
10940
|
-
|
|
10941
|
-
|
|
10942
|
-
|
|
10943
|
-
|
|
10944
|
-
|
|
10945
|
-
}
|
|
10946
|
-
|
|
10947
|
-
return node.getAttribute(`w:${name}`) || node.getAttribute(name) || "";
|
|
10948
|
-
}
|
|
10949
|
-
function xmlSpaceAttribute(node) {
|
|
10950
|
-
return node.getAttribute("xml:space") || node.getAttribute("space") || node.getAttributeNS?.("http://www.w3.org/XML/1998/namespace", "space") || "";
|
|
10951
|
-
}
|
|
10952
|
-
function isParagraphMarkRevision(node) {
|
|
10953
|
-
return localNameOf3(node.parentNode) === "rPr";
|
|
10956
|
+
// services/numbering-helpers.js
|
|
10957
|
+
function parseIntegerAttribute(element, names) {
|
|
10958
|
+
if (!element || !Array.isArray(names)) return null;
|
|
10959
|
+
for (const name of names) {
|
|
10960
|
+
const raw = element.getAttribute(name);
|
|
10961
|
+
if (raw == null || raw === "") continue;
|
|
10962
|
+
const parsed = Number.parseInt(String(raw), 10);
|
|
10963
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
10964
|
+
}
|
|
10965
|
+
return null;
|
|
10954
10966
|
}
|
|
10955
|
-
function
|
|
10956
|
-
|
|
10957
|
-
|
|
10958
|
-
|
|
10959
|
-
|
|
10960
|
-
|
|
10967
|
+
function nextAvailableId(startId, occupiedIds, maxPreferred = null) {
|
|
10968
|
+
let candidate = Number.isInteger(startId) && startId > 0 ? startId : 1;
|
|
10969
|
+
const occupied = occupiedIds instanceof Set ? occupiedIds : /* @__PURE__ */ new Set();
|
|
10970
|
+
while (occupied.has(candidate)) {
|
|
10971
|
+
candidate += 1;
|
|
10972
|
+
}
|
|
10973
|
+
if (Number.isInteger(maxPreferred) && maxPreferred > 0 && candidate > maxPreferred) {
|
|
10974
|
+
for (let probe = 1; probe <= maxPreferred; probe += 1) {
|
|
10975
|
+
if (!occupied.has(probe)) return probe;
|
|
10961
10976
|
}
|
|
10962
|
-
|
|
10963
|
-
|
|
10964
|
-
|
|
10965
|
-
|
|
10966
|
-
|
|
10977
|
+
}
|
|
10978
|
+
return candidate;
|
|
10979
|
+
}
|
|
10980
|
+
function createDynamicNumberingIdState(numberingXml, options = {}) {
|
|
10981
|
+
const minId = Number.isInteger(options?.minId) && options.minId > 0 ? options.minId : 1;
|
|
10982
|
+
const maxPreferred = Number.isInteger(options?.maxPreferred) && options.maxPreferred >= minId ? options.maxPreferred : 32767;
|
|
10983
|
+
const usedNumIds = /* @__PURE__ */ new Set();
|
|
10984
|
+
const usedAbstractNumIds = /* @__PURE__ */ new Set();
|
|
10985
|
+
if (String(numberingXml || "").trim()) {
|
|
10967
10986
|
try {
|
|
10968
|
-
|
|
10969
|
-
|
|
10970
|
-
|
|
10987
|
+
const numberingDoc = parseOoxml(numberingXml);
|
|
10988
|
+
const abstractNums = Array.from(numberingDoc.getElementsByTagNameNS("*", "abstractNum"));
|
|
10989
|
+
const nums = Array.from(numberingDoc.getElementsByTagNameNS("*", "num"));
|
|
10990
|
+
for (const abstractNum of abstractNums) {
|
|
10991
|
+
const id = parseIntegerAttribute(abstractNum, ["w:abstractNumId", "abstractNumId"]);
|
|
10992
|
+
if (id != null) usedAbstractNumIds.add(id);
|
|
10993
|
+
}
|
|
10994
|
+
for (const num of nums) {
|
|
10995
|
+
const id = parseIntegerAttribute(num, ["w:numId", "numId"]);
|
|
10996
|
+
if (id != null) usedNumIds.add(id);
|
|
10997
|
+
}
|
|
10998
|
+
} catch {
|
|
10971
10999
|
}
|
|
10972
11000
|
}
|
|
11001
|
+
const maxUsedNumId = usedNumIds.size > 0 ? Math.max(...usedNumIds) : 0;
|
|
11002
|
+
const maxUsedAbstractNumId = usedAbstractNumIds.size > 0 ? Math.max(...usedAbstractNumIds) : 0;
|
|
11003
|
+
const baseNumId = Math.max(minId, maxUsedNumId + 1);
|
|
11004
|
+
const baseAbstractNumId = Math.max(minId, maxUsedAbstractNumId + 1);
|
|
11005
|
+
return {
|
|
11006
|
+
// Kept so list edits can recognise which existing definition a source list uses and continue it.
|
|
11007
|
+
sourceNumberingXml: String(numberingXml || ""),
|
|
11008
|
+
nextNumId: nextAvailableId(baseNumId, usedNumIds, maxPreferred),
|
|
11009
|
+
nextAbstractNumId: nextAvailableId(baseAbstractNumId, usedAbstractNumIds, maxPreferred),
|
|
11010
|
+
usedNumIds,
|
|
11011
|
+
usedAbstractNumIds,
|
|
11012
|
+
minId,
|
|
11013
|
+
maxPreferred
|
|
11014
|
+
};
|
|
10973
11015
|
}
|
|
10974
|
-
function
|
|
10975
|
-
|
|
10976
|
-
|
|
10977
|
-
|
|
10978
|
-
if (!
|
|
10979
|
-
|
|
10980
|
-
return { valid: false, issues };
|
|
11016
|
+
function normalizeNumberingIdState(state) {
|
|
11017
|
+
if (!state || typeof state !== "object") return null;
|
|
11018
|
+
if (!(state.usedNumIds instanceof Set)) state.usedNumIds = /* @__PURE__ */ new Set();
|
|
11019
|
+
if (!(state.usedAbstractNumIds instanceof Set)) state.usedAbstractNumIds = /* @__PURE__ */ new Set();
|
|
11020
|
+
if (!Number.isInteger(state.minId) || state.minId < 1) {
|
|
11021
|
+
state.minId = 1;
|
|
10981
11022
|
}
|
|
10982
|
-
|
|
10983
|
-
|
|
10984
|
-
addIssue("PARSE_ERROR", "error", `OOXML does not parse as XML: ${error2}`);
|
|
10985
|
-
return { valid: false, issues };
|
|
11023
|
+
if (!Number.isInteger(state.maxPreferred) || state.maxPreferred < state.minId) {
|
|
11024
|
+
state.maxPreferred = 32767;
|
|
10986
11025
|
}
|
|
10987
|
-
|
|
10988
|
-
|
|
10989
|
-
const revisions = insElements.concat(delElements);
|
|
10990
|
-
for (const paragraph of elementsByLocalName(doc, "p")) {
|
|
10991
|
-
const nested = Array.from(paragraph.getElementsByTagName("*")).find((el) => el !== paragraph && localNameOf3(el) === "p");
|
|
10992
|
-
if (nested) {
|
|
10993
|
-
addIssue("NESTED_PARAGRAPH", "error", `<${paragraph.nodeName}> contains nested <${nested.nodeName}>.`);
|
|
10994
|
-
}
|
|
11026
|
+
if (!Number.isInteger(state.nextNumId) || state.nextNumId < state.minId) {
|
|
11027
|
+
state.nextNumId = state.minId;
|
|
10995
11028
|
}
|
|
10996
|
-
|
|
10997
|
-
|
|
10998
|
-
const sectPrIndexes = children.map((child, index) => localNameOf3(child) === "sectPr" ? index : -1).filter((index) => index >= 0);
|
|
10999
|
-
if (sectPrIndexes.length > 1) {
|
|
11000
|
-
addIssue("MULTIPLE_BODY_SECTPR", "error", "<w:body> contains multiple direct <w:sectPr> elements.");
|
|
11001
|
-
} else if (sectPrIndexes.length === 1 && sectPrIndexes[0] !== children.length - 1) {
|
|
11002
|
-
addIssue("SECTPR_NOT_LAST", "error", "<w:sectPr> is not the last element child of <w:body>.");
|
|
11003
|
-
}
|
|
11029
|
+
if (!Number.isInteger(state.nextAbstractNumId) || state.nextAbstractNumId < state.minId) {
|
|
11030
|
+
state.nextAbstractNumId = state.minId;
|
|
11004
11031
|
}
|
|
11005
|
-
|
|
11006
|
-
|
|
11007
|
-
|
|
11008
|
-
|
|
11009
|
-
|
|
11010
|
-
|
|
11011
|
-
|
|
11012
|
-
|
|
11013
|
-
|
|
11014
|
-
|
|
11015
|
-
|
|
11016
|
-
|
|
11017
|
-
|
|
11032
|
+
state.nextNumId = nextAvailableId(state.nextNumId, state.usedNumIds, state.maxPreferred);
|
|
11033
|
+
state.nextAbstractNumId = nextAvailableId(state.nextAbstractNumId, state.usedAbstractNumIds, state.maxPreferred);
|
|
11034
|
+
return state;
|
|
11035
|
+
}
|
|
11036
|
+
function reserveNextNumberingId(state, kind = "num") {
|
|
11037
|
+
const normalized = normalizeNumberingIdState(state);
|
|
11038
|
+
if (!normalized) return null;
|
|
11039
|
+
const useAbstract = kind === "abstract";
|
|
11040
|
+
const id = useAbstract ? normalized.nextAbstractNumId : normalized.nextNumId;
|
|
11041
|
+
if (!Number.isInteger(id) || id < 1) return null;
|
|
11042
|
+
if (useAbstract) {
|
|
11043
|
+
normalized.usedAbstractNumIds.add(id);
|
|
11044
|
+
normalized.nextAbstractNumId = nextAvailableId(id + 1, normalized.usedAbstractNumIds, normalized.maxPreferred);
|
|
11045
|
+
} else {
|
|
11046
|
+
normalized.usedNumIds.add(id);
|
|
11047
|
+
normalized.nextNumId = nextAvailableId(id + 1, normalized.usedNumIds, normalized.maxPreferred);
|
|
11018
11048
|
}
|
|
11019
|
-
|
|
11020
|
-
|
|
11021
|
-
|
|
11022
|
-
|
|
11023
|
-
|
|
11024
|
-
|
|
11025
|
-
|
|
11026
|
-
|
|
11027
|
-
|
|
11049
|
+
return id;
|
|
11050
|
+
}
|
|
11051
|
+
function reserveNextNumberingIdPair(state) {
|
|
11052
|
+
const numId = reserveNextNumberingId(state, "num");
|
|
11053
|
+
const abstractNumId = reserveNextNumberingId(state, "abstract");
|
|
11054
|
+
if (numId == null || abstractNumId == null) return null;
|
|
11055
|
+
return { numId, abstractNumId };
|
|
11056
|
+
}
|
|
11057
|
+
function hasXmlParseError(doc) {
|
|
11058
|
+
if (!doc || !doc.documentElement) return true;
|
|
11059
|
+
if (doc.documentElement.localName === "parsererror") return true;
|
|
11060
|
+
return doc.getElementsByTagName("parsererror").length > 0;
|
|
11061
|
+
}
|
|
11062
|
+
function isDirectWordChild(node, localName2) {
|
|
11063
|
+
return !!(node && node.nodeType === 1 && node.namespaceURI === WORD_MAIN_NS && node.localName === localName2);
|
|
11064
|
+
}
|
|
11065
|
+
function insertNumberingNodeInSchemaOrder(root, node, kind) {
|
|
11066
|
+
if (!root || !node) return;
|
|
11067
|
+
const directChildren = Array.from(root.childNodes || []).filter(
|
|
11068
|
+
(child) => child && child.nodeType === 1 && child.namespaceURI === WORD_MAIN_NS
|
|
11069
|
+
);
|
|
11070
|
+
let anchor = null;
|
|
11071
|
+
if (kind === "abstract") {
|
|
11072
|
+
anchor = directChildren.find(
|
|
11073
|
+
(child) => child.localName === "num" || child.localName === "numIdMacAtCleanup"
|
|
11074
|
+
) || null;
|
|
11075
|
+
} else {
|
|
11076
|
+
anchor = directChildren.find((child) => child.localName === "numIdMacAtCleanup") || null;
|
|
11028
11077
|
}
|
|
11029
|
-
|
|
11030
|
-
|
|
11031
|
-
|
|
11032
|
-
|
|
11033
|
-
|
|
11034
|
-
|
|
11035
|
-
|
|
11036
|
-
|
|
11037
|
-
|
|
11038
|
-
|
|
11039
|
-
|
|
11078
|
+
if (anchor) root.insertBefore(node, anchor);
|
|
11079
|
+
else root.appendChild(node);
|
|
11080
|
+
}
|
|
11081
|
+
function getAttributeFirst2(element, names) {
|
|
11082
|
+
for (const name of names || []) {
|
|
11083
|
+
const value = element?.getAttribute?.(name);
|
|
11084
|
+
if (value != null && value !== "") return value;
|
|
11085
|
+
}
|
|
11086
|
+
return null;
|
|
11087
|
+
}
|
|
11088
|
+
function getElementId2(element, names) {
|
|
11089
|
+
const raw = getAttributeFirst2(element, names);
|
|
11090
|
+
const parsed = Number.parseInt(String(raw || ""), 10);
|
|
11091
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
11092
|
+
}
|
|
11093
|
+
function setElementId(element, preferredName, idValue) {
|
|
11094
|
+
element?.setAttribute?.(preferredName, String(idValue));
|
|
11095
|
+
}
|
|
11096
|
+
function setElementVal2(element, value) {
|
|
11097
|
+
element?.setAttribute?.("w:val", String(value));
|
|
11098
|
+
}
|
|
11099
|
+
function overwriteParagraphNumIds(paragraphNodes, targetNumId) {
|
|
11100
|
+
if (!Array.isArray(paragraphNodes) || targetNumId == null) return;
|
|
11101
|
+
for (const node of paragraphNodes) {
|
|
11102
|
+
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []);
|
|
11103
|
+
for (const numIdNode of numIdNodes) {
|
|
11104
|
+
setElementVal2(numIdNode, targetNumId);
|
|
11040
11105
|
}
|
|
11041
11106
|
}
|
|
11042
|
-
|
|
11043
|
-
|
|
11044
|
-
|
|
11045
|
-
const
|
|
11046
|
-
|
|
11047
|
-
|
|
11048
|
-
|
|
11107
|
+
}
|
|
11108
|
+
function extractFirstParagraphNumId(paragraphNodes) {
|
|
11109
|
+
for (const node of paragraphNodes || []) {
|
|
11110
|
+
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []);
|
|
11111
|
+
for (const numIdNode of numIdNodes) {
|
|
11112
|
+
const numId = getElementId2(numIdNode, ["w:val", "val"]);
|
|
11113
|
+
if (numId != null) return String(numId);
|
|
11049
11114
|
}
|
|
11050
|
-
seenIds.add(id);
|
|
11051
11115
|
}
|
|
11052
|
-
|
|
11053
|
-
for (const node of textNodes) {
|
|
11054
|
-
const text2 = node.textContent || "";
|
|
11055
|
-
if (/^\s|\s$/.test(text2) && xmlSpaceAttribute(node) !== "preserve") {
|
|
11056
|
-
addIssue(
|
|
11057
|
-
"MISSING_SPACE_PRESERVE",
|
|
11058
|
-
"error",
|
|
11059
|
-
`<${node.nodeName}> has boundary whitespace without xml:space="preserve".`
|
|
11060
|
-
);
|
|
11061
|
-
}
|
|
11062
|
-
if (text2 === "") {
|
|
11063
|
-
addIssue("EMPTY_TEXT_ELEMENT", "warning", `<${node.nodeName}> is empty.`);
|
|
11064
|
-
}
|
|
11065
|
-
}
|
|
11066
|
-
for (const revision of revisions) {
|
|
11067
|
-
if (isParagraphMarkRevision(revision)) continue;
|
|
11068
|
-
const hasElementChild = Array.from(revision.childNodes || []).some((child) => child.nodeType === 1);
|
|
11069
|
-
if (!hasElementChild) {
|
|
11070
|
-
addIssue(
|
|
11071
|
-
"EMPTY_REVISION_WRAPPER",
|
|
11072
|
-
"warning",
|
|
11073
|
-
`<${revision.nodeName}> (w:id="${wordAttribute3(revision, "id")}") wraps no content.`
|
|
11074
|
-
);
|
|
11075
|
-
}
|
|
11076
|
-
}
|
|
11077
|
-
for (const resurrection of findForeignDeletedParagraphResurrections(doc)) {
|
|
11078
|
-
const ownerAuthor = resurrection.ownerAuthor || "unattributed";
|
|
11079
|
-
addIssue(
|
|
11080
|
-
"FOREIGN_PARAGRAPH_MARK_DELETION",
|
|
11081
|
-
"warning",
|
|
11082
|
-
`Paragraph deleted by ${ownerAuthor} also contains non-empty insertion content from another author; Accept/Reject lifecycle may not preserve the apparent restoration.`
|
|
11083
|
-
);
|
|
11084
|
-
}
|
|
11085
|
-
return { valid: !issues.some((issue) => issue.severity === "error"), issues };
|
|
11116
|
+
return null;
|
|
11086
11117
|
}
|
|
11087
|
-
|
|
11088
|
-
|
|
11089
|
-
|
|
11090
|
-
|
|
11091
|
-
|
|
11092
|
-
|
|
11093
|
-
|
|
11118
|
+
function buildExplicitDecimalMultilevelNumberingXml(numId, abstractNumId, startAt) {
|
|
11119
|
+
const safeNumId = String(numId);
|
|
11120
|
+
const safeAbstractNumId = String(abstractNumId);
|
|
11121
|
+
const safeStartAt = Number.isInteger(startAt) && startAt > 0 ? startAt : 1;
|
|
11122
|
+
const levelsXml = Array.from({ length: 9 }, (_, level) => {
|
|
11123
|
+
const lvlText = Array.from({ length: level + 1 }, (_2, i2) => `%${i2 + 1}`).join(".") + ".";
|
|
11124
|
+
const left = 720 * (level + 1);
|
|
11125
|
+
return `
|
|
11126
|
+
<w:lvl w:ilvl="${level}">
|
|
11127
|
+
<w:start w:val="1"/>
|
|
11128
|
+
<w:numFmt w:val="decimal"/>
|
|
11129
|
+
<w:lvlText w:val="${lvlText}"/>
|
|
11130
|
+
<w:lvlJc w:val="left"/>
|
|
11131
|
+
<w:pPr><w:ind w:left="${left}" w:hanging="360"/></w:pPr>
|
|
11132
|
+
</w:lvl>`;
|
|
11133
|
+
}).join("");
|
|
11134
|
+
return `
|
|
11135
|
+
<w:numbering xmlns:w="${WORD_MAIN_NS}">
|
|
11136
|
+
<w:abstractNum w:abstractNumId="${safeAbstractNumId}">
|
|
11137
|
+
<w:multiLevelType w:val="multilevel"/>
|
|
11138
|
+
${levelsXml}
|
|
11139
|
+
</w:abstractNum>
|
|
11140
|
+
<w:num w:numId="${safeNumId}">
|
|
11141
|
+
<w:abstractNumId w:val="${safeAbstractNumId}"/>
|
|
11142
|
+
<w:lvlOverride w:ilvl="0">
|
|
11143
|
+
<w:startOverride w:val="${safeStartAt}"/>
|
|
11144
|
+
</w:lvlOverride>
|
|
11145
|
+
</w:num>
|
|
11146
|
+
</w:numbering>`.trim();
|
|
11094
11147
|
}
|
|
11095
|
-
function
|
|
11096
|
-
|
|
11148
|
+
function isTrackedDeletedParagraph(node) {
|
|
11149
|
+
if (!node || node.nodeType !== 1 || node.localName !== "p") return false;
|
|
11150
|
+
const pPr = Array.from(node.childNodes || []).find((child) => isDirectWordChild(child, "pPr"));
|
|
11151
|
+
const rPr = pPr && Array.from(pPr.childNodes || []).find((child) => isDirectWordChild(child, "rPr"));
|
|
11152
|
+
return !!rPr && Array.from(rPr.childNodes || []).some((child) => isDirectWordChild(child, "del"));
|
|
11097
11153
|
}
|
|
11098
|
-
function
|
|
11099
|
-
|
|
11100
|
-
const matrix = rowElements.map((row) => {
|
|
11101
|
-
const cellElements = getDirectWordChildren(row, "tc");
|
|
11102
|
-
return cellElements.map((cell) => {
|
|
11103
|
-
const paragraphs = getDirectWordChildren(cell, "p");
|
|
11104
|
-
if (paragraphs.length === 0) return normalizeWhitespaceForTargeting(getParagraphText(cell));
|
|
11105
|
-
const lines = paragraphs.map((p) => normalizeWhitespaceForTargeting(getParagraphText(p))).filter(Boolean);
|
|
11106
|
-
return lines.join("\n");
|
|
11107
|
-
});
|
|
11108
|
-
});
|
|
11109
|
-
const columnCount = matrix.reduce((max2, row) => Math.max(max2, row.length), 0);
|
|
11110
|
-
matrix.forEach((row) => {
|
|
11111
|
-
while (row.length < columnCount) row.push("");
|
|
11112
|
-
});
|
|
11113
|
-
return { matrix, rowElements, columnCount };
|
|
11154
|
+
function paragraphNumIdNodes(node) {
|
|
11155
|
+
return Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []).filter((numIdNode) => numIdNode.parentNode?.localName === "numPr");
|
|
11114
11156
|
}
|
|
11115
|
-
function
|
|
11116
|
-
|
|
11117
|
-
const
|
|
11118
|
-
const
|
|
11119
|
-
|
|
11120
|
-
|
|
11121
|
-
}
|
|
11122
|
-
|
|
11123
|
-
const separator = new Array(columnCount).fill("---");
|
|
11124
|
-
const bodyRows = normalized.slice(1);
|
|
11125
|
-
const toLine = (row) => `| ${row.map((cell) => escapeMarkdownCell(cell)).join(" | ")} |`;
|
|
11126
|
-
return [toLine(header), toLine(separator), ...bodyRows.map(toLine)].join("\n");
|
|
11157
|
+
function levelFormats(abstractNum) {
|
|
11158
|
+
const formats = /* @__PURE__ */ new Map();
|
|
11159
|
+
for (const lvl of Array.from(abstractNum?.getElementsByTagNameNS?.("*", "lvl") || [])) {
|
|
11160
|
+
const ilvl = getElementId2(lvl, ["w:ilvl", "ilvl"]);
|
|
11161
|
+
const fmt = Array.from(lvl.getElementsByTagNameNS("*", "numFmt"))[0]?.getAttribute("w:val") || null;
|
|
11162
|
+
if (ilvl != null && fmt) formats.set(ilvl, fmt);
|
|
11163
|
+
}
|
|
11164
|
+
return formats;
|
|
11127
11165
|
}
|
|
11128
|
-
function
|
|
11129
|
-
|
|
11130
|
-
const normalized = rowValues.map((value) => normalizeWhitespaceForTargeting(value)).filter(Boolean);
|
|
11131
|
-
if (normalized.length < 2) return false;
|
|
11132
|
-
return normalized.every((value) => value === normalized[0]);
|
|
11166
|
+
function normalizeListFormat(format) {
|
|
11167
|
+
return format === "bullet" || format === "circle" || format === "square" ? "bullet" : format;
|
|
11133
11168
|
}
|
|
11134
|
-
function
|
|
11135
|
-
const
|
|
11136
|
-
|
|
11137
|
-
if (
|
|
11138
|
-
|
|
11139
|
-
if (/^\(.*\)$/.test(normalized)) return true;
|
|
11140
|
-
if (/:\s*$/.test(normalized)) return true;
|
|
11141
|
-
if (normalized.length <= 90 && !/[.!?]$/.test(normalized) && /[:\[\]()]/.test(normalized)) return true;
|
|
11142
|
-
if (/^[\[(]/.test(normalized)) return true;
|
|
11143
|
-
return false;
|
|
11169
|
+
function abstractNumForNumId(numberingDoc, numId) {
|
|
11170
|
+
const num = Array.from(numberingDoc.getElementsByTagNameNS("*", "num")).find((candidate) => getElementId2(candidate, ["w:numId", "numId"]) === numId);
|
|
11171
|
+
const abstractId = num && getElementId2(Array.from(num.getElementsByTagNameNS("*", "abstractNumId"))[0], ["w:val", "val"]);
|
|
11172
|
+
if (abstractId == null) return null;
|
|
11173
|
+
return Array.from(numberingDoc.getElementsByTagNameNS("*", "abstractNum")).find((candidate) => getElementId2(candidate, ["w:abstractNumId", "abstractNumId"]) === abstractId) || null;
|
|
11144
11174
|
}
|
|
11145
|
-
function
|
|
11146
|
-
const
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
const
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
scanned += 1;
|
|
11154
|
-
const nextCursor = cursor.nextSibling;
|
|
11155
|
-
if (cursor.nodeType !== 1 || cursor.namespaceURI !== WORD_MAIN_NS || cursor.localName !== "p") {
|
|
11156
|
-
cursor = nextCursor;
|
|
11157
|
-
continue;
|
|
11175
|
+
function reuseSourceListNumbering(numberingXml, replacementNodes, sourceParagraphs, numberingIdState) {
|
|
11176
|
+
const sourceXml = numberingIdState?.sourceNumberingXml;
|
|
11177
|
+
if (!numberingXml || !String(sourceXml || "").trim() || !Array.isArray(replacementNodes)) return null;
|
|
11178
|
+
const sourceNumIds = /* @__PURE__ */ new Set();
|
|
11179
|
+
for (const paragraph of sourceParagraphs || []) {
|
|
11180
|
+
for (const numIdNode of paragraphNumIdNodes(paragraph)) {
|
|
11181
|
+
const id = getElementId2(numIdNode, ["w:val", "val"]);
|
|
11182
|
+
if (id != null && id !== 0) sourceNumIds.add(id);
|
|
11158
11183
|
}
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11184
|
+
}
|
|
11185
|
+
if (sourceNumIds.size !== 1) return null;
|
|
11186
|
+
const [sourceNumId] = sourceNumIds;
|
|
11187
|
+
const sourceDoc = parseOoxml(sourceXml);
|
|
11188
|
+
const payloadDoc = parseOoxml(numberingXml);
|
|
11189
|
+
if (hasXmlParseError(sourceDoc) || hasXmlParseError(payloadDoc)) return null;
|
|
11190
|
+
const sourceAbstract = abstractNumForNumId(sourceDoc, sourceNumId);
|
|
11191
|
+
if (!sourceAbstract) return null;
|
|
11192
|
+
const sourceFormats = levelFormats(sourceAbstract);
|
|
11193
|
+
const newParagraphNumIds = [];
|
|
11194
|
+
for (const node of replacementNodes) {
|
|
11195
|
+
if (isTrackedDeletedParagraph(node)) continue;
|
|
11196
|
+
for (const numIdNode of paragraphNumIdNodes(node)) {
|
|
11197
|
+
const ilvlNode = Array.from(numIdNode.parentNode.childNodes || []).find((child) => child.localName === "ilvl");
|
|
11198
|
+
newParagraphNumIds.push({
|
|
11199
|
+
numId: getElementId2(numIdNode, ["w:val", "val"]),
|
|
11200
|
+
ilvl: ilvlNode ? getElementId2(ilvlNode, ["w:val", "val"]) ?? 0 : 0
|
|
11201
|
+
});
|
|
11164
11202
|
}
|
|
11165
|
-
if (!isLikelyStructuredTableSourceParagraph(text2)) break;
|
|
11166
|
-
block.push(cursor);
|
|
11167
|
-
cursor = nextCursor;
|
|
11168
11203
|
}
|
|
11169
|
-
|
|
11204
|
+
if (newParagraphNumIds.length === 0) return null;
|
|
11205
|
+
const payloadFormats = /* @__PURE__ */ new Map();
|
|
11206
|
+
for (const { numId, ilvl } of newParagraphNumIds) {
|
|
11207
|
+
if (numId == null) return null;
|
|
11208
|
+
if (!payloadFormats.has(numId)) {
|
|
11209
|
+
const abstract = abstractNumForNumId(payloadDoc, numId);
|
|
11210
|
+
if (!abstract) return null;
|
|
11211
|
+
payloadFormats.set(numId, levelFormats(abstract));
|
|
11212
|
+
}
|
|
11213
|
+
const generated = payloadFormats.get(numId).get(ilvl);
|
|
11214
|
+
const existing = sourceFormats.get(ilvl);
|
|
11215
|
+
if (!generated || !existing || normalizeListFormat(generated) !== normalizeListFormat(existing)) return null;
|
|
11216
|
+
}
|
|
11217
|
+
const clonedNodes = replacementNodes.map((node) => node?.cloneNode ? node.cloneNode(true) : node);
|
|
11218
|
+
for (const node of clonedNodes) {
|
|
11219
|
+
if (isTrackedDeletedParagraph(node)) continue;
|
|
11220
|
+
for (const numIdNode of paragraphNumIdNodes(node)) setElementVal2(numIdNode, sourceNumId);
|
|
11221
|
+
}
|
|
11222
|
+
return { numId: sourceNumId, replacementNodes: clonedNodes };
|
|
11170
11223
|
}
|
|
11171
|
-
function
|
|
11172
|
-
const
|
|
11173
|
-
|
|
11174
|
-
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
|
|
11178
|
-
if (isMarkdownTableText(rawModified)) return null;
|
|
11179
|
-
const lines = rawModified.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean);
|
|
11180
|
-
if (lines.length < 2) return null;
|
|
11181
|
-
const tableElement = options.tableElement || findContainingWordElement(targetParagraph, "tbl");
|
|
11182
|
-
const rowElement = findContainingWordElement(targetParagraph, "tr");
|
|
11183
|
-
const cellElement = findContainingWordElement(targetParagraph, "tc");
|
|
11184
|
-
if (!tableElement || !rowElement || !cellElement) return null;
|
|
11185
|
-
const currentParagraphText = normalizeWhitespaceForTargeting(
|
|
11186
|
-
options.currentParagraphText || getParagraphText(targetParagraph)
|
|
11187
|
-
);
|
|
11188
|
-
const firstLine = normalizeWhitespaceForTargeting(lines[0]);
|
|
11189
|
-
if (currentParagraphText && firstLine && firstLine !== currentParagraphText) {
|
|
11190
|
-
onWarn("[Table] Multiline cell text did not anchor to original cell text; skipping table-row synthesis heuristic.");
|
|
11191
|
-
return null;
|
|
11224
|
+
function remapNumberingPayloadForDocument(numberingXml, replacementNodes, numberingIdState) {
|
|
11225
|
+
const numberingDoc = parseOoxml(numberingXml);
|
|
11226
|
+
if (hasXmlParseError(numberingDoc)) {
|
|
11227
|
+
return {
|
|
11228
|
+
numberingXml: String(numberingXml || ""),
|
|
11229
|
+
replacementNodes: Array.isArray(replacementNodes) ? replacementNodes.map((node) => node?.cloneNode ? node.cloneNode(true) : node) : []
|
|
11230
|
+
};
|
|
11192
11231
|
}
|
|
11193
|
-
const
|
|
11194
|
-
|
|
11195
|
-
const
|
|
11196
|
-
const
|
|
11197
|
-
|
|
11198
|
-
|
|
11199
|
-
|
|
11200
|
-
|
|
11201
|
-
|
|
11202
|
-
|
|
11232
|
+
const abstractNumMap = /* @__PURE__ */ new Map();
|
|
11233
|
+
const numIdMap = /* @__PURE__ */ new Map();
|
|
11234
|
+
const abstractNums = Array.from(numberingDoc.getElementsByTagNameNS("*", "abstractNum"));
|
|
11235
|
+
for (const abstractNum of abstractNums) {
|
|
11236
|
+
const oldId = getElementId2(abstractNum, ["w:abstractNumId", "abstractNumId"]);
|
|
11237
|
+
if (oldId == null) continue;
|
|
11238
|
+
const newId = reserveNextNumberingId(numberingIdState, "abstract");
|
|
11239
|
+
if (newId == null) continue;
|
|
11240
|
+
abstractNumMap.set(oldId, newId);
|
|
11241
|
+
setElementId(abstractNum, "w:abstractNumId", newId);
|
|
11203
11242
|
}
|
|
11204
|
-
|
|
11205
|
-
|
|
11206
|
-
const
|
|
11207
|
-
if (
|
|
11208
|
-
|
|
11209
|
-
|
|
11210
|
-
|
|
11211
|
-
|
|
11212
|
-
|
|
11213
|
-
|
|
11214
|
-
|
|
11215
|
-
|
|
11216
|
-
|
|
11217
|
-
} else {
|
|
11218
|
-
const newRow = new Array(columnCount).fill("");
|
|
11219
|
-
if (mirrorAcrossColumns) {
|
|
11220
|
-
for (let col = 0; col < columnCount; col++) newRow[col] = extraValue;
|
|
11221
|
-
} else {
|
|
11222
|
-
newRow[colIndex] = extraValue;
|
|
11243
|
+
const nums = Array.from(numberingDoc.getElementsByTagNameNS("*", "num"));
|
|
11244
|
+
for (const num of nums) {
|
|
11245
|
+
const oldNumId = getElementId2(num, ["w:numId", "numId"]);
|
|
11246
|
+
if (oldNumId == null) continue;
|
|
11247
|
+
const newNumId = reserveNextNumberingId(numberingIdState, "num");
|
|
11248
|
+
if (newNumId == null) continue;
|
|
11249
|
+
numIdMap.set(oldNumId, newNumId);
|
|
11250
|
+
setElementId(num, "w:numId", newNumId);
|
|
11251
|
+
const abstractNumIdNode = Array.from(num.getElementsByTagNameNS("*", "abstractNumId"))[0] || null;
|
|
11252
|
+
if (abstractNumIdNode) {
|
|
11253
|
+
const oldAbsRef = getElementId2(abstractNumIdNode, ["w:val", "val"]);
|
|
11254
|
+
if (oldAbsRef != null && abstractNumMap.has(oldAbsRef)) {
|
|
11255
|
+
setElementVal2(abstractNumIdNode, abstractNumMap.get(oldAbsRef));
|
|
11223
11256
|
}
|
|
11224
|
-
matrix.splice(Math.min(insertIndex, matrix.length), 0, newRow);
|
|
11225
11257
|
}
|
|
11226
11258
|
}
|
|
11227
|
-
const
|
|
11228
|
-
|
|
11229
|
-
|
|
11230
|
-
|
|
11231
|
-
|
|
11232
|
-
|
|
11233
|
-
|
|
11234
|
-
|
|
11235
|
-
|
|
11236
|
-
|
|
11237
|
-
const raw = element.getAttribute(name);
|
|
11238
|
-
if (raw == null || raw === "") continue;
|
|
11239
|
-
const parsed = Number.parseInt(String(raw), 10);
|
|
11240
|
-
if (Number.isFinite(parsed)) return parsed;
|
|
11259
|
+
const clonedNodes = Array.isArray(replacementNodes) ? replacementNodes.map((node) => node?.cloneNode ? node.cloneNode(true) : node) : [];
|
|
11260
|
+
for (const node of clonedNodes) {
|
|
11261
|
+
if (isTrackedDeletedParagraph(node)) continue;
|
|
11262
|
+
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []);
|
|
11263
|
+
for (const numIdNode of numIdNodes) {
|
|
11264
|
+
const oldNumRef = getElementId2(numIdNode, ["w:val", "val"]);
|
|
11265
|
+
if (oldNumRef != null && numIdMap.has(oldNumRef)) {
|
|
11266
|
+
setElementVal2(numIdNode, numIdMap.get(oldNumRef));
|
|
11267
|
+
}
|
|
11268
|
+
}
|
|
11241
11269
|
}
|
|
11242
|
-
return
|
|
11270
|
+
return {
|
|
11271
|
+
numberingXml: serializeOoxml(numberingDoc),
|
|
11272
|
+
replacementNodes: clonedNodes
|
|
11273
|
+
};
|
|
11243
11274
|
}
|
|
11244
|
-
function
|
|
11245
|
-
|
|
11246
|
-
const
|
|
11247
|
-
|
|
11248
|
-
|
|
11249
|
-
|
|
11250
|
-
|
|
11251
|
-
|
|
11252
|
-
|
|
11275
|
+
function mergeNumberingXmlBySchemaOrder(existingNumberingXml, incomingNumberingXml) {
|
|
11276
|
+
const existingText = String(existingNumberingXml || "");
|
|
11277
|
+
const incomingText = String(incomingNumberingXml || "");
|
|
11278
|
+
if (!incomingText.trim()) return existingText;
|
|
11279
|
+
if (!existingText.trim()) return incomingText;
|
|
11280
|
+
try {
|
|
11281
|
+
const existingDoc = parseOoxml(existingText);
|
|
11282
|
+
const incomingDoc = parseOoxml(incomingText);
|
|
11283
|
+
if (hasXmlParseError(existingDoc) || hasXmlParseError(incomingDoc)) {
|
|
11284
|
+
return existingText;
|
|
11253
11285
|
}
|
|
11286
|
+
const existingRoot = existingDoc.documentElement;
|
|
11287
|
+
const incomingRoot = incomingDoc.documentElement;
|
|
11288
|
+
if (!existingRoot || !incomingRoot) return existingText;
|
|
11289
|
+
const existingAbstractIds = new Set(
|
|
11290
|
+
Array.from(existingRoot.childNodes || []).filter((node) => isDirectWordChild(node, "abstractNum")).map((node) => parseIntegerAttribute(node, ["w:abstractNumId", "abstractNumId"])).filter((id) => id != null)
|
|
11291
|
+
);
|
|
11292
|
+
const existingNumIds = new Set(
|
|
11293
|
+
Array.from(existingRoot.childNodes || []).filter((node) => isDirectWordChild(node, "num")).map((node) => parseIntegerAttribute(node, ["w:numId", "numId"])).filter((id) => id != null)
|
|
11294
|
+
);
|
|
11295
|
+
for (const incomingNode of Array.from(incomingRoot.childNodes || [])) {
|
|
11296
|
+
if (!isDirectWordChild(incomingNode, "abstractNum")) continue;
|
|
11297
|
+
const incomingId = parseIntegerAttribute(incomingNode, ["w:abstractNumId", "abstractNumId"]);
|
|
11298
|
+
if (incomingId == null || existingAbstractIds.has(incomingId)) continue;
|
|
11299
|
+
insertNumberingNodeInSchemaOrder(existingRoot, existingDoc.importNode(incomingNode, true), "abstract");
|
|
11300
|
+
existingAbstractIds.add(incomingId);
|
|
11301
|
+
}
|
|
11302
|
+
for (const incomingNode of Array.from(incomingRoot.childNodes || [])) {
|
|
11303
|
+
if (!isDirectWordChild(incomingNode, "num")) continue;
|
|
11304
|
+
const incomingId = parseIntegerAttribute(incomingNode, ["w:numId", "numId"]);
|
|
11305
|
+
if (incomingId == null || existingNumIds.has(incomingId)) continue;
|
|
11306
|
+
insertNumberingNodeInSchemaOrder(existingRoot, existingDoc.importNode(incomingNode, true), "num");
|
|
11307
|
+
existingNumIds.add(incomingId);
|
|
11308
|
+
}
|
|
11309
|
+
return serializeOoxml(existingDoc);
|
|
11310
|
+
} catch {
|
|
11311
|
+
return existingText;
|
|
11254
11312
|
}
|
|
11255
|
-
return candidate;
|
|
11256
11313
|
}
|
|
11257
|
-
|
|
11258
|
-
|
|
11259
|
-
|
|
11260
|
-
|
|
11261
|
-
|
|
11262
|
-
|
|
11314
|
+
|
|
11315
|
+
// core/redline-validation.js
|
|
11316
|
+
var REVISION_ID_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "rPrChange", "pPrChange"]);
|
|
11317
|
+
var REVISION_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}T/;
|
|
11318
|
+
function localNameOf3(node) {
|
|
11319
|
+
return String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
|
|
11320
|
+
}
|
|
11321
|
+
function elementsByLocalName(root, name) {
|
|
11322
|
+
return Array.from(root.getElementsByTagName("*")).filter((el) => localNameOf3(el) === name);
|
|
11323
|
+
}
|
|
11324
|
+
function wordAttribute3(node, name) {
|
|
11325
|
+
return node.getAttribute(`w:${name}`) || node.getAttribute(name) || "";
|
|
11326
|
+
}
|
|
11327
|
+
function xmlSpaceAttribute(node) {
|
|
11328
|
+
return node.getAttribute("xml:space") || node.getAttribute("space") || node.getAttributeNS?.("http://www.w3.org/XML/1998/namespace", "space") || "";
|
|
11329
|
+
}
|
|
11330
|
+
function isParagraphMarkRevision(node) {
|
|
11331
|
+
return localNameOf3(node.parentNode) === "rPr";
|
|
11332
|
+
}
|
|
11333
|
+
function parseOoxmlForValidation(oxml) {
|
|
11334
|
+
const attempt = (xml) => {
|
|
11335
|
+
const doc = parseXml(xml);
|
|
11336
|
+
const parseError = doc.getElementsByTagName("parsererror")[0];
|
|
11337
|
+
if (parseError) {
|
|
11338
|
+
throw new Error(parseError.textContent || "XML parse error");
|
|
11339
|
+
}
|
|
11340
|
+
return doc;
|
|
11341
|
+
};
|
|
11342
|
+
try {
|
|
11343
|
+
return { doc: attempt(oxml) };
|
|
11344
|
+
} catch {
|
|
11263
11345
|
try {
|
|
11264
|
-
|
|
11265
|
-
|
|
11266
|
-
|
|
11267
|
-
for (const abstractNum of abstractNums) {
|
|
11268
|
-
const id = parseIntegerAttribute(abstractNum, ["w:abstractNumId", "abstractNumId"]);
|
|
11269
|
-
if (id != null) usedAbstractNumIds.add(id);
|
|
11270
|
-
}
|
|
11271
|
-
for (const num of nums) {
|
|
11272
|
-
const id = parseIntegerAttribute(num, ["w:numId", "numId"]);
|
|
11273
|
-
if (id != null) usedNumIds.add(id);
|
|
11274
|
-
}
|
|
11275
|
-
} catch {
|
|
11346
|
+
return { doc: attempt(`<w:root xmlns:w="${NS_W}">${oxml}</w:root>`) };
|
|
11347
|
+
} catch (error2) {
|
|
11348
|
+
return { error: error2?.message || "XML parse error" };
|
|
11276
11349
|
}
|
|
11277
11350
|
}
|
|
11278
|
-
const maxUsedNumId = usedNumIds.size > 0 ? Math.max(...usedNumIds) : 0;
|
|
11279
|
-
const maxUsedAbstractNumId = usedAbstractNumIds.size > 0 ? Math.max(...usedAbstractNumIds) : 0;
|
|
11280
|
-
const baseNumId = Math.max(minId, maxUsedNumId + 1);
|
|
11281
|
-
const baseAbstractNumId = Math.max(minId, maxUsedAbstractNumId + 1);
|
|
11282
|
-
return {
|
|
11283
|
-
// Kept so list edits can recognise which existing definition a source list uses and continue it.
|
|
11284
|
-
sourceNumberingXml: String(numberingXml || ""),
|
|
11285
|
-
nextNumId: nextAvailableId(baseNumId, usedNumIds, maxPreferred),
|
|
11286
|
-
nextAbstractNumId: nextAvailableId(baseAbstractNumId, usedAbstractNumIds, maxPreferred),
|
|
11287
|
-
usedNumIds,
|
|
11288
|
-
usedAbstractNumIds,
|
|
11289
|
-
minId,
|
|
11290
|
-
maxPreferred
|
|
11291
|
-
};
|
|
11292
11351
|
}
|
|
11293
|
-
function
|
|
11294
|
-
|
|
11295
|
-
|
|
11296
|
-
|
|
11297
|
-
if (!
|
|
11298
|
-
|
|
11352
|
+
function validateRedlineOoxml(oxml) {
|
|
11353
|
+
const issues = [];
|
|
11354
|
+
const addIssue = (code, severity, message) => issues.push({ code, severity, message });
|
|
11355
|
+
const isDomNode = oxml && typeof oxml === "object" && (oxml.nodeType === 9 || oxml.nodeType === 1);
|
|
11356
|
+
if (!isDomNode && (typeof oxml !== "string" || oxml.trim() === "")) {
|
|
11357
|
+
addIssue("PARSE_ERROR", "error", "Input is not non-empty OOXML or a parsed XML DOM.");
|
|
11358
|
+
return { valid: false, issues };
|
|
11299
11359
|
}
|
|
11300
|
-
|
|
11301
|
-
|
|
11360
|
+
const { doc, error: error2 } = isDomNode ? { doc: oxml.nodeType === 9 ? oxml : oxml.ownerDocument } : parseOoxmlForValidation(oxml);
|
|
11361
|
+
if (!doc) {
|
|
11362
|
+
addIssue("PARSE_ERROR", "error", `OOXML does not parse as XML: ${error2}`);
|
|
11363
|
+
return { valid: false, issues };
|
|
11302
11364
|
}
|
|
11303
|
-
|
|
11304
|
-
|
|
11365
|
+
const insElements = elementsByLocalName(doc, "ins");
|
|
11366
|
+
const delElements = elementsByLocalName(doc, "del");
|
|
11367
|
+
const revisions = insElements.concat(delElements);
|
|
11368
|
+
for (const paragraph of elementsByLocalName(doc, "p")) {
|
|
11369
|
+
const nested = Array.from(paragraph.getElementsByTagName("*")).find((el) => el !== paragraph && localNameOf3(el) === "p");
|
|
11370
|
+
if (nested) {
|
|
11371
|
+
addIssue("NESTED_PARAGRAPH", "error", `<${paragraph.nodeName}> contains nested <${nested.nodeName}>.`);
|
|
11372
|
+
}
|
|
11305
11373
|
}
|
|
11306
|
-
|
|
11307
|
-
|
|
11374
|
+
for (const body of elementsByLocalName(doc, "body")) {
|
|
11375
|
+
const children = Array.from(body.childNodes || []).filter((child) => child.nodeType === 1);
|
|
11376
|
+
const sectPrIndexes = children.map((child, index) => localNameOf3(child) === "sectPr" ? index : -1).filter((index) => index >= 0);
|
|
11377
|
+
if (sectPrIndexes.length > 1) {
|
|
11378
|
+
addIssue("MULTIPLE_BODY_SECTPR", "error", "<w:body> contains multiple direct <w:sectPr> elements.");
|
|
11379
|
+
} else if (sectPrIndexes.length === 1 && sectPrIndexes[0] !== children.length - 1) {
|
|
11380
|
+
addIssue("SECTPR_NOT_LAST", "error", "<w:sectPr> is not the last element child of <w:body>.");
|
|
11381
|
+
}
|
|
11308
11382
|
}
|
|
11309
|
-
|
|
11310
|
-
|
|
11311
|
-
|
|
11312
|
-
|
|
11313
|
-
|
|
11314
|
-
|
|
11315
|
-
|
|
11316
|
-
|
|
11317
|
-
|
|
11318
|
-
|
|
11319
|
-
|
|
11320
|
-
|
|
11321
|
-
|
|
11322
|
-
} else {
|
|
11323
|
-
normalized.usedNumIds.add(id);
|
|
11324
|
-
normalized.nextNumId = nextAvailableId(id + 1, normalized.usedNumIds, normalized.maxPreferred);
|
|
11383
|
+
for (const revision of revisions) {
|
|
11384
|
+
const nested = Array.from(revision.getElementsByTagName("*")).filter((el) => el !== revision && ["ins", "del"].includes(localNameOf3(el)));
|
|
11385
|
+
const invalidNested = nested.find((candidate) => {
|
|
11386
|
+
if (localNameOf3(revision) !== "ins" || localNameOf3(candidate) !== "del") return true;
|
|
11387
|
+
return candidate.parentNode !== revision;
|
|
11388
|
+
});
|
|
11389
|
+
if (invalidNested) {
|
|
11390
|
+
addIssue(
|
|
11391
|
+
"NESTED_REVISION",
|
|
11392
|
+
"error",
|
|
11393
|
+
`<${revision.nodeName}> (w:id="${wordAttribute3(revision, "id")}") contains invalid nested <${invalidNested.nodeName}>.`
|
|
11394
|
+
);
|
|
11395
|
+
}
|
|
11325
11396
|
}
|
|
11326
|
-
|
|
11327
|
-
|
|
11328
|
-
|
|
11329
|
-
|
|
11330
|
-
|
|
11331
|
-
|
|
11332
|
-
|
|
11333
|
-
|
|
11334
|
-
|
|
11335
|
-
if (!doc || !doc.documentElement) return true;
|
|
11336
|
-
if (doc.documentElement.localName === "parsererror") return true;
|
|
11337
|
-
return doc.getElementsByTagName("parsererror").length > 0;
|
|
11338
|
-
}
|
|
11339
|
-
function isDirectWordChild(node, localName2) {
|
|
11340
|
-
return !!(node && node.nodeType === 1 && node.namespaceURI === WORD_MAIN_NS && node.localName === localName2);
|
|
11341
|
-
}
|
|
11342
|
-
function insertNumberingNodeInSchemaOrder(root, node, kind) {
|
|
11343
|
-
if (!root || !node) return;
|
|
11344
|
-
const directChildren = Array.from(root.childNodes || []).filter(
|
|
11345
|
-
(child) => child && child.nodeType === 1 && child.namespaceURI === WORD_MAIN_NS
|
|
11346
|
-
);
|
|
11347
|
-
let anchor = null;
|
|
11348
|
-
if (kind === "abstract") {
|
|
11349
|
-
anchor = directChildren.find(
|
|
11350
|
-
(child) => child.localName === "num" || child.localName === "numIdMacAtCleanup"
|
|
11351
|
-
) || null;
|
|
11352
|
-
} else {
|
|
11353
|
-
anchor = directChildren.find((child) => child.localName === "numIdMacAtCleanup") || null;
|
|
11397
|
+
for (const del of delElements) {
|
|
11398
|
+
const plainTextNodes = elementsByLocalName(del, "t");
|
|
11399
|
+
if (plainTextNodes.length > 0) {
|
|
11400
|
+
addIssue(
|
|
11401
|
+
"DEL_CONTAINS_T",
|
|
11402
|
+
"error",
|
|
11403
|
+
`<w:del> (w:id="${wordAttribute3(del, "id")}") contains <w:t>; deleted text must use <w:delText>.`
|
|
11404
|
+
);
|
|
11405
|
+
}
|
|
11354
11406
|
}
|
|
11355
|
-
|
|
11356
|
-
|
|
11357
|
-
|
|
11358
|
-
|
|
11359
|
-
|
|
11360
|
-
|
|
11361
|
-
|
|
11407
|
+
for (const revision of revisions) {
|
|
11408
|
+
const missing = [];
|
|
11409
|
+
if (!wordAttribute3(revision, "id")) missing.push("w:id");
|
|
11410
|
+
if (!wordAttribute3(revision, "author")) missing.push("w:author");
|
|
11411
|
+
if (!REVISION_DATE_PATTERN.test(wordAttribute3(revision, "date"))) missing.push("w:date");
|
|
11412
|
+
if (missing.length > 0) {
|
|
11413
|
+
addIssue(
|
|
11414
|
+
"MISSING_REVISION_METADATA",
|
|
11415
|
+
"error",
|
|
11416
|
+
`<${revision.nodeName}> is missing or has malformed ${missing.join(", ")}.`
|
|
11417
|
+
);
|
|
11418
|
+
}
|
|
11362
11419
|
}
|
|
11363
|
-
|
|
11364
|
-
|
|
11365
|
-
|
|
11366
|
-
|
|
11367
|
-
|
|
11368
|
-
|
|
11369
|
-
}
|
|
11370
|
-
function setElementId(element, preferredName, idValue) {
|
|
11371
|
-
element?.setAttribute?.(preferredName, String(idValue));
|
|
11372
|
-
}
|
|
11373
|
-
function setElementVal2(element, value) {
|
|
11374
|
-
element?.setAttribute?.("w:val", String(value));
|
|
11375
|
-
}
|
|
11376
|
-
function overwriteParagraphNumIds(paragraphNodes, targetNumId) {
|
|
11377
|
-
if (!Array.isArray(paragraphNodes) || targetNumId == null) return;
|
|
11378
|
-
for (const node of paragraphNodes) {
|
|
11379
|
-
const numIdNodes = Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []);
|
|
11380
|
-
for (const numIdNode of numIdNodes) {
|
|
11381
|
-
setElementVal2(numIdNode, targetNumId);
|
|
11420
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
11421
|
+
for (const node of Array.from(doc.getElementsByTagName("*"))) {
|
|
11422
|
+
if (!REVISION_ID_ELEMENTS.has(localNameOf3(node))) continue;
|
|
11423
|
+
const id = wordAttribute3(node, "id");
|
|
11424
|
+
if (!id) continue;
|
|
11425
|
+
if (seenIds.has(id)) {
|
|
11426
|
+
addIssue("DUPLICATE_REVISION_ID", "error", `Revision id ${id} appears more than once.`);
|
|
11382
11427
|
}
|
|
11428
|
+
seenIds.add(id);
|
|
11383
11429
|
}
|
|
11384
|
-
|
|
11385
|
-
|
|
11386
|
-
|
|
11387
|
-
|
|
11388
|
-
|
|
11389
|
-
|
|
11390
|
-
|
|
11430
|
+
const textNodes = elementsByLocalName(doc, "t").concat(elementsByLocalName(doc, "delText"));
|
|
11431
|
+
for (const node of textNodes) {
|
|
11432
|
+
const text2 = node.textContent || "";
|
|
11433
|
+
if (/^\s|\s$/.test(text2) && xmlSpaceAttribute(node) !== "preserve") {
|
|
11434
|
+
addIssue(
|
|
11435
|
+
"MISSING_SPACE_PRESERVE",
|
|
11436
|
+
"error",
|
|
11437
|
+
`<${node.nodeName}> has boundary whitespace without xml:space="preserve".`
|
|
11438
|
+
);
|
|
11439
|
+
}
|
|
11440
|
+
if (text2 === "") {
|
|
11441
|
+
addIssue("EMPTY_TEXT_ELEMENT", "warning", `<${node.nodeName}> is empty.`);
|
|
11391
11442
|
}
|
|
11392
11443
|
}
|
|
11393
|
-
|
|
11394
|
-
|
|
11395
|
-
|
|
11396
|
-
|
|
11397
|
-
|
|
11398
|
-
|
|
11399
|
-
|
|
11400
|
-
|
|
11401
|
-
|
|
11402
|
-
|
|
11403
|
-
<w:lvl w:ilvl="${level}">
|
|
11404
|
-
<w:start w:val="1"/>
|
|
11405
|
-
<w:numFmt w:val="decimal"/>
|
|
11406
|
-
<w:lvlText w:val="${lvlText}"/>
|
|
11407
|
-
<w:lvlJc w:val="left"/>
|
|
11408
|
-
<w:pPr><w:ind w:left="${left}" w:hanging="360"/></w:pPr>
|
|
11409
|
-
</w:lvl>`;
|
|
11410
|
-
}).join("");
|
|
11411
|
-
return `
|
|
11412
|
-
<w:numbering xmlns:w="${WORD_MAIN_NS}">
|
|
11413
|
-
<w:abstractNum w:abstractNumId="${safeAbstractNumId}">
|
|
11414
|
-
<w:multiLevelType w:val="multilevel"/>
|
|
11415
|
-
${levelsXml}
|
|
11416
|
-
</w:abstractNum>
|
|
11417
|
-
<w:num w:numId="${safeNumId}">
|
|
11418
|
-
<w:abstractNumId w:val="${safeAbstractNumId}"/>
|
|
11419
|
-
<w:lvlOverride w:ilvl="0">
|
|
11420
|
-
<w:startOverride w:val="${safeStartAt}"/>
|
|
11421
|
-
</w:lvlOverride>
|
|
11422
|
-
</w:num>
|
|
11423
|
-
</w:numbering>`.trim();
|
|
11424
|
-
}
|
|
11425
|
-
function isTrackedDeletedParagraph(node) {
|
|
11426
|
-
if (!node || node.nodeType !== 1 || node.localName !== "p") return false;
|
|
11427
|
-
const pPr = Array.from(node.childNodes || []).find((child) => isDirectWordChild(child, "pPr"));
|
|
11428
|
-
const rPr = pPr && Array.from(pPr.childNodes || []).find((child) => isDirectWordChild(child, "rPr"));
|
|
11429
|
-
return !!rPr && Array.from(rPr.childNodes || []).some((child) => isDirectWordChild(child, "del"));
|
|
11430
|
-
}
|
|
11431
|
-
function paragraphNumIdNodes(node) {
|
|
11432
|
-
return Array.from(node?.getElementsByTagNameNS?.("*", "numId") || []).filter((numIdNode) => numIdNode.parentNode?.localName === "numPr");
|
|
11433
|
-
}
|
|
11434
|
-
function levelFormats(abstractNum) {
|
|
11435
|
-
const formats = /* @__PURE__ */ new Map();
|
|
11436
|
-
for (const lvl of Array.from(abstractNum?.getElementsByTagNameNS?.("*", "lvl") || [])) {
|
|
11437
|
-
const ilvl = getElementId2(lvl, ["w:ilvl", "ilvl"]);
|
|
11438
|
-
const fmt = Array.from(lvl.getElementsByTagNameNS("*", "numFmt"))[0]?.getAttribute("w:val") || null;
|
|
11439
|
-
if (ilvl != null && fmt) formats.set(ilvl, fmt);
|
|
11444
|
+
for (const revision of revisions) {
|
|
11445
|
+
if (isParagraphMarkRevision(revision)) continue;
|
|
11446
|
+
const hasElementChild = Array.from(revision.childNodes || []).some((child) => child.nodeType === 1);
|
|
11447
|
+
if (!hasElementChild) {
|
|
11448
|
+
addIssue(
|
|
11449
|
+
"EMPTY_REVISION_WRAPPER",
|
|
11450
|
+
"warning",
|
|
11451
|
+
`<${revision.nodeName}> (w:id="${wordAttribute3(revision, "id")}") wraps no content.`
|
|
11452
|
+
);
|
|
11453
|
+
}
|
|
11440
11454
|
}
|
|
11441
|
-
|
|
11455
|
+
for (const resurrection of findForeignDeletedParagraphResurrections(doc)) {
|
|
11456
|
+
const ownerAuthor = resurrection.ownerAuthor || "unattributed";
|
|
11457
|
+
addIssue(
|
|
11458
|
+
"FOREIGN_PARAGRAPH_MARK_DELETION",
|
|
11459
|
+
"warning",
|
|
11460
|
+
`Paragraph deleted by ${ownerAuthor} also contains non-empty insertion content from another author; Accept/Reject lifecycle may not preserve the apparent restoration.`
|
|
11461
|
+
);
|
|
11462
|
+
}
|
|
11463
|
+
return { valid: !issues.some((issue) => issue.severity === "error"), issues };
|
|
11442
11464
|
}
|
|
11443
|
-
|
|
11444
|
-
|
|
11465
|
+
|
|
11466
|
+
// core/table-targeting.js
|
|
11467
|
+
function getDirectWordChildren(element, localName2) {
|
|
11468
|
+
if (!element) return [];
|
|
11469
|
+
return Array.from(element.childNodes || []).filter(
|
|
11470
|
+
(node) => node && node.nodeType === 1 && node.namespaceURI === WORD_MAIN_NS && node.localName === localName2
|
|
11471
|
+
);
|
|
11445
11472
|
}
|
|
11446
|
-
function
|
|
11447
|
-
|
|
11448
|
-
const abstractId = num && getElementId2(Array.from(num.getElementsByTagNameNS("*", "abstractNumId"))[0], ["w:val", "val"]);
|
|
11449
|
-
if (abstractId == null) return null;
|
|
11450
|
-
return Array.from(numberingDoc.getElementsByTagNameNS("*", "abstractNum")).find((candidate) => getElementId2(candidate, ["w:abstractNumId", "abstractNumId"]) === abstractId) || null;
|
|
11473
|
+
function escapeMarkdownCell(text2) {
|
|
11474
|
+
return String(text2 || "").replace(/\|/g, "\\|").replace(/\r?\n/g, "<br>");
|
|
11451
11475
|
}
|
|
11452
|
-
function
|
|
11453
|
-
const
|
|
11454
|
-
|
|
11455
|
-
|
|
11456
|
-
|
|
11457
|
-
|
|
11458
|
-
|
|
11459
|
-
|
|
11460
|
-
|
|
11461
|
-
|
|
11462
|
-
|
|
11463
|
-
const
|
|
11464
|
-
|
|
11465
|
-
|
|
11466
|
-
|
|
11467
|
-
|
|
11468
|
-
|
|
11469
|
-
|
|
11470
|
-
|
|
11471
|
-
|
|
11472
|
-
|
|
11473
|
-
|
|
11474
|
-
|
|
11475
|
-
|
|
11476
|
-
|
|
11477
|
-
|
|
11478
|
-
|
|
11476
|
+
function extractTableMatrix(tableElement) {
|
|
11477
|
+
const rowElements = getDirectWordChildren(tableElement, "tr");
|
|
11478
|
+
const matrix = rowElements.map((row) => {
|
|
11479
|
+
const cellElements = getDirectWordChildren(row, "tc");
|
|
11480
|
+
return cellElements.map((cell) => {
|
|
11481
|
+
const paragraphs = getDirectWordChildren(cell, "p");
|
|
11482
|
+
if (paragraphs.length === 0) return normalizeWhitespaceForTargeting(getParagraphText(cell));
|
|
11483
|
+
const lines = paragraphs.map((p) => normalizeWhitespaceForTargeting(getParagraphText(p))).filter(Boolean);
|
|
11484
|
+
return lines.join("\n");
|
|
11485
|
+
});
|
|
11486
|
+
});
|
|
11487
|
+
const columnCount = matrix.reduce((max2, row) => Math.max(max2, row.length), 0);
|
|
11488
|
+
matrix.forEach((row) => {
|
|
11489
|
+
while (row.length < columnCount) row.push("");
|
|
11490
|
+
});
|
|
11491
|
+
return { matrix, rowElements, columnCount };
|
|
11492
|
+
}
|
|
11493
|
+
function tableMatrixToMarkdown(matrix, columnCount) {
|
|
11494
|
+
if (!Array.isArray(matrix) || matrix.length === 0 || columnCount <= 0) return null;
|
|
11495
|
+
const normalized = matrix.map((row) => {
|
|
11496
|
+
const copy = Array.isArray(row) ? row.slice(0, columnCount) : [];
|
|
11497
|
+
while (copy.length < columnCount) copy.push("");
|
|
11498
|
+
return copy;
|
|
11499
|
+
});
|
|
11500
|
+
const header = normalized[0];
|
|
11501
|
+
const separator = new Array(columnCount).fill("---");
|
|
11502
|
+
const bodyRows = normalized.slice(1);
|
|
11503
|
+
const toLine = (row) => `| ${row.map((cell) => escapeMarkdownCell(cell)).join(" | ")} |`;
|
|
11504
|
+
return [toLine(header), toLine(separator), ...bodyRows.map(toLine)].join("\n");
|
|
11505
|
+
}
|
|
11506
|
+
function isSymmetricLabelRow(rowValues) {
|
|
11507
|
+
if (!Array.isArray(rowValues) || rowValues.length < 2) return false;
|
|
11508
|
+
const normalized = rowValues.map((value) => normalizeWhitespaceForTargeting(value)).filter(Boolean);
|
|
11509
|
+
if (normalized.length < 2) return false;
|
|
11510
|
+
return normalized.every((value) => value === normalized[0]);
|
|
11511
|
+
}
|
|
11512
|
+
function isLikelyStructuredTableSourceParagraph(text2) {
|
|
11513
|
+
const normalized = String(text2 || "").trim();
|
|
11514
|
+
if (!normalized) return false;
|
|
11515
|
+
if (/^and$/i.test(normalized)) return true;
|
|
11516
|
+
if (/^\[.*\]$/.test(normalized)) return true;
|
|
11517
|
+
if (/^\(.*\)$/.test(normalized)) return true;
|
|
11518
|
+
if (/:\s*$/.test(normalized)) return true;
|
|
11519
|
+
if (normalized.length <= 90 && !/[.!?]$/.test(normalized) && /[:\[\]()]/.test(normalized)) return true;
|
|
11520
|
+
if (/^[\[(]/.test(normalized)) return true;
|
|
11521
|
+
return false;
|
|
11522
|
+
}
|
|
11523
|
+
function inferTableReplacementParagraphBlock(startParagraph, options = {}) {
|
|
11524
|
+
const maxScan = Number.isInteger(options?.maxScan) && options.maxScan > 0 ? options.maxScan : 10;
|
|
11525
|
+
const paragraphTextGetter = typeof options?.getParagraphText === "function" ? options.getParagraphText : getParagraphText;
|
|
11526
|
+
if (!startParagraph || !startParagraph.parentNode) return null;
|
|
11527
|
+
const block = [startParagraph];
|
|
11528
|
+
let cursor = startParagraph.nextSibling;
|
|
11529
|
+
let scanned = 0;
|
|
11530
|
+
while (cursor && scanned < maxScan) {
|
|
11531
|
+
scanned += 1;
|
|
11532
|
+
const nextCursor = cursor.nextSibling;
|
|
11533
|
+
if (cursor.nodeType !== 1 || cursor.namespaceURI !== WORD_MAIN_NS || cursor.localName !== "p") {
|
|
11534
|
+
cursor = nextCursor;
|
|
11535
|
+
continue;
|
|
11479
11536
|
}
|
|
11480
|
-
|
|
11481
|
-
|
|
11482
|
-
|
|
11483
|
-
|
|
11484
|
-
|
|
11485
|
-
if (!payloadFormats.has(numId)) {
|
|
11486
|
-
const abstract = abstractNumForNumId(payloadDoc, numId);
|
|
11487
|
-
if (!abstract) return null;
|
|
11488
|
-
payloadFormats.set(numId, levelFormats(abstract));
|
|
11537
|
+
const text2 = String(paragraphTextGetter(cursor) || "").trim();
|
|
11538
|
+
if (!text2) {
|
|
11539
|
+
if (block.length > 1) break;
|
|
11540
|
+
cursor = nextCursor;
|
|
11541
|
+
continue;
|
|
11489
11542
|
}
|
|
11490
|
-
|
|
11491
|
-
|
|
11492
|
-
|
|
11493
|
-
}
|
|
11494
|
-
const clonedNodes = replacementNodes.map((node) => node?.cloneNode ? node.cloneNode(true) : node);
|
|
11495
|
-
for (const node of clonedNodes) {
|
|
11496
|
-
if (isTrackedDeletedParagraph(node)) continue;
|
|
11497
|
-
for (const numIdNode of paragraphNumIdNodes(node)) setElementVal2(numIdNode, sourceNumId);
|
|
11543
|
+
if (!isLikelyStructuredTableSourceParagraph(text2)) break;
|
|
11544
|
+
block.push(cursor);
|
|
11545
|
+
cursor = nextCursor;
|
|
11498
11546
|
}
|
|
11499
|
-
return
|
|
11547
|
+
return block.length > 1 ? block : null;
|
|
11500
11548
|
}
|
|
11501
|
-
function
|
|
11502
|
-
const
|
|
11503
|
-
|
|
11504
|
-
|
|
11505
|
-
|
|
11506
|
-
|
|
11507
|
-
|
|
11549
|
+
function synthesizeTableMarkdownFromMultilineCellEdit(targetParagraph, modifiedText, options = {}) {
|
|
11550
|
+
const onInfo = typeof options.onInfo === "function" ? options.onInfo : () => {
|
|
11551
|
+
};
|
|
11552
|
+
const onWarn = typeof options.onWarn === "function" ? options.onWarn : () => {
|
|
11553
|
+
};
|
|
11554
|
+
const rawModified = String(modifiedText || "");
|
|
11555
|
+
if (!rawModified.includes("\n")) return null;
|
|
11556
|
+
if (isMarkdownTableText(rawModified)) return null;
|
|
11557
|
+
const lines = rawModified.split(/\r?\n/g).map((line) => line.trim()).filter(Boolean);
|
|
11558
|
+
if (lines.length < 2) return null;
|
|
11559
|
+
const tableElement = options.tableElement || findContainingWordElement(targetParagraph, "tbl");
|
|
11560
|
+
const rowElement = findContainingWordElement(targetParagraph, "tr");
|
|
11561
|
+
const cellElement = findContainingWordElement(targetParagraph, "tc");
|
|
11562
|
+
if (!tableElement || !rowElement || !cellElement) return null;
|
|
11563
|
+
const currentParagraphText = normalizeWhitespaceForTargeting(
|
|
11564
|
+
options.currentParagraphText || getParagraphText(targetParagraph)
|
|
11565
|
+
);
|
|
11566
|
+
const firstLine = normalizeWhitespaceForTargeting(lines[0]);
|
|
11567
|
+
if (currentParagraphText && firstLine && firstLine !== currentParagraphText) {
|
|
11568
|
+
onWarn("[Table] Multiline cell text did not anchor to original cell text; skipping table-row synthesis heuristic.");
|
|
11569
|
+
return null;
|
|
11508
11570
|
}
|
|
11509
|
-
const
|
|
11510
|
-
|
|
11511
|
-
const
|
|
11512
|
-
|
|
11513
|
-
|
|
11514
|
-
|
|
11515
|
-
|
|
11516
|
-
|
|
11517
|
-
|
|
11518
|
-
|
|
11571
|
+
const { matrix, rowElements, columnCount } = extractTableMatrix(tableElement);
|
|
11572
|
+
if (matrix.length === 0 || columnCount === 0) return null;
|
|
11573
|
+
const rowIndex = rowElements.indexOf(rowElement);
|
|
11574
|
+
const cellElements = getDirectWordChildren(rowElement, "tc");
|
|
11575
|
+
const colIndex = cellElements.indexOf(cellElement);
|
|
11576
|
+
if (rowIndex < 0 || colIndex < 0 || colIndex >= columnCount) return null;
|
|
11577
|
+
matrix[rowIndex][colIndex] = lines[0];
|
|
11578
|
+
const mirrorAcrossColumns = isSymmetricLabelRow(matrix[rowIndex]);
|
|
11579
|
+
if (mirrorAcrossColumns) {
|
|
11580
|
+
onInfo("[Table] Symmetric row detected; mirroring inserted row values across columns.");
|
|
11519
11581
|
}
|
|
11520
|
-
|
|
11521
|
-
|
|
11522
|
-
const
|
|
11523
|
-
if (
|
|
11524
|
-
|
|
11525
|
-
|
|
11526
|
-
|
|
11527
|
-
|
|
11528
|
-
|
|
11529
|
-
|
|
11530
|
-
|
|
11531
|
-
|
|
11532
|
-
setElementVal2(abstractNumIdNode, abstractNumMap.get(oldAbsRef));
|
|
11582
|
+
for (let i2 = 1; i2 < lines.length; i2++) {
|
|
11583
|
+
const insertIndex = rowIndex + i2;
|
|
11584
|
+
const extraValue = lines[i2];
|
|
11585
|
+
if (insertIndex < matrix.length && !normalizeWhitespaceForTargeting(matrix[insertIndex][colIndex])) {
|
|
11586
|
+
if (mirrorAcrossColumns) {
|
|
11587
|
+
for (let col = 0; col < columnCount; col++) {
|
|
11588
|
+
if (!normalizeWhitespaceForTargeting(matrix[insertIndex][col])) {
|
|
11589
|
+
matrix[insertIndex][col] = extraValue;
|
|
11590
|
+
}
|
|
11591
|
+
}
|
|
11592
|
+
} else {
|
|
11593
|
+
matrix[insertIndex][colIndex] = extraValue;
|
|
11533
11594
|
}
|
|
11534
|
-
}
|
|
11535
|
-
|
|
11536
|
-
|
|
11537
|
-
|
|
11538
|
-
|
|
11539
|
-
|
|
11540
|
-
for (const numIdNode of numIdNodes) {
|
|
11541
|
-
const oldNumRef = getElementId2(numIdNode, ["w:val", "val"]);
|
|
11542
|
-
if (oldNumRef != null && numIdMap.has(oldNumRef)) {
|
|
11543
|
-
setElementVal2(numIdNode, numIdMap.get(oldNumRef));
|
|
11595
|
+
} else {
|
|
11596
|
+
const newRow = new Array(columnCount).fill("");
|
|
11597
|
+
if (mirrorAcrossColumns) {
|
|
11598
|
+
for (let col = 0; col < columnCount; col++) newRow[col] = extraValue;
|
|
11599
|
+
} else {
|
|
11600
|
+
newRow[colIndex] = extraValue;
|
|
11544
11601
|
}
|
|
11602
|
+
matrix.splice(Math.min(insertIndex, matrix.length), 0, newRow);
|
|
11545
11603
|
}
|
|
11546
11604
|
}
|
|
11547
|
-
|
|
11548
|
-
|
|
11549
|
-
|
|
11550
|
-
|
|
11551
|
-
}
|
|
11552
|
-
function mergeNumberingXmlBySchemaOrder(existingNumberingXml, incomingNumberingXml) {
|
|
11553
|
-
const existingText = String(existingNumberingXml || "");
|
|
11554
|
-
const incomingText = String(incomingNumberingXml || "");
|
|
11555
|
-
if (!incomingText.trim()) return existingText;
|
|
11556
|
-
if (!existingText.trim()) return incomingText;
|
|
11557
|
-
try {
|
|
11558
|
-
const existingDoc = parseOoxml(existingText);
|
|
11559
|
-
const incomingDoc = parseOoxml(incomingText);
|
|
11560
|
-
if (hasXmlParseError(existingDoc) || hasXmlParseError(incomingDoc)) {
|
|
11561
|
-
return existingText;
|
|
11562
|
-
}
|
|
11563
|
-
const existingRoot = existingDoc.documentElement;
|
|
11564
|
-
const incomingRoot = incomingDoc.documentElement;
|
|
11565
|
-
if (!existingRoot || !incomingRoot) return existingText;
|
|
11566
|
-
const existingAbstractIds = new Set(
|
|
11567
|
-
Array.from(existingRoot.childNodes || []).filter((node) => isDirectWordChild(node, "abstractNum")).map((node) => parseIntegerAttribute(node, ["w:abstractNumId", "abstractNumId"])).filter((id) => id != null)
|
|
11568
|
-
);
|
|
11569
|
-
const existingNumIds = new Set(
|
|
11570
|
-
Array.from(existingRoot.childNodes || []).filter((node) => isDirectWordChild(node, "num")).map((node) => parseIntegerAttribute(node, ["w:numId", "numId"])).filter((id) => id != null)
|
|
11571
|
-
);
|
|
11572
|
-
for (const incomingNode of Array.from(incomingRoot.childNodes || [])) {
|
|
11573
|
-
if (!isDirectWordChild(incomingNode, "abstractNum")) continue;
|
|
11574
|
-
const incomingId = parseIntegerAttribute(incomingNode, ["w:abstractNumId", "abstractNumId"]);
|
|
11575
|
-
if (incomingId == null || existingAbstractIds.has(incomingId)) continue;
|
|
11576
|
-
insertNumberingNodeInSchemaOrder(existingRoot, existingDoc.importNode(incomingNode, true), "abstract");
|
|
11577
|
-
existingAbstractIds.add(incomingId);
|
|
11578
|
-
}
|
|
11579
|
-
for (const incomingNode of Array.from(incomingRoot.childNodes || [])) {
|
|
11580
|
-
if (!isDirectWordChild(incomingNode, "num")) continue;
|
|
11581
|
-
const incomingId = parseIntegerAttribute(incomingNode, ["w:numId", "numId"]);
|
|
11582
|
-
if (incomingId == null || existingNumIds.has(incomingId)) continue;
|
|
11583
|
-
insertNumberingNodeInSchemaOrder(existingRoot, existingDoc.importNode(incomingNode, true), "num");
|
|
11584
|
-
existingNumIds.add(incomingId);
|
|
11585
|
-
}
|
|
11586
|
-
return serializeOoxml(existingDoc);
|
|
11587
|
-
} catch {
|
|
11588
|
-
return existingText;
|
|
11589
|
-
}
|
|
11605
|
+
const markdown = tableMatrixToMarkdown(matrix, columnCount);
|
|
11606
|
+
if (!markdown) return null;
|
|
11607
|
+
onInfo("[Table] Synthesized full markdown table from multiline cell edit for table-scope reconciliation.");
|
|
11608
|
+
return markdown;
|
|
11590
11609
|
}
|
|
11591
11610
|
|
|
11592
11611
|
// pipeline/ingestion-export.js
|
|
@@ -15236,7 +15255,7 @@ function validateDocumentOperation(operation) {
|
|
|
15236
15255
|
}
|
|
15237
15256
|
};
|
|
15238
15257
|
}
|
|
15239
|
-
if (target.revisionView !== "accepted" || target.captureRef || target.occurrence != null || normalized.captureKey || normalized.targetEndDescriptor || normalized.targetEndRef != null
|
|
15258
|
+
if (target.revisionView !== "accepted" || target.captureRef || target.occurrence != null || normalized.captureKey || normalized.targetEndDescriptor || normalized.targetEndRef != null) {
|
|
15240
15259
|
return {
|
|
15241
15260
|
valid: false,
|
|
15242
15261
|
error: {
|
|
@@ -15258,6 +15277,15 @@ function validateDocumentOperation(operation) {
|
|
|
15258
15277
|
}
|
|
15259
15278
|
};
|
|
15260
15279
|
}
|
|
15280
|
+
if (replacementValidation.replacements.some((replacement) => /\r|\n/.test(replacement.find))) {
|
|
15281
|
+
return {
|
|
15282
|
+
valid: false,
|
|
15283
|
+
error: {
|
|
15284
|
+
code: "INVALID_OPERATION",
|
|
15285
|
+
message: 'Localized replacements v1 cannot span a line break; use one replacement per line or a full "modified" paragraph.'
|
|
15286
|
+
}
|
|
15287
|
+
};
|
|
15288
|
+
}
|
|
15261
15289
|
normalized.replacements = replacementValidation.replacements;
|
|
15262
15290
|
}
|
|
15263
15291
|
}
|
|
@@ -15452,6 +15480,12 @@ function validateDocumentOperation(operation) {
|
|
|
15452
15480
|
error: { code: "INVALID_OPERATION", message: 'Format operations require a non-empty "textToFormat" field.' }
|
|
15453
15481
|
};
|
|
15454
15482
|
}
|
|
15483
|
+
if (normalized.textOccurrence != null && (!Number.isInteger(normalized.textOccurrence) || normalized.textOccurrence < 1)) {
|
|
15484
|
+
return {
|
|
15485
|
+
valid: false,
|
|
15486
|
+
error: { code: "INVALID_OPERATION", message: "textOccurrence must be a positive integer when provided." }
|
|
15487
|
+
};
|
|
15488
|
+
}
|
|
15455
15489
|
if (!isRecord(normalized.properties)) {
|
|
15456
15490
|
return {
|
|
15457
15491
|
valid: false,
|
|
@@ -15908,7 +15942,7 @@ function compileOperationBatch(xmlDoc, operations = [], options = {}) {
|
|
|
15908
15942
|
};
|
|
15909
15943
|
const compiled = { ...sourceOperation };
|
|
15910
15944
|
const dynamicOccurrence = operation.targetDescriptor?.occurrence != null && !operation.targetDescriptor?.paragraphId && !operation.targetDescriptor?.fingerprint;
|
|
15911
|
-
const compositeTextTarget = typeof operation.targetDescriptor?.text === "string" && /\r|\n/.test(operation.targetDescriptor.text) && !operation.targetEndDescriptor;
|
|
15945
|
+
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));
|
|
15912
15946
|
if (dynamicOccurrence || compositeTextTarget) binding.dynamic = true;
|
|
15913
15947
|
if (validation.valid && operation.operationKind !== "comment_reply" && operation.operationKind !== "comment_resolve" && !operation.targetDescriptor?.captureRef && !binding.dynamic) {
|
|
15914
15948
|
try {
|
|
@@ -16246,6 +16280,17 @@ function deriveSingleParagraphPlainAdjacencyInsertion(currentParagraphText, modi
|
|
|
16246
16280
|
}
|
|
16247
16281
|
return null;
|
|
16248
16282
|
}
|
|
16283
|
+
function deriveParagraphThenTableAppend(modifiedText) {
|
|
16284
|
+
const lines = String(modifiedText || "").split(/\r?\n/g).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0);
|
|
16285
|
+
if (lines.length < 3) return null;
|
|
16286
|
+
const tableStart = lines.findIndex((line) => line.trim().startsWith("|"));
|
|
16287
|
+
if (tableStart !== 1) return null;
|
|
16288
|
+
const tableLines = lines.slice(1);
|
|
16289
|
+
if (!tableLines.every((line) => line.trim().startsWith("|"))) return null;
|
|
16290
|
+
const separator = tableLines[1].replace(/\s+/g, "");
|
|
16291
|
+
if (!/^\|:?-{3,}:?(\|:?-{3,}:?)+\|?$/.test(separator)) return null;
|
|
16292
|
+
return { paragraphMarkdown: lines[0].trim(), tableText: tableLines.join("\n") };
|
|
16293
|
+
}
|
|
16249
16294
|
|
|
16250
16295
|
// services/capture-engine.js
|
|
16251
16296
|
function createCaptureError(code, message) {
|
|
@@ -18031,6 +18076,81 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
18031
18076
|
numberingXml: null
|
|
18032
18077
|
};
|
|
18033
18078
|
}
|
|
18079
|
+
const tableAppendCandidate = !useTableScope && !hasExplicitRangeScope && !targetListInfo && !containingTable && !targetEndRef ? deriveParagraphThenTableAppend(effectiveModifiedText) : null;
|
|
18080
|
+
const tableAppendParagraph = tableAppendCandidate ? preprocessMarkdown(tableAppendCandidate.paragraphMarkdown) : null;
|
|
18081
|
+
if (tableAppendCandidate && normalizeWhitespaceForTargeting(tableAppendParagraph.cleanText) === normalizeWhitespaceForTargeting(currentParagraphText)) {
|
|
18082
|
+
const tableData = parseTable(tableAppendCandidate.tableText);
|
|
18083
|
+
if (tableData.headers.length > 0 || tableData.rows.length > 0) {
|
|
18084
|
+
onInfo("[Table] Applying retained-paragraph table append heuristic.");
|
|
18085
|
+
const parent2 = targetParagraph.parentNode;
|
|
18086
|
+
if (!parent2) throw new Error("Target paragraph has no parent for table append");
|
|
18087
|
+
const followingNode = targetParagraph.nextSibling;
|
|
18088
|
+
if (tableAppendParagraph.formatHints.length > 0) {
|
|
18089
|
+
const formatted = await applyRedlineToOxml2(
|
|
18090
|
+
serializer.serializeToString(targetParagraph),
|
|
18091
|
+
currentParagraphText || targetText,
|
|
18092
|
+
tableAppendCandidate.paragraphMarkdown,
|
|
18093
|
+
{
|
|
18094
|
+
author,
|
|
18095
|
+
generateRedlines,
|
|
18096
|
+
existingRevisions: options.existingRevisions || "merge-same-author",
|
|
18097
|
+
structuredContent: false,
|
|
18098
|
+
_revisionIdAllocator: revisionIdAllocator
|
|
18099
|
+
}
|
|
18100
|
+
);
|
|
18101
|
+
if (formatted?.status === "error") {
|
|
18102
|
+
return {
|
|
18103
|
+
documentXml,
|
|
18104
|
+
hasChanges: false,
|
|
18105
|
+
numberingXml: null,
|
|
18106
|
+
status: "error",
|
|
18107
|
+
error: formatted.error
|
|
18108
|
+
};
|
|
18109
|
+
}
|
|
18110
|
+
if (formatted?.hasChanges) {
|
|
18111
|
+
if (typeof formatted.oxml !== "string") {
|
|
18112
|
+
throw new Error("Reconciliation engine did not return OOXML for a changed paragraph format");
|
|
18113
|
+
}
|
|
18114
|
+
for (const node of extractReplacementNodes(formatted.oxml).replacementNodes) {
|
|
18115
|
+
const imported = xmlDoc.importNode(node, true);
|
|
18116
|
+
ensureParagraphIdsOnImportedNode(imported, operationSession);
|
|
18117
|
+
options?._mutationLiveNodes?.push(parent2.insertBefore(imported, targetParagraph));
|
|
18118
|
+
}
|
|
18119
|
+
options?._mutationRemovedNodes?.push(targetParagraph);
|
|
18120
|
+
parent2.removeChild(targetParagraph);
|
|
18121
|
+
}
|
|
18122
|
+
}
|
|
18123
|
+
const tableOoxml = generateTableOoxml(tableData, {
|
|
18124
|
+
generateRedlines,
|
|
18125
|
+
author,
|
|
18126
|
+
revisionIdAllocator,
|
|
18127
|
+
trackAsBlock: true
|
|
18128
|
+
});
|
|
18129
|
+
for (const node of extractReplacementNodes(tableOoxml).replacementNodes) {
|
|
18130
|
+
options?._mutationLiveNodes?.push(parent2.insertBefore(xmlDoc.importNode(node, true), followingNode));
|
|
18131
|
+
}
|
|
18132
|
+
const endsContainer = !followingNode || followingNode.nodeType === 1 && followingNode.namespaceURI === NS_W9 && followingNode.localName === "sectPr";
|
|
18133
|
+
if (endsContainer) {
|
|
18134
|
+
const trailing = createWordElement(xmlDoc, "w:p");
|
|
18135
|
+
const trailingPPr = clonePropertiesWithoutRevisionHistory(getDirectWordChild4(targetParagraph, "pPr"));
|
|
18136
|
+
if (trailingPPr) {
|
|
18137
|
+
for (const child of Array.from(trailingPPr.childNodes)) {
|
|
18138
|
+
if (child.nodeType === 1 && ["sectPr", "numPr"].includes(child.localName)) trailingPPr.removeChild(child);
|
|
18139
|
+
}
|
|
18140
|
+
trailing.appendChild(trailingPPr);
|
|
18141
|
+
}
|
|
18142
|
+
if (generateRedlines) markParagraphMarkInserted(xmlDoc, trailing, author);
|
|
18143
|
+
ensureParagraphIdsOnImportedNode(trailing, operationSession);
|
|
18144
|
+
options?._mutationLiveNodes?.push(parent2.insertBefore(trailing, followingNode));
|
|
18145
|
+
}
|
|
18146
|
+
normalizeBodySectionOrder(xmlDoc);
|
|
18147
|
+
return {
|
|
18148
|
+
documentXml: completedDocumentXml(xmlDoc, serializer, documentXml, operationSession),
|
|
18149
|
+
hasChanges: true,
|
|
18150
|
+
numberingXml: null
|
|
18151
|
+
};
|
|
18152
|
+
}
|
|
18153
|
+
}
|
|
18034
18154
|
const plainAdjacencyInsertionCandidate = !bypassSingleParagraphHeuristics && !useTableScope && !hasExplicitRangeScope && !targetListInfo ? deriveSingleParagraphPlainAdjacencyInsertion(currentParagraphText, effectiveModifiedText) : null;
|
|
18035
18155
|
if (plainAdjacencyInsertionCandidate) {
|
|
18036
18156
|
onInfo(
|
|
@@ -18156,6 +18276,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
18156
18276
|
explicitStructuredContent: options.explicitStructuredContent === true,
|
|
18157
18277
|
pairReplacements: options.pairReplacements === true,
|
|
18158
18278
|
insertionAffinity: options.insertionAffinity || null,
|
|
18279
|
+
literalTextEdit: options.literalTextEdit === true,
|
|
18159
18280
|
_revisionIdAllocator: revisionIdAllocator,
|
|
18160
18281
|
_isolatedTableCell: useTableScope
|
|
18161
18282
|
});
|
|
@@ -18443,7 +18564,8 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
|
|
|
18443
18564
|
spanRanges.push({ ...entry, start, end });
|
|
18444
18565
|
}
|
|
18445
18566
|
const targetDescriptor = options.targetDescriptor;
|
|
18446
|
-
const
|
|
18567
|
+
const explicitTextOccurrence = Number.isInteger(options.textOccurrence) && options.textOccurrence > 0 ? options.textOccurrence : null;
|
|
18568
|
+
const occurrence = explicitTextOccurrence ?? targetDescriptor?.occurrence ?? null;
|
|
18447
18569
|
let matchStart = -1;
|
|
18448
18570
|
if (occurrence != null && occurrence > 0) {
|
|
18449
18571
|
let count = 0;
|
|
@@ -18461,6 +18583,17 @@ async function applyFormattingToParagraphByExactText(documentXml, targetText, te
|
|
|
18461
18583
|
} else {
|
|
18462
18584
|
matchStart = fullParaText.indexOf(textToFormat);
|
|
18463
18585
|
}
|
|
18586
|
+
if (matchStart === -1 && explicitTextOccurrence != null && fullParaText.includes(textToFormat)) {
|
|
18587
|
+
return {
|
|
18588
|
+
documentXml,
|
|
18589
|
+
hasChanges: false,
|
|
18590
|
+
status: "error",
|
|
18591
|
+
error: {
|
|
18592
|
+
code: "PATCH_SOURCE_NOT_FOUND",
|
|
18593
|
+
message: `Occurrence ${explicitTextOccurrence} of text to format was not found in target paragraph: "${textToFormat}".`
|
|
18594
|
+
}
|
|
18595
|
+
};
|
|
18596
|
+
}
|
|
18464
18597
|
if (matchStart === -1) {
|
|
18465
18598
|
return {
|
|
18466
18599
|
documentXml,
|
|
@@ -18743,9 +18876,11 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
|
|
|
18743
18876
|
...operation.existingRevisions ? { existingRevisions: operation.existingRevisions } : {},
|
|
18744
18877
|
structuredContent: typeof operation.structuredContent === "boolean" ? operation.structuredContent : options.structuredContent !== false,
|
|
18745
18878
|
explicitStructuredContent: operation.structuredContent === true,
|
|
18879
|
+
...operation._localizedReplacementCompilation && operation.structuredContent !== true ? { literalTextEdit: true } : {},
|
|
18746
18880
|
pairReplacements: typeof operation.pairReplacements === "boolean" ? operation.pairReplacements : options.pairReplacements !== false,
|
|
18747
18881
|
...operation.insertionAffinity ? { insertionAffinity: operation.insertionAffinity } : {},
|
|
18748
18882
|
...operation.formattingRevisionPolicy ? { formattingRevisionPolicy: operation.formattingRevisionPolicy } : {},
|
|
18883
|
+
...operation.operationKind === "format" && Number.isInteger(operation.textOccurrence) ? { textOccurrence: operation.textOccurrence } : {},
|
|
18749
18884
|
targetDescriptor: operation.targetDescriptor,
|
|
18750
18885
|
targetEndDescriptor: operation.targetEndDescriptor,
|
|
18751
18886
|
_compiledSourceId: operation._compiledSourceId || null,
|
|
@@ -19369,6 +19504,9 @@ async function applyOperationsToDocumentXml(documentXml, operations, author, run
|
|
|
19369
19504
|
const scheduled = dependencyPlan.scheduled;
|
|
19370
19505
|
const atomic = options.atomic === true;
|
|
19371
19506
|
const continueOnError = options.continueOnError !== false;
|
|
19507
|
+
if (runtimeContext && typeof runtimeContext === "object" && !runtimeContext.numberingIdState && typeof runtimeContext.numberingXml === "string" && runtimeContext.numberingXml.trim()) {
|
|
19508
|
+
runtimeContext.numberingIdState = createDynamicNumberingIdState(runtimeContext.numberingXml);
|
|
19509
|
+
}
|
|
19372
19510
|
const context = cloneBatchRuntimeContext(runtimeContext);
|
|
19373
19511
|
if (!(context.targetRefSnapshot instanceof Map)) {
|
|
19374
19512
|
context.targetRefSnapshot = session.initialTargetReferenceSnapshot;
|
|
@@ -21794,8 +21932,32 @@ function openDocx(input) {
|
|
|
21794
21932
|
}
|
|
21795
21933
|
|
|
21796
21934
|
// index.js
|
|
21935
|
+
function remapGeneratedNumberingForSource(result, options) {
|
|
21936
|
+
const sourceNumbering = typeof options?.numberingXml === "string" && options.numberingXml.trim() ? options.numberingXml : null;
|
|
21937
|
+
const state = options?.numberingIdState && typeof options.numberingIdState === "object" ? options.numberingIdState : sourceNumbering ? createDynamicNumberingIdState(sourceNumbering) : null;
|
|
21938
|
+
if (!state || !result?.hasChanges || typeof result.oxml !== "string" || !result.oxml.includes("/word/numbering.xml")) {
|
|
21939
|
+
return result;
|
|
21940
|
+
}
|
|
21941
|
+
const pkgDoc = parseOoxml(result.oxml);
|
|
21942
|
+
const parts = Array.from(pkgDoc.getElementsByTagNameNS("*", "part"));
|
|
21943
|
+
const payloadOf = (name) => {
|
|
21944
|
+
const part = parts.find((candidate) => (candidate.getAttribute("pkg:name") || candidate.getAttribute("name")) === name);
|
|
21945
|
+
const xmlData = part?.getElementsByTagNameNS("*", "xmlData")[0];
|
|
21946
|
+
return xmlData ? Array.from(xmlData.childNodes).find((node) => node.nodeType === 1) || null : null;
|
|
21947
|
+
};
|
|
21948
|
+
const numberingNode = payloadOf("/word/numbering.xml");
|
|
21949
|
+
const body = payloadOf("/word/document.xml")?.getElementsByTagNameNS("*", "body")[0];
|
|
21950
|
+
if (!numberingNode || !body) return result;
|
|
21951
|
+
const bodyNodes = Array.from(body.childNodes).filter((node) => node.nodeType === 1 && node.localName !== "sectPr");
|
|
21952
|
+
const remapped = remapNumberingPayloadForDocument(serializeOoxml(numberingNode), bodyNodes, state);
|
|
21953
|
+
const newNumbering = pkgDoc.importNode(parseOoxml(remapped.numberingXml).documentElement, true);
|
|
21954
|
+
numberingNode.parentNode.replaceChild(newNumbering, numberingNode);
|
|
21955
|
+
bodyNodes.forEach((node, index) => body.replaceChild(remapped.replacementNodes[index], node));
|
|
21956
|
+
return { ...result, oxml: serializeOoxml(pkgDoc) };
|
|
21957
|
+
}
|
|
21797
21958
|
async function applyRedlineToOxml3(oxml, originalText, modifiedText, options = {}) {
|
|
21798
|
-
const
|
|
21959
|
+
const rawResult = await applyRedlineToOxml(oxml, originalText, modifiedText, options);
|
|
21960
|
+
const result = remapGeneratedNumberingForSource(rawResult, options);
|
|
21799
21961
|
if (result?.useNativeApi && typeof result?.oxml !== "string") {
|
|
21800
21962
|
const existingWarnings = Array.isArray(result?.warnings) ? result.warnings : [];
|
|
21801
21963
|
return withOoxmlSourceType({
|