@ansonlai/docx-redline-js 0.4.0 → 0.5.0
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/AGENTS.md +589 -287
- package/ARCHITECTURE.md +215 -9
- package/CHANGELOG.md +319 -0
- package/README.md +604 -360
- package/adapters/config.js +45 -43
- package/bin/docx-redline.js +3 -0
- package/core/list-targeting.js +101 -110
- package/core/paragraph-targeting.js +501 -61
- package/core/paragraph-text.js +209 -0
- package/core/revision-cloning.js +38 -0
- package/core/types.js +64 -10
- package/core/word-xml.js +43 -15
- package/dist/docx-redline-js.esm.js +2849 -466
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +87 -76
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/TESTING.md +342 -23
- package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +1669 -0
- package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +669 -0
- package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +427 -0
- package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +519 -0
- package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +69 -0
- package/docs/plans/completed/structural-revision-capability-matrix.md +115 -0
- package/docs/schemas/document-operations.schema.json +109 -0
- package/docs/test-comparison-dashboard.html +4250 -7
- package/engine/formatting-removal.js +11 -2
- package/engine/oxml-engine.js +491 -336
- package/engine/reconstruction-mode.js +15 -14
- package/engine/reconstruction-writer.js +247 -142
- package/engine/route-selection.js +35 -0
- package/engine/rpr-helpers.js +334 -35
- package/engine/run-builders.js +239 -196
- package/engine/surgical-diff-application.js +222 -37
- package/engine/surgical-mode.js +134 -6
- package/engine/surgical-spans.js +52 -1
- package/engine/table-cell-context.js +3 -6
- package/engine/table-mode.js +1 -1
- package/index.d.ts +234 -6
- package/index.js +24 -1
- package/node/cli.js +317 -0
- package/node/docx-document.js +302 -0
- package/node/index.d.ts +31 -0
- package/node/index.js +2 -0
- package/node/zip-archive.js +52 -0
- package/orchestration/list-markdown.js +10 -16
- package/orchestration/list-parsing.js +7 -12
- package/orchestration/list-structural-fallback.js +21 -10
- package/package.json +24 -3
- package/pipeline/content-analysis.js +12 -17
- package/pipeline/ingestion-export.js +3 -31
- package/pipeline/ingestion-paragraph.js +10 -5
- package/pipeline/list-generation.js +150 -55
- package/pipeline/list-markers.js +70 -3
- package/pipeline/serialization.js +4 -2
- package/pipeline/structured-content.js +160 -0
- package/scripts/apply_changes.mjs +27 -0
- package/scripts/benchmark-operation-session.mjs +137 -0
- package/scripts/benchmark-targeting-browser.html +74 -0
- package/scripts/benchmark-targeting-hot-paths.mjs +67 -0
- package/scripts/benchmark-test-runner.mjs +59 -0
- package/scripts/build-test-dashboard.mjs +23 -0
- package/scripts/export-lane1-fixtures.mjs +380 -0
- package/scripts/export-reredline-stress-fixtures.mjs +317 -0
- package/scripts/export-validation-fixtures.mjs +1 -1
- package/scripts/extract_text.mjs +7 -0
- package/scripts/generate-paragraph-boundary-fixtures.ps1 +215 -0
- package/scripts/generate-test-dashboard.mjs +362 -11
- package/scripts/lib/word-coverage-catalogue.mjs +6 -2
- package/scripts/profile-route-selection.mjs +19 -0
- package/scripts/render-agenda-multilevel.mjs +0 -5
- package/scripts/render-multilevel-cases.mjs +0 -1
- package/scripts/run-tests.mjs +107 -35
- package/scripts/word-com-corpus-suite.ps1 +3 -0
- package/scripts/word-com-differential.ps1 +64 -4
- package/scripts/word-com-suite.ps1 +3 -0
- package/services/batch-operation-orchestrator.js +494 -0
- package/services/capture-engine.js +226 -0
- package/services/comment-builders.js +23 -6
- package/services/comment-engine.js +108 -47
- package/services/comment-locator.js +187 -82
- package/services/comment-replies.js +95 -0
- package/services/document-inspection.js +258 -0
- package/services/document-operation-applier.js +372 -0
- package/services/document-operation-contract.js +323 -0
- package/services/document-operation-mutations.js +1733 -0
- package/services/document-operation-session.js +258 -0
- package/services/numbering-service.js +14 -5
- package/services/operation-heuristics.js +173 -0
- package/services/operation-preflight.js +366 -0
- package/services/receipt-collector.js +288 -0
- package/services/revision-comment-management.js +37 -5
- package/services/revision-token.js +290 -0
- package/services/standalone-docx-plumbing.js +123 -8
- package/services/standalone-operation-runner.d.ts +296 -0
- package/services/standalone-operation-runner.js +10 -1455
- package/services/table-reconciliation.js +15 -6
- package/docs/VALIDATION.md +0 -183
- package/docs/WORD-MANUAL-REVIEW.md +0 -138
- package/docs/plans/2026-09-01-performance-and-complexity-reduction.md +0 -210
- /package/docs/plans/{2026-08-30-reliability-testing-improvements.md → completed/2026-08-30-reliability-testing-improvements.md} +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ansonlai/docx-redline-js v0.
|
|
1
|
+
// @ansonlai/docx-redline-js v0.5.0 — 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;
|
|
@@ -1612,10 +1612,11 @@ function serializeXml(node) {
|
|
|
1612
1612
|
}
|
|
1613
1613
|
|
|
1614
1614
|
// adapters/config.js
|
|
1615
|
-
var
|
|
1615
|
+
var fallbackAuthor = () => typeof process !== "undefined" && process.env?.DOCX_REDLINE_AUTHOR || "AI Redliner";
|
|
1616
|
+
var _defaultAuthor = fallbackAuthor();
|
|
1616
1617
|
var _platform = "Unknown";
|
|
1617
1618
|
function setDefaultAuthor(author) {
|
|
1618
|
-
_defaultAuthor = typeof author === "string" && author.trim() ? author.trim() :
|
|
1619
|
+
_defaultAuthor = typeof author === "string" && author.trim() ? author.trim() : fallbackAuthor();
|
|
1619
1620
|
}
|
|
1620
1621
|
function getDefaultAuthor() {
|
|
1621
1622
|
return _defaultAuthor;
|
|
@@ -1743,7 +1744,7 @@ function decodeHtmlEntities(text) {
|
|
|
1743
1744
|
}
|
|
1744
1745
|
|
|
1745
1746
|
// pipeline/list-markers.js
|
|
1746
|
-
var LIST_MARKER_CORE = String.raw`(?:\d+(?:\.\d+)*\.?|\((?:\d+|[a-zA-Z]|[ivxlcIVXLC]+)\)|[a-zA-Z]\.|\d+\.|[ivxlcIVXLC]+\.|[
|
|
1747
|
+
var LIST_MARKER_CORE = String.raw`(?:\d+(?:\.\d+)*\.?|\((?:\d+|[a-zA-Z]|[ivxlcIVXLC]+)\)|[a-zA-Z]\.|\d+\.|[ivxlcIVXLC]+\.|[-*+\u2022])`;
|
|
1747
1748
|
var LINE_REGEX_STRICT = new RegExp(`^(\\s*)((?:${LIST_MARKER_CORE})\\s+)`);
|
|
1748
1749
|
var LINE_REGEX_LOOSE = new RegExp(`^(\\s*)((?:${LIST_MARKER_CORE})\\s*)`);
|
|
1749
1750
|
var MULTILINE_REGEX_STRICT = new RegExp(`^(\\s*)((?:${LIST_MARKER_CORE})\\s+)`, "m");
|
|
@@ -1766,6 +1767,43 @@ function stripListMarker(line, options = {}) {
|
|
|
1766
1767
|
const regex = allowZeroSpaceAfterMarker ? LINE_REGEX_LOOSE : LINE_REGEX_STRICT;
|
|
1767
1768
|
return line.replace(regex, "");
|
|
1768
1769
|
}
|
|
1770
|
+
function classifyListMarker(marker) {
|
|
1771
|
+
return /^[-*+\u2022]$/.test(String(marker || "").trim()) ? "bullet" : "numbered";
|
|
1772
|
+
}
|
|
1773
|
+
function inferNumberingStyleFromMarker(marker) {
|
|
1774
|
+
const value = String(marker || "").trim();
|
|
1775
|
+
if (classifyListMarker(value) === "bullet") return "bullet";
|
|
1776
|
+
if (/^\d+(?:\.\d+)*\.?$/.test(value) || /^\(\d+\)$/.test(value)) return "decimal";
|
|
1777
|
+
if (/^[ivxlcdm]+\.$/.test(value)) return "lowerRoman";
|
|
1778
|
+
if (/^[IVXLCDM]{2,}\.$/.test(value)) return "upperRoman";
|
|
1779
|
+
if (/^[a-z]\.$/.test(value)) return "lowerAlpha";
|
|
1780
|
+
if (/^[A-Z]\.$/.test(value)) return "upperAlpha";
|
|
1781
|
+
return "decimal";
|
|
1782
|
+
}
|
|
1783
|
+
function parseOutlineLevelFromMarker(marker) {
|
|
1784
|
+
const value = String(marker || "").trim();
|
|
1785
|
+
if (!/^\d+(?:\.\d+)+\.?$/.test(value)) return null;
|
|
1786
|
+
return Math.max(0, value.replace(/\.$/, "").split(".").length - 1);
|
|
1787
|
+
}
|
|
1788
|
+
function parseListItem(line, options = {}) {
|
|
1789
|
+
const match = matchListMarker(String(line || ""), options);
|
|
1790
|
+
if (!match) return null;
|
|
1791
|
+
const marker = match[2].trim();
|
|
1792
|
+
const indent = (match[1] || "").length;
|
|
1793
|
+
const indentSpaces = Math.max(1, Number(options.indentSpaces) || 2);
|
|
1794
|
+
const markerType = classifyListMarker(marker);
|
|
1795
|
+
return {
|
|
1796
|
+
line: String(line || ""),
|
|
1797
|
+
text: stripListMarker(String(line || ""), options),
|
|
1798
|
+
marker,
|
|
1799
|
+
indent,
|
|
1800
|
+
level: Math.min(8, Math.floor(indent / indentSpaces)),
|
|
1801
|
+
markerType,
|
|
1802
|
+
listType: markerType,
|
|
1803
|
+
numberingStyle: inferNumberingStyleFromMarker(marker),
|
|
1804
|
+
outlineLevel: markerType === "numbered" ? parseOutlineLevelFromMarker(marker) : null
|
|
1805
|
+
};
|
|
1806
|
+
}
|
|
1769
1807
|
|
|
1770
1808
|
// core/types.js
|
|
1771
1809
|
var NS_W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
@@ -1849,8 +1887,8 @@ var REVISION_ELEMENT_NAMES = /* @__PURE__ */ new Set([
|
|
|
1849
1887
|
var revisionAllocatorByDocument = /* @__PURE__ */ new WeakMap();
|
|
1850
1888
|
function isRevisionIdElement(element) {
|
|
1851
1889
|
if (!element || element.nodeType !== 1) return false;
|
|
1852
|
-
const
|
|
1853
|
-
if (!REVISION_ELEMENT_NAMES.has(
|
|
1890
|
+
const localName2 = String(element.localName || element.nodeName || "").replace(/^.*:/, "");
|
|
1891
|
+
if (!REVISION_ELEMENT_NAMES.has(localName2)) return false;
|
|
1854
1892
|
return !element.namespaceURI || element.namespaceURI === NS_W || String(element.nodeName || "").startsWith("w:");
|
|
1855
1893
|
}
|
|
1856
1894
|
function readWordId(element) {
|
|
@@ -1866,14 +1904,22 @@ var RevisionIdAllocator = class {
|
|
|
1866
1904
|
}
|
|
1867
1905
|
seed(xmlDoc) {
|
|
1868
1906
|
let maxFound = -1;
|
|
1869
|
-
const
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
if (
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1907
|
+
const traversalRoot = xmlDoc?.nodeType === 9 ? xmlDoc.documentElement : xmlDoc;
|
|
1908
|
+
let node = traversalRoot || null;
|
|
1909
|
+
while (node) {
|
|
1910
|
+
if (isRevisionIdElement(node)) {
|
|
1911
|
+
const id = readWordId(node);
|
|
1912
|
+
if (id != null) {
|
|
1913
|
+
this.occupiedIds.add(id);
|
|
1914
|
+
maxFound = Math.max(maxFound, id);
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
if (node.firstChild) {
|
|
1918
|
+
node = node.firstChild;
|
|
1919
|
+
continue;
|
|
1920
|
+
}
|
|
1921
|
+
while (node && node !== traversalRoot && !node.nextSibling) node = node.parentNode;
|
|
1922
|
+
node = node && node !== traversalRoot ? node.nextSibling : null;
|
|
1877
1923
|
}
|
|
1878
1924
|
const highRiskBoundary = MAX_PRACTICAL_REVISION_ID - REVISION_ID_SAFETY_MARGIN;
|
|
1879
1925
|
this.nextId = maxFound >= highRiskBoundary ? this.startValue : Math.max(this.nextId, maxFound + 1);
|
|
@@ -1920,15 +1966,36 @@ function getNextRevisionId() {
|
|
|
1920
1966
|
function getRevisionTimestamp(date = /* @__PURE__ */ new Date()) {
|
|
1921
1967
|
return date.toISOString();
|
|
1922
1968
|
}
|
|
1923
|
-
function createRevisionMetadata(author, allocatorOrNode = null) {
|
|
1969
|
+
function createRevisionMetadata(author, allocatorOrNode = null, kind = null) {
|
|
1924
1970
|
const resolvedAuthor = typeof author === "string" && author.trim() ? author.trim() : getDefaultAuthor();
|
|
1925
1971
|
const allocator = allocatorOrNode instanceof RevisionIdAllocator ? allocatorOrNode : getRevisionIdAllocatorForDocument(allocatorOrNode) || defaultRevisionIdAllocator;
|
|
1972
|
+
const id = allocator.next();
|
|
1973
|
+
if (allocator._receiptCollector) {
|
|
1974
|
+
allocator._receiptCollector.recordRevision(id, kind || "structural");
|
|
1975
|
+
}
|
|
1926
1976
|
return {
|
|
1927
|
-
id
|
|
1977
|
+
id,
|
|
1928
1978
|
author: resolvedAuthor,
|
|
1929
1979
|
date: getRevisionTimestamp()
|
|
1930
1980
|
};
|
|
1931
1981
|
}
|
|
1982
|
+
function createReplacementRevisionEvent(author, allocatorOrNode = null) {
|
|
1983
|
+
const resolvedAuthor = typeof author === "string" && author.trim() ? author.trim() : getDefaultAuthor();
|
|
1984
|
+
const allocator = allocatorOrNode instanceof RevisionIdAllocator ? allocatorOrNode : getRevisionIdAllocatorForDocument(allocatorOrNode) || defaultRevisionIdAllocator;
|
|
1985
|
+
const date = getRevisionTimestamp();
|
|
1986
|
+
const deletionId = allocator.next();
|
|
1987
|
+
const insertionId = allocator.next();
|
|
1988
|
+
if (allocator._receiptCollector) {
|
|
1989
|
+
allocator._receiptCollector.recordRevision(deletionId, "del");
|
|
1990
|
+
allocator._receiptCollector.recordRevision(insertionId, "ins");
|
|
1991
|
+
}
|
|
1992
|
+
return {
|
|
1993
|
+
deletionId,
|
|
1994
|
+
insertionId,
|
|
1995
|
+
author: resolvedAuthor,
|
|
1996
|
+
date
|
|
1997
|
+
};
|
|
1998
|
+
}
|
|
1932
1999
|
function seedRevisionIdsFromDocument(xmlDoc, allocator = defaultRevisionIdAllocator) {
|
|
1933
2000
|
const resolvedAllocator = allocator instanceof RevisionIdAllocator ? allocator : defaultRevisionIdAllocator;
|
|
1934
2001
|
const nextId = resolvedAllocator.seed(xmlDoc);
|
|
@@ -1965,22 +2032,22 @@ function getFirstElementByTag(node, tagName) {
|
|
|
1965
2032
|
const elements = node.getElementsByTagName(tagName);
|
|
1966
2033
|
return elements.length > 0 ? elements[0] : null;
|
|
1967
2034
|
}
|
|
1968
|
-
function getElementsByTagNS(node, namespaceUri,
|
|
2035
|
+
function getElementsByTagNS(node, namespaceUri, localName2) {
|
|
1969
2036
|
if (!node || typeof node.getElementsByTagNameNS !== "function") return [];
|
|
1970
|
-
return Array.from(node.getElementsByTagNameNS(namespaceUri,
|
|
2037
|
+
return Array.from(node.getElementsByTagNameNS(namespaceUri, localName2));
|
|
1971
2038
|
}
|
|
1972
|
-
function getFirstElementByTagNS(node, namespaceUri,
|
|
2039
|
+
function getFirstElementByTagNS(node, namespaceUri, localName2) {
|
|
1973
2040
|
if (!node || typeof node.getElementsByTagNameNS !== "function") return null;
|
|
1974
|
-
const elements = node.getElementsByTagNameNS(namespaceUri,
|
|
2041
|
+
const elements = node.getElementsByTagNameNS(namespaceUri, localName2);
|
|
1975
2042
|
return elements.length > 0 ? elements[0] : null;
|
|
1976
2043
|
}
|
|
1977
|
-
function getElementsByTagNSOrTag(node, namespaceUri,
|
|
1978
|
-
const namespacedElements = getElementsByTagNS(node, namespaceUri,
|
|
2044
|
+
function getElementsByTagNSOrTag(node, namespaceUri, localName2, fallbackTagName = `w:${localName2}`) {
|
|
2045
|
+
const namespacedElements = getElementsByTagNS(node, namespaceUri, localName2);
|
|
1979
2046
|
if (namespacedElements.length > 0) return namespacedElements;
|
|
1980
2047
|
return getElementsByTag(node, fallbackTagName);
|
|
1981
2048
|
}
|
|
1982
|
-
function getFirstElementByTagNSOrTag(node, namespaceUri,
|
|
1983
|
-
const namespacedElement = getFirstElementByTagNS(node, namespaceUri,
|
|
2049
|
+
function getFirstElementByTagNSOrTag(node, namespaceUri, localName2, fallbackTagName = `w:${localName2}`) {
|
|
2050
|
+
const namespacedElement = getFirstElementByTagNS(node, namespaceUri, localName2);
|
|
1984
2051
|
if (namespacedElement) return namespacedElement;
|
|
1985
2052
|
return getFirstElementByTag(node, fallbackTagName);
|
|
1986
2053
|
}
|
|
@@ -1993,12 +2060,12 @@ function childNodesToArray(node) {
|
|
|
1993
2060
|
return Array.from(node?.childNodes || []);
|
|
1994
2061
|
}
|
|
1995
2062
|
function serializeAttributes(element) {
|
|
1996
|
-
return Array.from(element.attributes).map((
|
|
2063
|
+
return Array.from(element.attributes).map((attr4) => `${attr4.name}="${attr4.value}"`).join(" ");
|
|
1997
2064
|
}
|
|
1998
|
-
function isNamespacedNode(node, namespaceUri,
|
|
2065
|
+
function isNamespacedNode(node, namespaceUri, localName2 = "") {
|
|
1999
2066
|
if (!node || node.namespaceURI !== namespaceUri) return false;
|
|
2000
|
-
if (!
|
|
2001
|
-
return node.localName ===
|
|
2067
|
+
if (!localName2) return true;
|
|
2068
|
+
return node.localName === localName2;
|
|
2002
2069
|
}
|
|
2003
2070
|
|
|
2004
2071
|
// pipeline/ingestion-paragraph.js
|
|
@@ -2048,6 +2115,7 @@ function detectNumberingContext(pElement) {
|
|
|
2048
2115
|
const ilvlEl = getFirstElementByTagNS(numPr, NS_W, "ilvl");
|
|
2049
2116
|
if (!numIdEl) return null;
|
|
2050
2117
|
const numId = numIdEl.getAttribute("w:val");
|
|
2118
|
+
if (!/^\d+$/.test(numId) || Number.parseInt(numId, 10) === 0) return null;
|
|
2051
2119
|
const type = numId === "1" ? "bullet" : numId === "2" ? "numbered" : "unknown";
|
|
2052
2120
|
return {
|
|
2053
2121
|
numId,
|
|
@@ -2246,6 +2314,8 @@ function processRun(runElement, startOffset) {
|
|
|
2246
2314
|
text += " ";
|
|
2247
2315
|
} else if (nodeName.endsWith(":noBreakHyphen") || nodeName === "noBreakHyphen") {
|
|
2248
2316
|
text += "\u2011";
|
|
2317
|
+
} else if (nodeName.endsWith(":softHyphen") || nodeName === "softHyphen") {
|
|
2318
|
+
text += "\xAD";
|
|
2249
2319
|
}
|
|
2250
2320
|
}
|
|
2251
2321
|
if (!text) return null;
|
|
@@ -3155,7 +3225,8 @@ function buildSimpleRun(text, rPrXml) {
|
|
|
3155
3225
|
function buildDeletionXml(item, options = {}) {
|
|
3156
3226
|
const metadata = createRevisionMetadata(
|
|
3157
3227
|
options.author ?? getDefaultAuthor(),
|
|
3158
|
-
options.revisionIdAllocator
|
|
3228
|
+
options.revisionIdAllocator,
|
|
3229
|
+
"del"
|
|
3159
3230
|
);
|
|
3160
3231
|
const font = options.font ?? null;
|
|
3161
3232
|
let rPr = item.rPrXml ? stripNamespaceDeclarations(item.rPrXml) : "";
|
|
@@ -3167,7 +3238,8 @@ function buildDeletionXml(item, options = {}) {
|
|
|
3167
3238
|
function buildInsertionXml(item, formatHints, options = {}) {
|
|
3168
3239
|
const metadata = createRevisionMetadata(
|
|
3169
3240
|
options.author ?? getDefaultAuthor(),
|
|
3170
|
-
options.revisionIdAllocator
|
|
3241
|
+
options.revisionIdAllocator,
|
|
3242
|
+
"ins"
|
|
3171
3243
|
);
|
|
3172
3244
|
const font = options.font ?? null;
|
|
3173
3245
|
const applicableHints = getApplicableFormatHints(formatHints, item.startOffset, item.endOffset);
|
|
@@ -3255,6 +3327,10 @@ function stripNamespaceDeclarations(xml) {
|
|
|
3255
3327
|
}
|
|
3256
3328
|
|
|
3257
3329
|
// services/numbering-service.js
|
|
3330
|
+
function isUsableNumId(numId) {
|
|
3331
|
+
const normalized = String(numId ?? "");
|
|
3332
|
+
return /^\d+$/.test(normalized) && Number.parseInt(normalized, 10) > 0;
|
|
3333
|
+
}
|
|
3258
3334
|
var NumberingService = class {
|
|
3259
3335
|
constructor() {
|
|
3260
3336
|
this.contextMap = /* @__PURE__ */ new Map();
|
|
@@ -3268,7 +3344,9 @@ var NumberingService = class {
|
|
|
3268
3344
|
* @param {string} numId - Existing numId from Word
|
|
3269
3345
|
*/
|
|
3270
3346
|
registerExistingNumId(signature, numId) {
|
|
3271
|
-
|
|
3347
|
+
if (isUsableNumId(numId)) {
|
|
3348
|
+
this.contextMap.set(signature, String(numId));
|
|
3349
|
+
}
|
|
3272
3350
|
}
|
|
3273
3351
|
/**
|
|
3274
3352
|
* Resolves the best numId to use for a requested list format.
|
|
@@ -3279,13 +3357,15 @@ var NumberingService = class {
|
|
|
3279
3357
|
*/
|
|
3280
3358
|
getOrCreateNumId(formatConfig, existingContext = null) {
|
|
3281
3359
|
const requestedType = formatConfig.type || NumberFormat.BULLET;
|
|
3282
|
-
if (existingContext && existingContext.numId) {
|
|
3360
|
+
if (existingContext && isUsableNumId(existingContext.numId)) {
|
|
3283
3361
|
if (existingContext.type === requestedType || existingContext.type === "unknown") {
|
|
3284
3362
|
return existingContext.numId;
|
|
3285
3363
|
}
|
|
3286
3364
|
}
|
|
3287
3365
|
if (this.contextMap.has(requestedType)) {
|
|
3288
|
-
|
|
3366
|
+
const cachedNumId = this.contextMap.get(requestedType);
|
|
3367
|
+
if (isUsableNumId(cachedNumId)) return cachedNumId;
|
|
3368
|
+
this.contextMap.delete(requestedType);
|
|
3289
3369
|
}
|
|
3290
3370
|
if (requestedType === NumberFormat.OUTLINE) {
|
|
3291
3371
|
return "3";
|
|
@@ -3488,8 +3568,13 @@ var NumberingService = class {
|
|
|
3488
3568
|
|
|
3489
3569
|
// services/table-reconciliation.js
|
|
3490
3570
|
function generateTableOoxml(tableData, options = {}) {
|
|
3491
|
-
const {
|
|
3492
|
-
|
|
3571
|
+
const {
|
|
3572
|
+
generateRedlines = false,
|
|
3573
|
+
author = "AI",
|
|
3574
|
+
revisionIdAllocator = null,
|
|
3575
|
+
trackAsBlock = false
|
|
3576
|
+
} = options;
|
|
3577
|
+
const tableInsertMeta = generateRedlines && trackAsBlock ? createRevisionMetadata(author, revisionIdAllocator) : null;
|
|
3493
3578
|
const numCols = tableData.headers?.length || (tableData.rows?.[0]?.length || 1);
|
|
3494
3579
|
const tblPr = `
|
|
3495
3580
|
<w:tblPr>
|
|
@@ -3517,7 +3602,7 @@ function generateTableOoxml(tableData, options = {}) {
|
|
|
3517
3602
|
const cellText = row[c] || "";
|
|
3518
3603
|
const { cleanText, formatHints } = preprocessMarkdown(cellText);
|
|
3519
3604
|
const runModel = [{
|
|
3520
|
-
kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
|
|
3605
|
+
kind: generateRedlines && !trackAsBlock ? RunKind.INSERTION : RunKind.TEXT,
|
|
3521
3606
|
text: cleanText,
|
|
3522
3607
|
rPrXml: isHeaderRow ? "<w:rPr><w:b/></w:rPr>" : "",
|
|
3523
3608
|
author,
|
|
@@ -3532,7 +3617,7 @@ function generateTableOoxml(tableData, options = {}) {
|
|
|
3532
3617
|
const tcPr = '<w:tcPr><w:tcW w:w="0" w:type="auto"/></w:tcPr>';
|
|
3533
3618
|
cellsXml += `<w:tc>${tcPr}${runsOoxml}</w:tc>`;
|
|
3534
3619
|
}
|
|
3535
|
-
const trPr = "<w:trPr
|
|
3620
|
+
const trPr = isHeaderRow ? "<w:trPr><w:tblHeader/><w:cantSplit/></w:trPr>" : "<w:trPr><w:cantSplit/></w:trPr>";
|
|
3536
3621
|
rowsXml += `<w:tr>${trPr}${cellsXml}</w:tr>`;
|
|
3537
3622
|
}
|
|
3538
3623
|
let tableXml = `<w:tbl>${tblPr}${tblGrid}${rowsXml}</w:tbl>`;
|
|
@@ -3750,6 +3835,71 @@ function parseTable(text) {
|
|
|
3750
3835
|
};
|
|
3751
3836
|
}
|
|
3752
3837
|
|
|
3838
|
+
// core/word-xml.js
|
|
3839
|
+
function isWordElement(node, localName2) {
|
|
3840
|
+
if (!node || node.nodeType !== 1) return false;
|
|
3841
|
+
if (node.namespaceURI === NS_W && node.localName === localName2) return true;
|
|
3842
|
+
const nodeName = String(node.nodeName || "");
|
|
3843
|
+
return nodeName === `w:${localName2}` || nodeName === localName2;
|
|
3844
|
+
}
|
|
3845
|
+
function createWordElement(xmlDoc, qualifiedName) {
|
|
3846
|
+
return typeof xmlDoc.createElementNS === "function" ? xmlDoc.createElementNS(NS_W, qualifiedName) : xmlDoc.createElement(qualifiedName);
|
|
3847
|
+
}
|
|
3848
|
+
function wordElementsByLocalName(xmlDoc, localName2) {
|
|
3849
|
+
const namespaced = Array.from(xmlDoc?.getElementsByTagNameNS?.(NS_W, localName2) || []);
|
|
3850
|
+
if (namespaced.length > 0) return namespaced;
|
|
3851
|
+
return Array.from(xmlDoc?.getElementsByTagName?.("*") || []).filter((node) => isWordElement(node, localName2));
|
|
3852
|
+
}
|
|
3853
|
+
var TRACKED_CHANGE_NAMES = [
|
|
3854
|
+
"ins",
|
|
3855
|
+
"del",
|
|
3856
|
+
"moveFrom",
|
|
3857
|
+
"moveTo",
|
|
3858
|
+
"moveFromRangeStart",
|
|
3859
|
+
"moveFromRangeEnd",
|
|
3860
|
+
"moveToRangeStart",
|
|
3861
|
+
"moveToRangeEnd",
|
|
3862
|
+
"rPrChange",
|
|
3863
|
+
"pPrChange",
|
|
3864
|
+
"tblPrChange",
|
|
3865
|
+
"trPrChange",
|
|
3866
|
+
"tcPrChange",
|
|
3867
|
+
"cellIns",
|
|
3868
|
+
"cellDel"
|
|
3869
|
+
];
|
|
3870
|
+
function containsTrackedChanges(xmlDoc) {
|
|
3871
|
+
return TRACKED_CHANGE_NAMES.some((localName2) => wordElementsByLocalName(xmlDoc, localName2).length > 0);
|
|
3872
|
+
}
|
|
3873
|
+
function getTrackedChangeAuthors(xmlDocOrElement) {
|
|
3874
|
+
if (!xmlDocOrElement) return [];
|
|
3875
|
+
const authors = /* @__PURE__ */ new Set();
|
|
3876
|
+
for (const localName2 of TRACKED_CHANGE_NAMES) {
|
|
3877
|
+
for (const node of wordElementsByLocalName(xmlDocOrElement, localName2)) {
|
|
3878
|
+
const author = node.getAttribute?.("w:author") || node.getAttribute?.("author") || (typeof node.getAttributeNS === "function" ? node.getAttributeNS(NS_W, "author") : null);
|
|
3879
|
+
if (author && typeof author === "string" && author.trim()) {
|
|
3880
|
+
authors.add(author.trim());
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
}
|
|
3884
|
+
return [...authors].sort();
|
|
3885
|
+
}
|
|
3886
|
+
function classifyOoxmlSourceType(oxml) {
|
|
3887
|
+
const trimmed = String(oxml || "").trim();
|
|
3888
|
+
if (/^<\?xml\b[^>]*>\s*<pkg:package\b/i.test(trimmed) || /^<pkg:package\b/i.test(trimmed)) {
|
|
3889
|
+
return "package";
|
|
3890
|
+
}
|
|
3891
|
+
if (/^<\?xml\b[^>]*>\s*<(?:w:)?document\b/i.test(trimmed) || /^<(?:w:)?document\b/i.test(trimmed)) {
|
|
3892
|
+
return "document";
|
|
3893
|
+
}
|
|
3894
|
+
return "fragment";
|
|
3895
|
+
}
|
|
3896
|
+
function withOoxmlSourceType(result) {
|
|
3897
|
+
if (!result || typeof result !== "object" || result.sourceType || typeof result.oxml !== "string") {
|
|
3898
|
+
return result;
|
|
3899
|
+
}
|
|
3900
|
+
return { ...result, sourceType: classifyOoxmlSourceType(result.oxml) };
|
|
3901
|
+
}
|
|
3902
|
+
|
|
3753
3903
|
// pipeline/list-generation.js
|
|
3754
3904
|
async function executeListGeneration(options) {
|
|
3755
3905
|
const {
|
|
@@ -3767,6 +3917,13 @@ async function executeListGeneration(options) {
|
|
|
3767
3917
|
const lineMetadata = buildLineMetadata(normalizedListText);
|
|
3768
3918
|
const rawLines = lineMetadata.map((line) => line.raw);
|
|
3769
3919
|
const results = [];
|
|
3920
|
+
const sourcePPr = getSourceParagraphProperties(originalRunModel);
|
|
3921
|
+
const inheritedTypographyRPrXml = extractInheritedTypographyRPrXml(originalRunModel, sourcePPr);
|
|
3922
|
+
const inheritedHeadingRPrXml = extractInheritedTypographyRPrXml(
|
|
3923
|
+
originalRunModel,
|
|
3924
|
+
sourcePPr,
|
|
3925
|
+
["rFonts", "kern", "position", "rtl", "cs", "lang"]
|
|
3926
|
+
);
|
|
3770
3927
|
let deletionRuns = [];
|
|
3771
3928
|
if (generateRedlines) {
|
|
3772
3929
|
if (originalRunModel && originalRunModel.length > 0) {
|
|
@@ -3782,6 +3939,15 @@ async function executeListGeneration(options) {
|
|
|
3782
3939
|
}];
|
|
3783
3940
|
}
|
|
3784
3941
|
}
|
|
3942
|
+
if (generateRedlines && deletionRuns.length > 0) {
|
|
3943
|
+
const deletedPPr = addParagraphMarkRevision(sourcePPr, "del", author, revisionIdAllocator);
|
|
3944
|
+
const deletedParagraph = serializeToOoxml(deletionRuns, deletedPPr, [], {
|
|
3945
|
+
author,
|
|
3946
|
+
generateRedlines,
|
|
3947
|
+
revisionIdAllocator
|
|
3948
|
+
});
|
|
3949
|
+
results.push(deletedParagraph);
|
|
3950
|
+
}
|
|
3785
3951
|
const indentStep = detectIndentationStep(rawLines);
|
|
3786
3952
|
log(`[ListGen] Detected indentation step: ${indentStep} spaces/chars`);
|
|
3787
3953
|
const firstMarker = lineMetadata.find((line) => line.marker)?.marker || "";
|
|
@@ -3792,18 +3958,11 @@ async function executeListGeneration(options) {
|
|
|
3792
3958
|
if (tableBlock) {
|
|
3793
3959
|
const tableData = parseTable(tableBlock.tableText);
|
|
3794
3960
|
if (tableData.headers.length > 0 || tableData.rows.length > 0) {
|
|
3795
|
-
if (generateRedlines && results.length === 0 && deletionRuns.length > 0) {
|
|
3796
|
-
results.push(serializeToOoxml(deletionRuns, null, [], {
|
|
3797
|
-
author,
|
|
3798
|
-
generateRedlines,
|
|
3799
|
-
font,
|
|
3800
|
-
revisionIdAllocator
|
|
3801
|
-
}));
|
|
3802
|
-
}
|
|
3803
3961
|
results.push(generateTableOoxml(tableData, {
|
|
3804
3962
|
generateRedlines,
|
|
3805
3963
|
author,
|
|
3806
|
-
revisionIdAllocator
|
|
3964
|
+
revisionIdAllocator,
|
|
3965
|
+
trackAsBlock: true
|
|
3807
3966
|
}));
|
|
3808
3967
|
i = tableBlock.endIndex;
|
|
3809
3968
|
continue;
|
|
@@ -3812,7 +3971,6 @@ async function executeListGeneration(options) {
|
|
|
3812
3971
|
const line = lineMetadata[i];
|
|
3813
3972
|
const entry = buildListEntry(
|
|
3814
3973
|
line,
|
|
3815
|
-
i,
|
|
3816
3974
|
indentStep,
|
|
3817
3975
|
numberingContext,
|
|
3818
3976
|
numberingService,
|
|
@@ -3820,18 +3978,17 @@ async function executeListGeneration(options) {
|
|
|
3820
3978
|
author,
|
|
3821
3979
|
font,
|
|
3822
3980
|
revisionIdAllocator,
|
|
3823
|
-
|
|
3981
|
+
inheritedTypographyRPrXml,
|
|
3982
|
+
inheritedHeadingRPrXml
|
|
3824
3983
|
);
|
|
3825
3984
|
results.push(entry.ooxml);
|
|
3826
3985
|
}
|
|
3827
3986
|
const numberingXml = numberingService.generateNumberingXml();
|
|
3828
3987
|
const finalOoxml = results.join("");
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
log(`[ListGen] \u2705 Generated OOXML for ${results.length} list items, total length: ${oxmlWithSpacing.length}`);
|
|
3832
|
-
log(`[ListGen] First 200 chars: ${oxmlWithSpacing.substring(0, 200)}...`);
|
|
3988
|
+
log(`[ListGen] \u2705 Generated OOXML for ${results.length} paragraphs, total length: ${finalOoxml.length}`);
|
|
3989
|
+
log(`[ListGen] First 200 chars: ${finalOoxml.substring(0, 200)}...`);
|
|
3833
3990
|
return {
|
|
3834
|
-
ooxml:
|
|
3991
|
+
ooxml: finalOoxml,
|
|
3835
3992
|
isValid: true,
|
|
3836
3993
|
warnings: ["Paragraph expanded to list fragment"],
|
|
3837
3994
|
type: "fragment",
|
|
@@ -3910,9 +4067,10 @@ function collectMarkdownTableBlock(lineMetadata, index) {
|
|
|
3910
4067
|
endIndex: cursor - 1
|
|
3911
4068
|
};
|
|
3912
4069
|
}
|
|
3913
|
-
function buildListEntry(line,
|
|
4070
|
+
function buildListEntry(line, indentStep, numberingContext, numberingService, generateRedlines, author, font, revisionIdAllocator, inheritedTypographyRPrXml, inheritedHeadingRPrXml) {
|
|
3914
4071
|
let pPrXml = "";
|
|
3915
4072
|
let segmentText = "";
|
|
4073
|
+
let insertedRPrXml = inheritedTypographyRPrXml;
|
|
3916
4074
|
if (line.headerMatch) {
|
|
3917
4075
|
const level = Math.min(line.headerMatch[1].length, 9);
|
|
3918
4076
|
const outlineLevel = Math.min(level - 1, 8);
|
|
@@ -3920,6 +4078,10 @@ function buildListEntry(line, lineIndex, indentStep, numberingContext, numbering
|
|
|
3920
4078
|
const headingSize = headingSizes[level - 1] || headingSizes[headingSizes.length - 1];
|
|
3921
4079
|
segmentText = line.headerMatch[2].trim();
|
|
3922
4080
|
pPrXml = `<w:pPr><w:pStyle w:val="Heading${level}"/><w:outlineLvl w:val="${outlineLevel}"/><w:rPr><w:b/><w:sz w:val="${headingSize}"/><w:szCs w:val="${headingSize}"/></w:rPr></w:pPr>`;
|
|
4081
|
+
insertedRPrXml = mergeRunProperties(
|
|
4082
|
+
inheritedHeadingRPrXml,
|
|
4083
|
+
`<w:rPr><w:b/><w:sz w:val="${headingSize}"/><w:szCs w:val="${headingSize}"/></w:rPr>`
|
|
4084
|
+
);
|
|
3923
4085
|
} else if (line.marker) {
|
|
3924
4086
|
const lineFormat = numberingService.detectNumberingFormat(line.marker);
|
|
3925
4087
|
const indentLevel = indentStep > 0 ? Math.floor(line.indentSize / indentStep) : 0;
|
|
@@ -3931,14 +4093,20 @@ function buildListEntry(line, lineIndex, indentStep, numberingContext, numbering
|
|
|
3931
4093
|
} else {
|
|
3932
4094
|
segmentText = line.raw;
|
|
3933
4095
|
}
|
|
4096
|
+
if (generateRedlines) {
|
|
4097
|
+
pPrXml = addParagraphMarkRevision(
|
|
4098
|
+
pPrXml || "<w:pPr/>",
|
|
4099
|
+
"ins",
|
|
4100
|
+
author,
|
|
4101
|
+
revisionIdAllocator
|
|
4102
|
+
);
|
|
4103
|
+
}
|
|
3934
4104
|
const { cleanText, formatHints } = preprocessMarkdown(segmentText);
|
|
3935
4105
|
const runModel = [];
|
|
3936
|
-
if (lineIndex === 0 && deletionRuns.length > 0) {
|
|
3937
|
-
runModel.push(...deletionRuns);
|
|
3938
|
-
}
|
|
3939
4106
|
runModel.push({
|
|
3940
4107
|
kind: generateRedlines ? "insertion" : "run",
|
|
3941
4108
|
text: cleanText,
|
|
4109
|
+
rPrXml: insertedRPrXml,
|
|
3942
4110
|
author,
|
|
3943
4111
|
startOffset: 0,
|
|
3944
4112
|
endOffset: cleanText.length
|
|
@@ -3952,6 +4120,59 @@ function buildListEntry(line, lineIndex, indentStep, numberingContext, numbering
|
|
|
3952
4120
|
})
|
|
3953
4121
|
};
|
|
3954
4122
|
}
|
|
4123
|
+
function getSourceParagraphProperties(originalRunModel) {
|
|
4124
|
+
const paragraphStart = (originalRunModel || []).find((run) => run.kind === RunKind.PARAGRAPH_START);
|
|
4125
|
+
return paragraphStart?.pPrElement || paragraphStart?.pPrXml || null;
|
|
4126
|
+
}
|
|
4127
|
+
function extractInheritedTypographyRPrXml(originalRunModel, sourcePPr, tagOrder = ["rFonts", "kern", "position", "sz", "szCs", "rtl", "cs", "lang"]) {
|
|
4128
|
+
const sources = (originalRunModel || []).filter((run) => (run.kind === RunKind.TEXT || run.kind === "text") && run.rPrXml).map((run) => run.rPrXml);
|
|
4129
|
+
if (sourcePPr) {
|
|
4130
|
+
sources.push(typeof sourcePPr === "string" ? sourcePPr : serializeXml(sourcePPr));
|
|
4131
|
+
}
|
|
4132
|
+
const inherited = [];
|
|
4133
|
+
for (const tagName of tagOrder) {
|
|
4134
|
+
let match = null;
|
|
4135
|
+
for (const source of sources) {
|
|
4136
|
+
match = String(source || "").match(new RegExp(`<w:${tagName}\\b[^>]*(?:\\/>|>[\\s\\S]*?<\\/w:${tagName}>)`));
|
|
4137
|
+
if (match) break;
|
|
4138
|
+
}
|
|
4139
|
+
if (match) inherited.push(match[0]);
|
|
4140
|
+
}
|
|
4141
|
+
return inherited.length > 0 ? `<w:rPr>${inherited.join("")}</w:rPr>` : "";
|
|
4142
|
+
}
|
|
4143
|
+
function mergeRunProperties(...sources) {
|
|
4144
|
+
const properties = sources.map((source) => String(source || "").replace(/^\s*<w:rPr[^>]*>|<\/w:rPr>\s*$/g, "")).filter(Boolean).join("");
|
|
4145
|
+
return properties ? `<w:rPr>${properties}</w:rPr>` : "";
|
|
4146
|
+
}
|
|
4147
|
+
function addParagraphMarkRevision(xml, type, author, revisionIdAllocator) {
|
|
4148
|
+
const metadata = createRevisionMetadata(author, revisionIdAllocator);
|
|
4149
|
+
let pPr;
|
|
4150
|
+
let ownerDoc;
|
|
4151
|
+
if (xml?.nodeType === 1) {
|
|
4152
|
+
pPr = xml.cloneNode(true);
|
|
4153
|
+
ownerDoc = pPr.ownerDocument;
|
|
4154
|
+
} else {
|
|
4155
|
+
const pPrXml = typeof xml === "string" && xml.trim() ? xml : "<w:pPr/>";
|
|
4156
|
+
const parsed = parseOoxmlSafe(`<w:root xmlns:w="${NS_W}">${pPrXml}</w:root>`);
|
|
4157
|
+
if (!parsed.doc) {
|
|
4158
|
+
throw new Error(parsed.error?.message || "Could not parse paragraph properties for list revision");
|
|
4159
|
+
}
|
|
4160
|
+
ownerDoc = parsed.doc;
|
|
4161
|
+
pPr = Array.from(ownerDoc.documentElement.childNodes || []).find((node) => node.nodeType === 1 && node.localName === "pPr");
|
|
4162
|
+
}
|
|
4163
|
+
if (!pPr || !ownerDoc) throw new Error("List paragraph properties are unavailable");
|
|
4164
|
+
let rPr = Array.from(pPr.childNodes || []).find((node) => node.nodeType === 1 && node.localName === "rPr");
|
|
4165
|
+
if (!rPr) {
|
|
4166
|
+
rPr = createWordElement(ownerDoc, "w:rPr");
|
|
4167
|
+
pPr.appendChild(rPr);
|
|
4168
|
+
}
|
|
4169
|
+
const marker = createWordElement(ownerDoc, type === "del" ? "w:del" : "w:ins");
|
|
4170
|
+
marker.setAttribute("w:id", String(metadata.id));
|
|
4171
|
+
marker.setAttribute("w:author", metadata.author);
|
|
4172
|
+
marker.setAttribute("w:date", metadata.date);
|
|
4173
|
+
rPr.appendChild(marker);
|
|
4174
|
+
return serializeXml(pPr);
|
|
4175
|
+
}
|
|
3955
4176
|
|
|
3956
4177
|
// pipeline/pipeline.js
|
|
3957
4178
|
var WEB_PLATFORM_NAMES = /* @__PURE__ */ new Set(["officeonline", "officeweb", "web"]);
|
|
@@ -4217,53 +4438,127 @@ var ReconciliationPipeline = class {
|
|
|
4217
4438
|
}
|
|
4218
4439
|
};
|
|
4219
4440
|
|
|
4220
|
-
//
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
}
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4441
|
+
// pipeline/structured-content.js
|
|
4442
|
+
var TABLE_SEPARATOR = /^\s*\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?\s*$/;
|
|
4443
|
+
function isBlank(line) {
|
|
4444
|
+
return !line || line.trim().length === 0;
|
|
4445
|
+
}
|
|
4446
|
+
function isTableLine(line) {
|
|
4447
|
+
const trimmed = String(line || "").trim();
|
|
4448
|
+
return trimmed.startsWith("|") && trimmed.endsWith("|");
|
|
4449
|
+
}
|
|
4450
|
+
function tableCells(line) {
|
|
4451
|
+
return String(line || "").trim().split("|").slice(1, -1).map((cell) => cell.trim());
|
|
4452
|
+
}
|
|
4453
|
+
function classifyLine(line) {
|
|
4454
|
+
if (isBlank(line)) return "blank";
|
|
4455
|
+
if (/^\s*#{1,9}\s+\S/.test(line)) return "heading";
|
|
4456
|
+
if (isTableLine(line)) return "table";
|
|
4457
|
+
if (matchListMarker(line)) return "list";
|
|
4458
|
+
return "paragraph";
|
|
4459
|
+
}
|
|
4460
|
+
function analyzeStructuredContent(markdown) {
|
|
4461
|
+
const source = typeof markdown === "string" ? markdown.replace(/\r\n?/g, "\n") : String(markdown ?? "");
|
|
4462
|
+
const lines = source.split("\n");
|
|
4463
|
+
const blocks = [];
|
|
4464
|
+
const issues = [];
|
|
4465
|
+
let index = 0;
|
|
4466
|
+
while (index < lines.length) {
|
|
4467
|
+
if (isBlank(lines[index])) {
|
|
4468
|
+
index++;
|
|
4469
|
+
continue;
|
|
4470
|
+
}
|
|
4471
|
+
const kind = classifyLine(lines[index]);
|
|
4472
|
+
const startLine = index + 1;
|
|
4473
|
+
if (kind === "heading") {
|
|
4474
|
+
const match = lines[index].match(/^\s*(#{1,9})\s+(.+?)\s*$/);
|
|
4475
|
+
blocks.push({ type: "heading", level: match[1].length, text: match[2], markdown: lines[index].trim() });
|
|
4476
|
+
index++;
|
|
4477
|
+
continue;
|
|
4478
|
+
}
|
|
4479
|
+
if (kind === "table") {
|
|
4480
|
+
const tableLines = [];
|
|
4481
|
+
while (index < lines.length && isTableLine(lines[index])) {
|
|
4482
|
+
tableLines.push(lines[index].trim());
|
|
4483
|
+
index++;
|
|
4484
|
+
}
|
|
4485
|
+
const separatorPresent = tableLines.length > 1 && TABLE_SEPARATOR.test(tableLines[1]);
|
|
4486
|
+
const widths = tableLines.filter((line) => !TABLE_SEPARATOR.test(line)).map((line) => tableCells(line).length);
|
|
4487
|
+
if (!separatorPresent) {
|
|
4488
|
+
issues.push({
|
|
4489
|
+
severity: "error",
|
|
4490
|
+
code: "TABLE_SEPARATOR_REQUIRED",
|
|
4491
|
+
line: startLine,
|
|
4492
|
+
message: "Markdown tables require a separator row immediately after the header (for example | --- | --- |)."
|
|
4493
|
+
});
|
|
4494
|
+
}
|
|
4495
|
+
if (widths.length < 2) {
|
|
4496
|
+
issues.push({
|
|
4497
|
+
severity: "error",
|
|
4498
|
+
code: "TABLE_DATA_ROW_REQUIRED",
|
|
4499
|
+
line: startLine,
|
|
4500
|
+
message: "Markdown tables require a header and at least one data row."
|
|
4501
|
+
});
|
|
4502
|
+
} else if (new Set(widths).size > 1) {
|
|
4503
|
+
issues.push({
|
|
4504
|
+
severity: "error",
|
|
4505
|
+
code: "TABLE_COLUMN_COUNT_MISMATCH",
|
|
4506
|
+
line: startLine,
|
|
4507
|
+
message: `Markdown table rows have inconsistent column counts: ${widths.join(", ")}.`
|
|
4508
|
+
});
|
|
4509
|
+
}
|
|
4510
|
+
blocks.push({
|
|
4511
|
+
type: "table",
|
|
4512
|
+
columns: widths[0] || 0,
|
|
4513
|
+
rows: Math.max(0, widths.length - 1),
|
|
4514
|
+
hasHeader: separatorPresent,
|
|
4515
|
+
markdown: tableLines.join("\n")
|
|
4516
|
+
});
|
|
4517
|
+
continue;
|
|
4518
|
+
}
|
|
4519
|
+
if (kind === "list") {
|
|
4520
|
+
const listLines = [];
|
|
4521
|
+
while (index < lines.length && classifyLine(lines[index]) === "list") {
|
|
4522
|
+
listLines.push(lines[index].trimEnd());
|
|
4523
|
+
index++;
|
|
4524
|
+
}
|
|
4525
|
+
blocks.push({ type: "list", items: listLines.length, markdown: listLines.join("\n") });
|
|
4526
|
+
continue;
|
|
4527
|
+
}
|
|
4528
|
+
const paragraphLines = [];
|
|
4529
|
+
while (index < lines.length && classifyLine(lines[index]) === "paragraph") {
|
|
4530
|
+
paragraphLines.push(lines[index].trim());
|
|
4531
|
+
index++;
|
|
4532
|
+
}
|
|
4533
|
+
const text = paragraphLines.join(" ").trim();
|
|
4534
|
+
blocks.push({ type: "paragraph", text, markdown: text });
|
|
4259
4535
|
}
|
|
4260
|
-
|
|
4536
|
+
const counts = { heading: 0, paragraph: 0, list: 0, table: 0 };
|
|
4537
|
+
for (const block of blocks) counts[block.type] = (counts[block.type] || 0) + 1;
|
|
4538
|
+
const normalizedMarkdown = blocks.map((block) => block.markdown).join("\n\n");
|
|
4539
|
+
return {
|
|
4540
|
+
valid: issues.every((issue) => issue.severity !== "error"),
|
|
4541
|
+
normalizedMarkdown,
|
|
4542
|
+
blocks,
|
|
4543
|
+
issues,
|
|
4544
|
+
counts,
|
|
4545
|
+
requiresStructuredContent: blocks.length > 1 || blocks.some((block) => block.type === "heading" || block.type === "table" || block.type === "list" && block.items > 1)
|
|
4546
|
+
};
|
|
4261
4547
|
}
|
|
4262
|
-
function
|
|
4263
|
-
|
|
4264
|
-
|
|
4265
|
-
|
|
4266
|
-
|
|
4548
|
+
function planStructuredReplacement(target, markdown, options = {}) {
|
|
4549
|
+
const analysis = analyzeStructuredContent(markdown);
|
|
4550
|
+
return {
|
|
4551
|
+
...analysis,
|
|
4552
|
+
operation: analysis.valid ? {
|
|
4553
|
+
type: "replace",
|
|
4554
|
+
target,
|
|
4555
|
+
modified: analysis.normalizedMarkdown,
|
|
4556
|
+
structuredContent: true,
|
|
4557
|
+
...options.author ? { author: options.author } : {},
|
|
4558
|
+
...typeof options.generateRedlines === "boolean" ? { generateRedlines: options.generateRedlines } : {},
|
|
4559
|
+
...options.existingRevisions ? { existingRevisions: options.existingRevisions } : {}
|
|
4560
|
+
} : null
|
|
4561
|
+
};
|
|
4267
4562
|
}
|
|
4268
4563
|
|
|
4269
4564
|
// engine/rpr-helpers.js
|
|
@@ -4306,7 +4601,8 @@ var RPR_SCHEMA_ORDER = [
|
|
|
4306
4601
|
"w:lang",
|
|
4307
4602
|
"w:eastAsianLayout",
|
|
4308
4603
|
"w:specVanish",
|
|
4309
|
-
"w:oMath"
|
|
4604
|
+
"w:oMath",
|
|
4605
|
+
"w:rPrChange"
|
|
4310
4606
|
];
|
|
4311
4607
|
function insertRPrChildInOrder(rPr, el) {
|
|
4312
4608
|
const myIndex = RPR_SCHEMA_ORDER.indexOf(el.nodeName);
|
|
@@ -4415,11 +4711,11 @@ function isFormattingElementEnabled(element, isUnderline) {
|
|
|
4415
4711
|
|
|
4416
4712
|
// engine/format-extraction.js
|
|
4417
4713
|
var NS_W3 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
4418
|
-
function isWordElement2(node,
|
|
4714
|
+
function isWordElement2(node, localName2) {
|
|
4419
4715
|
if (!node || node.nodeType !== 1) return false;
|
|
4420
|
-
if (node.namespaceURI === NS_W3 && node.localName ===
|
|
4716
|
+
if (node.namespaceURI === NS_W3 && node.localName === localName2) return true;
|
|
4421
4717
|
const nodeName = String(node.nodeName || "");
|
|
4422
|
-
return nodeName === `w:${
|
|
4718
|
+
return nodeName === `w:${localName2}` || nodeName === localName2;
|
|
4423
4719
|
}
|
|
4424
4720
|
function isExcludedRevisionContainer(node) {
|
|
4425
4721
|
if (!node || node.nodeType !== 1 || node.namespaceURI !== NS_W3) return false;
|
|
@@ -4449,8 +4745,8 @@ function getDocumentParagraphs(xmlDoc) {
|
|
|
4449
4745
|
return allParagraphs.filter((p) => {
|
|
4450
4746
|
let node = p.parentNode;
|
|
4451
4747
|
while (node && node.nodeName) {
|
|
4452
|
-
const
|
|
4453
|
-
if (excludedContainers.has(
|
|
4748
|
+
const localName2 = String(node.localName || "").toLowerCase();
|
|
4749
|
+
if (excludedContainers.has(localName2)) {
|
|
4454
4750
|
return false;
|
|
4455
4751
|
}
|
|
4456
4752
|
node = node.parentNode;
|
|
@@ -4580,9 +4876,9 @@ function extractFormattingFromOoxml(xmlDoc) {
|
|
|
4580
4876
|
}
|
|
4581
4877
|
|
|
4582
4878
|
// engine/run-builders.js
|
|
4583
|
-
function createTrackChange(xmlDoc, type, run, author) {
|
|
4879
|
+
function createTrackChange(xmlDoc, type, run, author, revisionMetadata = null) {
|
|
4584
4880
|
const wrapper = createWordElement(xmlDoc, type === "ins" ? "w:ins" : "w:del");
|
|
4585
|
-
const metadata = createRevisionMetadata(author, xmlDoc);
|
|
4881
|
+
const metadata = revisionMetadata || createRevisionMetadata(author, xmlDoc, type === "ins" ? "ins" : "del");
|
|
4586
4882
|
wrapper.setAttribute("w:id", String(metadata.id));
|
|
4587
4883
|
wrapper.setAttribute("w:author", metadata.author);
|
|
4588
4884
|
wrapper.setAttribute("w:date", metadata.date);
|
|
@@ -4591,8 +4887,8 @@ function createTrackChange(xmlDoc, type, run, author) {
|
|
|
4591
4887
|
}
|
|
4592
4888
|
return wrapper;
|
|
4593
4889
|
}
|
|
4594
|
-
function getDirectWordChild(node,
|
|
4595
|
-
return Array.from(node?.childNodes || []).find((child) => child.nodeType === 1 && (child.localName ===
|
|
4890
|
+
function getDirectWordChild(node, localName2) {
|
|
4891
|
+
return Array.from(node?.childNodes || []).find((child) => child.nodeType === 1 && (child.localName === localName2 || child.nodeName === `w:${localName2}`)) || null;
|
|
4596
4892
|
}
|
|
4597
4893
|
function ensureParagraphProperties(xmlDoc, paragraph) {
|
|
4598
4894
|
let pPr = getDirectWordChild(paragraph, "pPr");
|
|
@@ -4614,7 +4910,7 @@ function ensureParagraphMarkRunProperties(xmlDoc, pPr) {
|
|
|
4614
4910
|
}
|
|
4615
4911
|
return rPr;
|
|
4616
4912
|
}
|
|
4617
|
-
function markParagraphMark(xmlDoc, paragraph, author, type) {
|
|
4913
|
+
function markParagraphMark(xmlDoc, paragraph, author, type, revisionMetadata = null) {
|
|
4618
4914
|
const pPr = ensureParagraphProperties(xmlDoc, paragraph);
|
|
4619
4915
|
const rPr = ensureParagraphMarkRunProperties(xmlDoc, pPr);
|
|
4620
4916
|
for (const child of Array.from(rPr.childNodes || [])) {
|
|
@@ -4624,18 +4920,18 @@ function markParagraphMark(xmlDoc, paragraph, author, type) {
|
|
|
4624
4920
|
}
|
|
4625
4921
|
}
|
|
4626
4922
|
const marker = createWordElement(xmlDoc, type === "ins" ? "w:ins" : "w:del");
|
|
4627
|
-
const metadata = createRevisionMetadata(author, xmlDoc);
|
|
4923
|
+
const metadata = revisionMetadata || createRevisionMetadata(author, xmlDoc, type === "ins" ? "ins" : "del");
|
|
4628
4924
|
marker.setAttribute("w:id", String(metadata.id));
|
|
4629
4925
|
marker.setAttribute("w:author", metadata.author);
|
|
4630
4926
|
marker.setAttribute("w:date", metadata.date);
|
|
4631
4927
|
rPr.appendChild(marker);
|
|
4632
4928
|
return marker;
|
|
4633
4929
|
}
|
|
4634
|
-
function markParagraphMarkInserted(xmlDoc, paragraph, author) {
|
|
4635
|
-
return markParagraphMark(xmlDoc, paragraph, author, "ins");
|
|
4930
|
+
function markParagraphMarkInserted(xmlDoc, paragraph, author, revisionMetadata = null) {
|
|
4931
|
+
return markParagraphMark(xmlDoc, paragraph, author, "ins", revisionMetadata);
|
|
4636
4932
|
}
|
|
4637
|
-
function markParagraphMarkDeleted(xmlDoc, paragraph, author) {
|
|
4638
|
-
return markParagraphMark(xmlDoc, paragraph, author, "del");
|
|
4933
|
+
function markParagraphMarkDeleted(xmlDoc, paragraph, author, revisionMetadata = null) {
|
|
4934
|
+
return markParagraphMark(xmlDoc, paragraph, author, "del", revisionMetadata);
|
|
4639
4935
|
}
|
|
4640
4936
|
function createTextRun(xmlDoc, text, rPr, isDelete) {
|
|
4641
4937
|
const run = createWordElement(xmlDoc, "w:r");
|
|
@@ -4763,7 +5059,7 @@ function injectFormattingToRPr(xmlDoc, baseRPr, format, author, generateRedlines
|
|
|
4763
5059
|
}
|
|
4764
5060
|
function snapshotAndAttachRPrChange(xmlDoc, rPr, author, dateStr, sourceNode) {
|
|
4765
5061
|
const rPrChange = createWordElement(xmlDoc, "w:rPrChange");
|
|
4766
|
-
const metadata = createRevisionMetadata(author, xmlDoc);
|
|
5062
|
+
const metadata = createRevisionMetadata(author, xmlDoc, "rPrChange");
|
|
4767
5063
|
rPrChange.setAttribute("w:id", String(metadata.id));
|
|
4768
5064
|
rPrChange.setAttribute("w:author", metadata.author);
|
|
4769
5065
|
rPrChange.setAttribute("w:date", dateStr || metadata.date);
|
|
@@ -4788,11 +5084,11 @@ function createRPrChange(xmlDoc, rPr, author, previousRPrArg) {
|
|
|
4788
5084
|
|
|
4789
5085
|
// engine/format-paragraph-targeting.js
|
|
4790
5086
|
var NS_W4 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
4791
|
-
function isWordElement3(node,
|
|
5087
|
+
function isWordElement3(node, localName2) {
|
|
4792
5088
|
if (!node || node.nodeType !== 1) return false;
|
|
4793
|
-
if (node.namespaceURI === NS_W4 && node.localName ===
|
|
5089
|
+
if (node.namespaceURI === NS_W4 && node.localName === localName2) return true;
|
|
4794
5090
|
const nodeName = String(node.nodeName || "");
|
|
4795
|
-
return nodeName === `w:${
|
|
5091
|
+
return nodeName === `w:${localName2}` || nodeName === localName2;
|
|
4796
5092
|
}
|
|
4797
5093
|
function buildParagraphInfos(xmlDoc, paragraphs, textSpans) {
|
|
4798
5094
|
void xmlDoc;
|
|
@@ -4972,11 +5268,11 @@ function splitSpanAtOffset(xmlDoc, span, absoluteOffset) {
|
|
|
4972
5268
|
|
|
4973
5269
|
// engine/format-application.js
|
|
4974
5270
|
var NS_W5 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
4975
|
-
function isWordElement4(node,
|
|
5271
|
+
function isWordElement4(node, localName2) {
|
|
4976
5272
|
if (!node || node.nodeType !== 1) return false;
|
|
4977
|
-
if (node.namespaceURI === NS_W5 && node.localName ===
|
|
5273
|
+
if (node.namespaceURI === NS_W5 && node.localName === localName2) return true;
|
|
4978
5274
|
const nodeName = String(node.nodeName || "");
|
|
4979
|
-
return nodeName === `w:${
|
|
5275
|
+
return nodeName === `w:${localName2}` || nodeName === localName2;
|
|
4980
5276
|
}
|
|
4981
5277
|
function normalizePrecomputedFormatContext(precomputedContext) {
|
|
4982
5278
|
if (Array.isArray(precomputedContext)) {
|
|
@@ -5158,6 +5454,174 @@ function createFormatHintOverlapLookup(formatHints) {
|
|
|
5158
5454
|
};
|
|
5159
5455
|
}
|
|
5160
5456
|
|
|
5457
|
+
// core/paragraph-text.js
|
|
5458
|
+
function localName(node) {
|
|
5459
|
+
return String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
|
|
5460
|
+
}
|
|
5461
|
+
function isNodeVisibleInRevisionView(node, boundary = null, revisionView = "accepted") {
|
|
5462
|
+
const view = revisionView === "current" ? "accepted" : revisionView;
|
|
5463
|
+
let cursor = node;
|
|
5464
|
+
while (cursor && cursor !== boundary) {
|
|
5465
|
+
const name = localName(cursor);
|
|
5466
|
+
if (view === "accepted" && (name === "del" || name === "moveFrom")) return false;
|
|
5467
|
+
if (view === "rejected" && (name === "ins" || name === "moveTo")) return false;
|
|
5468
|
+
cursor = cursor.parentNode;
|
|
5469
|
+
}
|
|
5470
|
+
return true;
|
|
5471
|
+
}
|
|
5472
|
+
function readCanonicalRunText(run, options = {}) {
|
|
5473
|
+
const revisionView = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
|
|
5474
|
+
const boundary = options.boundary || null;
|
|
5475
|
+
if (!isNodeVisibleInRevisionView(run, boundary, revisionView)) return "";
|
|
5476
|
+
let text = "";
|
|
5477
|
+
const visit = (node) => {
|
|
5478
|
+
for (const child of Array.from(node?.childNodes || [])) {
|
|
5479
|
+
if (child?.nodeType !== 1 || child.namespaceURI && child.namespaceURI !== NS_W) continue;
|
|
5480
|
+
if (!isNodeVisibleInRevisionView(child, boundary, revisionView)) continue;
|
|
5481
|
+
const name = localName(child);
|
|
5482
|
+
if (name === "t" || name === "delText" && revisionView === "rejected") text += child.textContent || "";
|
|
5483
|
+
else if (name === "tab") text += " ";
|
|
5484
|
+
else if (name === "br" || name === "cr") text += "\n";
|
|
5485
|
+
else if (name === "noBreakHyphen") text += "\u2011";
|
|
5486
|
+
else if (name === "softHyphen") text += "\xAD";
|
|
5487
|
+
else visit(child);
|
|
5488
|
+
}
|
|
5489
|
+
};
|
|
5490
|
+
visit(run);
|
|
5491
|
+
return text;
|
|
5492
|
+
}
|
|
5493
|
+
var attr = (node, name) => node?.getAttribute?.(`w:${name}`) || node?.getAttribute?.(name) || "";
|
|
5494
|
+
function extractParagraphRevisionSegments(paragraph, options = {}) {
|
|
5495
|
+
if (!paragraph) return [];
|
|
5496
|
+
const rawPieces = [];
|
|
5497
|
+
function walk(node, currentRevision, currentCarrier) {
|
|
5498
|
+
for (const child of Array.from(node?.childNodes || [])) {
|
|
5499
|
+
if (child?.nodeType !== 1) continue;
|
|
5500
|
+
if (child.namespaceURI && child.namespaceURI !== NS_W) continue;
|
|
5501
|
+
const name = localName(child);
|
|
5502
|
+
if (name === "pPr" || name === "rPr") continue;
|
|
5503
|
+
let nextRevision = currentRevision;
|
|
5504
|
+
if (name === "ins") {
|
|
5505
|
+
nextRevision = {
|
|
5506
|
+
kind: "insertion",
|
|
5507
|
+
author: attr(child, "author") || void 0,
|
|
5508
|
+
revisionId: attr(child, "id") || void 0
|
|
5509
|
+
};
|
|
5510
|
+
} else if (name === "del") {
|
|
5511
|
+
nextRevision = {
|
|
5512
|
+
kind: "deletion",
|
|
5513
|
+
author: attr(child, "author") || void 0,
|
|
5514
|
+
revisionId: attr(child, "id") || void 0
|
|
5515
|
+
};
|
|
5516
|
+
} else if (name === "moveFrom") {
|
|
5517
|
+
nextRevision = {
|
|
5518
|
+
kind: "move_from",
|
|
5519
|
+
author: attr(child, "author") || void 0,
|
|
5520
|
+
revisionId: attr(child, "id") || void 0
|
|
5521
|
+
};
|
|
5522
|
+
} else if (name === "moveTo") {
|
|
5523
|
+
nextRevision = {
|
|
5524
|
+
kind: "move_to",
|
|
5525
|
+
author: attr(child, "author") || void 0,
|
|
5526
|
+
revisionId: attr(child, "id") || void 0
|
|
5527
|
+
};
|
|
5528
|
+
}
|
|
5529
|
+
let nextCarrier = currentCarrier;
|
|
5530
|
+
if (name === "r") {
|
|
5531
|
+
nextCarrier = child;
|
|
5532
|
+
}
|
|
5533
|
+
let text = null;
|
|
5534
|
+
let kind = nextRevision ? nextRevision.kind : "baseline";
|
|
5535
|
+
let author = nextRevision?.author;
|
|
5536
|
+
let revisionId = nextRevision?.revisionId;
|
|
5537
|
+
if (name === "t") {
|
|
5538
|
+
text = child.textContent || "";
|
|
5539
|
+
} else if (name === "delText") {
|
|
5540
|
+
text = child.textContent || "";
|
|
5541
|
+
if (!nextRevision) {
|
|
5542
|
+
kind = "deletion";
|
|
5543
|
+
}
|
|
5544
|
+
} else if (name === "tab") {
|
|
5545
|
+
text = " ";
|
|
5546
|
+
} else if (name === "br" || name === "cr") {
|
|
5547
|
+
text = "\n";
|
|
5548
|
+
} else if (name === "noBreakHyphen") {
|
|
5549
|
+
text = "\u2011";
|
|
5550
|
+
} else if (name === "softHyphen") {
|
|
5551
|
+
text = "\xAD";
|
|
5552
|
+
}
|
|
5553
|
+
if (text !== null) {
|
|
5554
|
+
if (text.length > 0) {
|
|
5555
|
+
rawPieces.push({
|
|
5556
|
+
text,
|
|
5557
|
+
kind,
|
|
5558
|
+
author,
|
|
5559
|
+
revisionId,
|
|
5560
|
+
carrier: nextCarrier || child,
|
|
5561
|
+
carrierContainer: (nextCarrier || child)?.parentNode || null
|
|
5562
|
+
});
|
|
5563
|
+
}
|
|
5564
|
+
} else {
|
|
5565
|
+
walk(child, nextRevision, nextCarrier);
|
|
5566
|
+
}
|
|
5567
|
+
}
|
|
5568
|
+
}
|
|
5569
|
+
const initialCarrier = localName(paragraph) === "r" ? paragraph : null;
|
|
5570
|
+
walk(paragraph, null, initialCarrier);
|
|
5571
|
+
if (rawPieces.length === 0) return [];
|
|
5572
|
+
const mergeRuns = options.mergeRuns !== false;
|
|
5573
|
+
const merged = [];
|
|
5574
|
+
let current = null;
|
|
5575
|
+
for (const piece of rawPieces) {
|
|
5576
|
+
if (!current) {
|
|
5577
|
+
current = { ...piece };
|
|
5578
|
+
continue;
|
|
5579
|
+
}
|
|
5580
|
+
const sameKind = current.kind === piece.kind;
|
|
5581
|
+
const sameAuthor = current.author === piece.author;
|
|
5582
|
+
const sameRevisionId = current.revisionId === piece.revisionId;
|
|
5583
|
+
const sameCarrier = current.carrier === piece.carrier;
|
|
5584
|
+
const compatibleContainer = mergeRuns && current.carrierContainer === piece.carrierContainer;
|
|
5585
|
+
if (sameKind && sameAuthor && sameRevisionId && (sameCarrier || compatibleContainer)) {
|
|
5586
|
+
current.text += piece.text;
|
|
5587
|
+
} else {
|
|
5588
|
+
merged.push(current);
|
|
5589
|
+
current = { ...piece };
|
|
5590
|
+
}
|
|
5591
|
+
}
|
|
5592
|
+
if (current) merged.push(current);
|
|
5593
|
+
let acceptedCursor = 0;
|
|
5594
|
+
let rejectedCursor = 0;
|
|
5595
|
+
const segments = [];
|
|
5596
|
+
for (const item of merged) {
|
|
5597
|
+
const isAccepted = item.kind !== "deletion" && item.kind !== "move_from";
|
|
5598
|
+
const isRejected = item.kind !== "insertion" && item.kind !== "move_to";
|
|
5599
|
+
const acceptedStart = isAccepted ? acceptedCursor : null;
|
|
5600
|
+
const rejectedStart = isRejected ? rejectedCursor : null;
|
|
5601
|
+
if (isAccepted) acceptedCursor += item.text.length;
|
|
5602
|
+
if (isRejected) rejectedCursor += item.text.length;
|
|
5603
|
+
const segment = {
|
|
5604
|
+
text: item.text,
|
|
5605
|
+
kind: item.kind,
|
|
5606
|
+
acceptedStart,
|
|
5607
|
+
rejectedStart
|
|
5608
|
+
};
|
|
5609
|
+
if (item.author !== void 0) segment.author = item.author;
|
|
5610
|
+
if (item.revisionId !== void 0) segment.revisionId = item.revisionId;
|
|
5611
|
+
segments.push(segment);
|
|
5612
|
+
}
|
|
5613
|
+
return segments;
|
|
5614
|
+
}
|
|
5615
|
+
function extractCanonicalParagraphText(paragraph, options = {}) {
|
|
5616
|
+
if (!paragraph) return "";
|
|
5617
|
+
const view = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
|
|
5618
|
+
const segments = extractParagraphRevisionSegments(paragraph, options);
|
|
5619
|
+
if (view === "rejected") {
|
|
5620
|
+
return segments.filter((s) => s.rejectedStart !== null).map((s) => s.text).join("");
|
|
5621
|
+
}
|
|
5622
|
+
return segments.filter((s) => s.acceptedStart !== null).map((s) => s.text).join("");
|
|
5623
|
+
}
|
|
5624
|
+
|
|
5161
5625
|
// engine/table-cell-context.js
|
|
5162
5626
|
var W14_NS = "http://schemas.microsoft.com/office/word/2010/wordml";
|
|
5163
5627
|
function detectTableCellContext(xmlDoc, originalText, options = {}) {
|
|
@@ -5193,11 +5657,7 @@ function detectTableCellContext(xmlDoc, originalText, options = {}) {
|
|
|
5193
5657
|
const normalizedTarget = originalText.trim();
|
|
5194
5658
|
if (!targetParagraph) {
|
|
5195
5659
|
for (const p of paragraphsInCells) {
|
|
5196
|
-
const
|
|
5197
|
-
let paragraphText = "";
|
|
5198
|
-
for (const t of textNodes) {
|
|
5199
|
-
paragraphText += t.textContent || "";
|
|
5200
|
-
}
|
|
5660
|
+
const paragraphText = extractCanonicalParagraphText(p);
|
|
5201
5661
|
if (paragraphText.trim() === normalizedTarget) {
|
|
5202
5662
|
targetParagraph = p;
|
|
5203
5663
|
log(`[OxmlEngine] Found target paragraph by text match: "${normalizedTarget.substring(0, 30)}..."`);
|
|
@@ -5246,10 +5706,11 @@ function getRunChildText(child) {
|
|
|
5246
5706
|
if (isWordElement(child, "br") || isWordElement(child, "cr")) return "\n";
|
|
5247
5707
|
if (isWordElement(child, "tab")) return " ";
|
|
5248
5708
|
if (isWordElement(child, "noBreakHyphen")) return "\u2011";
|
|
5709
|
+
if (isWordElement(child, "softHyphen")) return "\xAD";
|
|
5249
5710
|
return "";
|
|
5250
5711
|
}
|
|
5251
5712
|
function isTextLikeRunChild(child) {
|
|
5252
|
-
return isWordElement(child, "t") || isWordElement(child, "br") || isWordElement(child, "cr") || isWordElement(child, "tab") || isWordElement(child, "noBreakHyphen");
|
|
5713
|
+
return isWordElement(child, "t") || isWordElement(child, "br") || isWordElement(child, "cr") || isWordElement(child, "tab") || isWordElement(child, "noBreakHyphen") || isWordElement(child, "softHyphen");
|
|
5253
5714
|
}
|
|
5254
5715
|
function buildSurgicalTextSpans(paragraphs) {
|
|
5255
5716
|
let fullText = "";
|
|
@@ -5265,6 +5726,27 @@ function buildSurgicalTextSpans(paragraphs) {
|
|
|
5265
5726
|
fullText += processRunElement(hc, paragraph, container, fullText.length, textSpans).text;
|
|
5266
5727
|
}
|
|
5267
5728
|
}
|
|
5729
|
+
} else if (isWordElement(child, "ins")) {
|
|
5730
|
+
for (let ic = child.firstChild; ic; ic = ic.nextSibling) {
|
|
5731
|
+
if (isWordElement(ic, "r")) {
|
|
5732
|
+
fullText += processRunElement(ic, paragraph, container, fullText.length, textSpans).text;
|
|
5733
|
+
}
|
|
5734
|
+
}
|
|
5735
|
+
} else if (isWordElement(child, "sdt")) {
|
|
5736
|
+
const sdtContent = Array.from(child.childNodes || []).find((n) => isWordElement(n, "sdtContent"));
|
|
5737
|
+
if (sdtContent) {
|
|
5738
|
+
for (let sc = sdtContent.firstChild; sc; sc = sc.nextSibling) {
|
|
5739
|
+
if (isWordElement(sc, "r")) {
|
|
5740
|
+
fullText += processRunElement(sc, paragraph, container, fullText.length, textSpans).text;
|
|
5741
|
+
}
|
|
5742
|
+
}
|
|
5743
|
+
}
|
|
5744
|
+
} else if (isWordElement(child, "smartTag")) {
|
|
5745
|
+
for (let st = child.firstChild; st; st = st.nextSibling) {
|
|
5746
|
+
if (isWordElement(st, "r")) {
|
|
5747
|
+
fullText += processRunElement(st, paragraph, container, fullText.length, textSpans).text;
|
|
5748
|
+
}
|
|
5749
|
+
}
|
|
5268
5750
|
}
|
|
5269
5751
|
}
|
|
5270
5752
|
fullText = appendParagraphBoundary(fullText, paragraphIndex, paragraphs.length);
|
|
@@ -5374,6 +5856,28 @@ function lowerBound(values, target) {
|
|
|
5374
5856
|
}
|
|
5375
5857
|
return left;
|
|
5376
5858
|
}
|
|
5859
|
+
function describeInsertionBoundary(spanIndex, pos, fallbackParagraph = null) {
|
|
5860
|
+
if (!spanIndex || !spanIndex.spans || spanIndex.spans.length === 0) {
|
|
5861
|
+
return {
|
|
5862
|
+
leftSpan: null,
|
|
5863
|
+
rightSpan: null,
|
|
5864
|
+
containingSpan: null,
|
|
5865
|
+
isInterior: false,
|
|
5866
|
+
fallbackParagraph
|
|
5867
|
+
};
|
|
5868
|
+
}
|
|
5869
|
+
const containingSpan = findContainingSpan(spanIndex, pos);
|
|
5870
|
+
const isInterior = containingSpan !== null && pos > containingSpan.charStart && pos < containingSpan.charEnd;
|
|
5871
|
+
const leftSpan = findFirstSpanEndingAt(spanIndex, pos) || (pos > 0 ? findLastSpanEndingBeforeOrAt(spanIndex, pos) : null);
|
|
5872
|
+
const rightSpan = spanIndex.spans.find((s) => s.charStart === pos) || (pos === 0 ? spanIndex.spans[0] : null);
|
|
5873
|
+
return {
|
|
5874
|
+
leftSpan,
|
|
5875
|
+
rightSpan,
|
|
5876
|
+
containingSpan,
|
|
5877
|
+
isInterior,
|
|
5878
|
+
fallbackParagraph
|
|
5879
|
+
};
|
|
5880
|
+
}
|
|
5377
5881
|
|
|
5378
5882
|
// engine/surgical-run-splitting.js
|
|
5379
5883
|
function getRunContentPieces(runElement) {
|
|
@@ -5487,7 +5991,7 @@ function reconcileFormattingForTextSpan(xmlDoc, span, start, end, applicableHint
|
|
|
5487
5991
|
parent.removeChild(span.runElement);
|
|
5488
5992
|
return true;
|
|
5489
5993
|
}
|
|
5490
|
-
function processDelete(xmlDoc, spanIndex, startPos, endPos, author, generateRedlines) {
|
|
5994
|
+
function processDelete(xmlDoc, spanIndex, startPos, endPos, author, generateRedlines, revisionMetadata = null) {
|
|
5491
5995
|
const spans = [];
|
|
5492
5996
|
forEachOverlappingSpan(spanIndex, startPos, endPos, (span) => {
|
|
5493
5997
|
spans.push(span);
|
|
@@ -5500,6 +6004,7 @@ function processDelete(xmlDoc, spanIndex, startPos, endPos, author, generateRedl
|
|
|
5500
6004
|
spansByRun.get(span.runElement).push(span);
|
|
5501
6005
|
});
|
|
5502
6006
|
let changed = false;
|
|
6007
|
+
let usedDelMetadata = false;
|
|
5503
6008
|
spansByRun.forEach((runSpans, runElement) => {
|
|
5504
6009
|
const parent = runElement.parentNode;
|
|
5505
6010
|
if (!parent) return;
|
|
@@ -5523,7 +6028,9 @@ function processDelete(xmlDoc, spanIndex, startPos, endPos, author, generateRedl
|
|
|
5523
6028
|
insertRunPiecesBefore(xmlDoc, parent, runElement, beforePieces, runSpans[0].rPr);
|
|
5524
6029
|
if (generateRedlines && deletedPieces.length > 0) {
|
|
5525
6030
|
const delRun = createRunFromPieces(xmlDoc, deletedPieces, runSpans[0].rPr);
|
|
5526
|
-
const
|
|
6031
|
+
const metadata = revisionMetadata ? usedDelMetadata ? { ...revisionMetadata, id: createRevisionMetadata(author, xmlDoc).id } : revisionMetadata : null;
|
|
6032
|
+
usedDelMetadata = true;
|
|
6033
|
+
const delWrapper = createTrackChange(xmlDoc, "del", delRun, author, metadata);
|
|
5527
6034
|
parent.insertBefore(delWrapper, runElement);
|
|
5528
6035
|
}
|
|
5529
6036
|
insertRunPiecesBefore(xmlDoc, parent, runElement, afterPieces, runSpans[0].rPr);
|
|
@@ -5532,50 +6039,200 @@ function processDelete(xmlDoc, spanIndex, startPos, endPos, author, generateRedl
|
|
|
5532
6039
|
});
|
|
5533
6040
|
return changed;
|
|
5534
6041
|
}
|
|
5535
|
-
function processInsert(xmlDoc, spanIndex, pos, text, author, formatHints = [], insertOffset = 0, generateRedlines = true, fallbackParagraph = null) {
|
|
5536
|
-
|
|
5537
|
-
|
|
5538
|
-
targetSpan
|
|
6042
|
+
function processInsert(xmlDoc, spanIndex, pos, text, author, formatHints = [], insertOffset = 0, generateRedlines = true, fallbackParagraph = null, revisionMetadata = null, affinity = null) {
|
|
6043
|
+
if (!affinity) {
|
|
6044
|
+
let targetSpan = findContainingSpan(spanIndex, pos);
|
|
6045
|
+
if (!targetSpan && pos > 0) {
|
|
6046
|
+
targetSpan = findFirstSpanEndingAt(spanIndex, pos);
|
|
6047
|
+
}
|
|
6048
|
+
if (!targetSpan && pos > 0) {
|
|
6049
|
+
targetSpan = findLastSpanEndingBeforeOrAt(spanIndex, pos);
|
|
6050
|
+
}
|
|
6051
|
+
if (!targetSpan && spanIndex.spans.length > 0) {
|
|
6052
|
+
targetSpan = spanIndex.spans[spanIndex.spans.length - 1];
|
|
6053
|
+
}
|
|
6054
|
+
if (!targetSpan) {
|
|
6055
|
+
if (!fallbackParagraph) return false;
|
|
6056
|
+
insertTextRuns(xmlDoc, fallbackParagraph, null, text, null, author, formatHints, insertOffset, generateRedlines, revisionMetadata);
|
|
6057
|
+
return true;
|
|
6058
|
+
}
|
|
6059
|
+
const parent2 = targetSpan.runElement.parentNode;
|
|
6060
|
+
if (!parent2) {
|
|
6061
|
+
if (!fallbackParagraph) return false;
|
|
6062
|
+
insertTextRuns(xmlDoc, fallbackParagraph, null, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines, revisionMetadata);
|
|
6063
|
+
return true;
|
|
6064
|
+
}
|
|
6065
|
+
const pieces = getRunContentPieces(targetSpan.runElement);
|
|
6066
|
+
const targetPiece = pieces.find((piece) => piece.node === targetSpan.textElement);
|
|
6067
|
+
const localInsertPos = targetPiece ? targetPiece.start + Math.max(0, Math.min(pos - targetSpan.charStart, targetSpan.charEnd - targetSpan.charStart)) : pos <= targetSpan.charStart ? 0 : getRunTextLength(pieces);
|
|
6068
|
+
if (localInsertPos > 0 && localInsertPos < getRunTextLength(pieces)) {
|
|
6069
|
+
const beforePieces = sliceRunPieces(xmlDoc, pieces, 0, localInsertPos, false);
|
|
6070
|
+
const afterPieces = sliceRunPieces(xmlDoc, pieces, localInsertPos, getRunTextLength(pieces), false);
|
|
6071
|
+
insertRunPiecesBefore(xmlDoc, parent2, targetSpan.runElement, beforePieces, targetSpan.rPr);
|
|
6072
|
+
insertTextRuns(xmlDoc, parent2, targetSpan.runElement, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines, revisionMetadata);
|
|
6073
|
+
insertRunPiecesBefore(xmlDoc, parent2, targetSpan.runElement, afterPieces, targetSpan.rPr);
|
|
6074
|
+
parent2.removeChild(targetSpan.runElement);
|
|
6075
|
+
return true;
|
|
6076
|
+
}
|
|
6077
|
+
const referenceNode2 = pos <= targetSpan.charStart ? targetSpan.runElement : targetSpan.runElement.nextSibling;
|
|
6078
|
+
insertTextRuns(xmlDoc, parent2, referenceNode2, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines, revisionMetadata);
|
|
6079
|
+
return true;
|
|
5539
6080
|
}
|
|
5540
|
-
|
|
5541
|
-
|
|
6081
|
+
const boundary = describeInsertionBoundary(spanIndex, pos, fallbackParagraph);
|
|
6082
|
+
const isLeftInHyperlink = boundary.leftSpan && isWordElement(boundary.leftSpan.runElement?.parentNode, "hyperlink");
|
|
6083
|
+
const isRightInHyperlink = boundary.rightSpan && isWordElement(boundary.rightSpan.runElement?.parentNode, "hyperlink");
|
|
6084
|
+
const isContainingInHyperlink = boundary.containingSpan && isWordElement(boundary.containingSpan.runElement?.parentNode, "hyperlink");
|
|
6085
|
+
if (affinity.hyperlink === "outside") {
|
|
6086
|
+
if (boundary.isInterior && isContainingInHyperlink) {
|
|
6087
|
+
return {
|
|
6088
|
+
error: {
|
|
6089
|
+
code: "UNSUPPORTED_INSERTION_AFFINITY",
|
|
6090
|
+
message: "Cannot place insertion outside hyperlink from strictly interior position."
|
|
6091
|
+
}
|
|
6092
|
+
};
|
|
6093
|
+
}
|
|
6094
|
+
} else if (affinity.hyperlink === "inside") {
|
|
6095
|
+
if (!isLeftInHyperlink && !isRightInHyperlink && !isContainingInHyperlink) {
|
|
6096
|
+
return {
|
|
6097
|
+
error: {
|
|
6098
|
+
code: "UNSUPPORTED_INSERTION_AFFINITY",
|
|
6099
|
+
message: "Cannot place insertion inside hyperlink when no hyperlink is present at boundary."
|
|
6100
|
+
}
|
|
6101
|
+
};
|
|
6102
|
+
}
|
|
5542
6103
|
}
|
|
5543
|
-
|
|
5544
|
-
|
|
6104
|
+
let baseRPr = null;
|
|
6105
|
+
if (affinity.formatting === "none") {
|
|
6106
|
+
baseRPr = null;
|
|
6107
|
+
} else if (affinity.formatting === "right") {
|
|
6108
|
+
baseRPr = boundary.rightSpan?.rPr || null;
|
|
6109
|
+
} else if (affinity.formatting === "left") {
|
|
6110
|
+
baseRPr = boundary.leftSpan?.rPr || null;
|
|
6111
|
+
} else {
|
|
6112
|
+
baseRPr = (boundary.containingSpan || boundary.leftSpan || boundary.rightSpan)?.rPr || null;
|
|
6113
|
+
}
|
|
6114
|
+
if (boundary.isInterior) {
|
|
6115
|
+
const targetSpan = boundary.containingSpan;
|
|
6116
|
+
const parent2 = targetSpan.runElement.parentNode || fallbackParagraph;
|
|
6117
|
+
if (!parent2) return false;
|
|
6118
|
+
const pieces = getRunContentPieces(targetSpan.runElement);
|
|
6119
|
+
const targetPiece = pieces.find((piece) => piece.node === targetSpan.textElement);
|
|
6120
|
+
const localInsertPos = targetPiece ? targetPiece.start + Math.max(0, Math.min(pos - targetSpan.charStart, targetSpan.charEnd - targetSpan.charStart)) : pos <= targetSpan.charStart ? 0 : getRunTextLength(pieces);
|
|
6121
|
+
if (localInsertPos > 0 && localInsertPos < getRunTextLength(pieces)) {
|
|
6122
|
+
const beforePieces = sliceRunPieces(xmlDoc, pieces, 0, localInsertPos, false);
|
|
6123
|
+
const afterPieces = sliceRunPieces(xmlDoc, pieces, localInsertPos, getRunTextLength(pieces), false);
|
|
6124
|
+
insertRunPiecesBefore(xmlDoc, parent2, targetSpan.runElement, beforePieces, targetSpan.rPr);
|
|
6125
|
+
insertTextRuns(xmlDoc, parent2, targetSpan.runElement, text, baseRPr, author, formatHints, insertOffset, generateRedlines, revisionMetadata);
|
|
6126
|
+
insertRunPiecesBefore(xmlDoc, parent2, targetSpan.runElement, afterPieces, targetSpan.rPr);
|
|
6127
|
+
parent2.removeChild(targetSpan.runElement);
|
|
6128
|
+
return true;
|
|
6129
|
+
}
|
|
5545
6130
|
}
|
|
5546
|
-
|
|
5547
|
-
|
|
5548
|
-
|
|
5549
|
-
|
|
6131
|
+
let parent = null;
|
|
6132
|
+
let referenceNode = null;
|
|
6133
|
+
if (affinity.hyperlink === "outside") {
|
|
6134
|
+
if (isRightInHyperlink) {
|
|
6135
|
+
const hyperlinkNode = boundary.rightSpan.runElement.parentNode;
|
|
6136
|
+
parent = hyperlinkNode.parentNode || fallbackParagraph;
|
|
6137
|
+
referenceNode = hyperlinkNode;
|
|
6138
|
+
} else if (isLeftInHyperlink) {
|
|
6139
|
+
const hyperlinkNode = boundary.leftSpan.runElement.parentNode;
|
|
6140
|
+
parent = hyperlinkNode.parentNode || fallbackParagraph;
|
|
6141
|
+
referenceNode = hyperlinkNode.nextSibling;
|
|
6142
|
+
}
|
|
6143
|
+
} else if (affinity.hyperlink === "inside") {
|
|
6144
|
+
if (isRightInHyperlink) {
|
|
6145
|
+
parent = boundary.rightSpan.runElement.parentNode;
|
|
6146
|
+
referenceNode = boundary.rightSpan.runElement;
|
|
6147
|
+
} else if (isLeftInHyperlink) {
|
|
6148
|
+
parent = boundary.leftSpan.runElement.parentNode;
|
|
6149
|
+
referenceNode = boundary.leftSpan.runElement.nextSibling;
|
|
6150
|
+
}
|
|
5550
6151
|
}
|
|
5551
|
-
const parent = targetSpan.runElement.parentNode;
|
|
5552
6152
|
if (!parent) {
|
|
5553
|
-
if (
|
|
5554
|
-
|
|
5555
|
-
|
|
6153
|
+
if (boundary.rightSpan) {
|
|
6154
|
+
parent = boundary.rightSpan.runElement.parentNode || fallbackParagraph;
|
|
6155
|
+
referenceNode = boundary.rightSpan.runElement;
|
|
6156
|
+
} else if (boundary.leftSpan) {
|
|
6157
|
+
parent = boundary.leftSpan.runElement.parentNode || fallbackParagraph;
|
|
6158
|
+
referenceNode = boundary.leftSpan.runElement.nextSibling;
|
|
6159
|
+
} else {
|
|
6160
|
+
parent = fallbackParagraph;
|
|
6161
|
+
referenceNode = null;
|
|
6162
|
+
}
|
|
5556
6163
|
}
|
|
5557
|
-
|
|
5558
|
-
|
|
5559
|
-
|
|
5560
|
-
|
|
5561
|
-
|
|
5562
|
-
|
|
5563
|
-
|
|
5564
|
-
|
|
5565
|
-
|
|
5566
|
-
|
|
5567
|
-
|
|
6164
|
+
if (affinity.bookmark && parent) {
|
|
6165
|
+
if (affinity.bookmark === "outside") {
|
|
6166
|
+
if (referenceNode && isWordElement(referenceNode.previousSibling, "bookmarkStart")) {
|
|
6167
|
+
referenceNode = referenceNode.previousSibling;
|
|
6168
|
+
}
|
|
6169
|
+
if (boundary.leftSpan && isWordElement(boundary.leftSpan.runElement.nextSibling, "bookmarkEnd")) {
|
|
6170
|
+
referenceNode = boundary.leftSpan.runElement.nextSibling.nextSibling;
|
|
6171
|
+
}
|
|
6172
|
+
} else if (affinity.bookmark === "inside") {
|
|
6173
|
+
if (referenceNode && isWordElement(referenceNode, "bookmarkStart")) {
|
|
6174
|
+
referenceNode = referenceNode.nextSibling;
|
|
6175
|
+
}
|
|
6176
|
+
if (boundary.leftSpan && isWordElement(boundary.leftSpan.runElement.nextSibling, "bookmarkEnd")) {
|
|
6177
|
+
referenceNode = boundary.leftSpan.runElement.nextSibling;
|
|
6178
|
+
}
|
|
6179
|
+
}
|
|
6180
|
+
}
|
|
6181
|
+
if (affinity.comment && parent) {
|
|
6182
|
+
if (affinity.comment === "outside") {
|
|
6183
|
+
if (referenceNode && isWordElement(referenceNode.previousSibling, "commentRangeStart")) {
|
|
6184
|
+
referenceNode = referenceNode.previousSibling;
|
|
6185
|
+
}
|
|
6186
|
+
if (boundary.leftSpan && isWordElement(boundary.leftSpan.runElement.nextSibling, "commentRangeEnd")) {
|
|
6187
|
+
let afterComment = boundary.leftSpan.runElement.nextSibling.nextSibling;
|
|
6188
|
+
if (afterComment && (isWordElement(afterComment, "commentReference") || isWordElement(afterComment, "r"))) {
|
|
6189
|
+
const hasCRef = Array.from(afterComment.childNodes || []).some((n) => isWordElement(n, "commentReference"));
|
|
6190
|
+
if (hasCRef) afterComment = afterComment.nextSibling;
|
|
6191
|
+
}
|
|
6192
|
+
referenceNode = afterComment;
|
|
6193
|
+
}
|
|
6194
|
+
} else if (affinity.comment === "inside") {
|
|
6195
|
+
if (referenceNode && isWordElement(referenceNode, "commentRangeStart")) {
|
|
6196
|
+
referenceNode = referenceNode.nextSibling;
|
|
6197
|
+
}
|
|
6198
|
+
if (boundary.leftSpan && isWordElement(boundary.leftSpan.runElement.nextSibling, "commentRangeEnd")) {
|
|
6199
|
+
referenceNode = boundary.leftSpan.runElement.nextSibling;
|
|
6200
|
+
}
|
|
6201
|
+
}
|
|
6202
|
+
}
|
|
6203
|
+
if (generateRedlines && affinity.revision === "coalesce_same_author") {
|
|
6204
|
+
let insElem = null;
|
|
6205
|
+
let insRef = null;
|
|
6206
|
+
if (boundary.leftSpan && isWordElement(boundary.leftSpan.runElement.parentNode, "ins")) {
|
|
6207
|
+
const candidate = boundary.leftSpan.runElement.parentNode;
|
|
6208
|
+
const candAuthor = candidate.getAttribute("w:author") || candidate.getAttributeNS(NS_W, "author");
|
|
6209
|
+
if (candAuthor === author) {
|
|
6210
|
+
insElem = candidate;
|
|
6211
|
+
insRef = boundary.leftSpan.runElement.nextSibling;
|
|
6212
|
+
}
|
|
6213
|
+
} else if (boundary.rightSpan && isWordElement(boundary.rightSpan.runElement.parentNode, "ins")) {
|
|
6214
|
+
const candidate = boundary.rightSpan.runElement.parentNode;
|
|
6215
|
+
const candAuthor = candidate.getAttribute("w:author") || candidate.getAttributeNS(NS_W, "author");
|
|
6216
|
+
if (candAuthor === author) {
|
|
6217
|
+
insElem = candidate;
|
|
6218
|
+
insRef = boundary.rightSpan.runElement;
|
|
6219
|
+
}
|
|
6220
|
+
}
|
|
6221
|
+
if (insElem) {
|
|
6222
|
+
const insRun = createTextRun(xmlDoc, text, baseRPr, false);
|
|
6223
|
+
insElem.insertBefore(insRun, insRef);
|
|
6224
|
+
return true;
|
|
6225
|
+
}
|
|
5568
6226
|
}
|
|
5569
|
-
|
|
5570
|
-
insertTextRuns(xmlDoc, parent, referenceNode, text, targetSpan.rPr, author, formatHints, insertOffset, generateRedlines);
|
|
6227
|
+
insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, formatHints, insertOffset, generateRedlines, revisionMetadata);
|
|
5571
6228
|
return true;
|
|
5572
6229
|
}
|
|
5573
|
-
function insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, formatHints, insertOffset, generateRedlines) {
|
|
6230
|
+
function insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, formatHints, insertOffset, generateRedlines, revisionMetadata = null) {
|
|
5574
6231
|
const applicableHints = getApplicableFormatHints(formatHints, insertOffset, insertOffset + text.length);
|
|
5575
6232
|
if (applicableHints.length === 0) {
|
|
5576
6233
|
const insRun = createTextRun(xmlDoc, text, baseRPr, false);
|
|
5577
6234
|
if (generateRedlines) {
|
|
5578
|
-
const insWrapper = createTrackChange(xmlDoc, "ins", insRun, author);
|
|
6235
|
+
const insWrapper = createTrackChange(xmlDoc, "ins", insRun, author, revisionMetadata);
|
|
5579
6236
|
parent.insertBefore(insWrapper, referenceNode);
|
|
5580
6237
|
} else {
|
|
5581
6238
|
parent.insertBefore(insRun, referenceNode);
|
|
@@ -5584,7 +6241,7 @@ function insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, fo
|
|
|
5584
6241
|
}
|
|
5585
6242
|
const runs = createFormattedRuns(xmlDoc, text, baseRPr, applicableHints, insertOffset, author, generateRedlines);
|
|
5586
6243
|
if (generateRedlines) {
|
|
5587
|
-
const insWrapper = createTrackChange(xmlDoc, "ins", null, author);
|
|
6244
|
+
const insWrapper = createTrackChange(xmlDoc, "ins", null, author, revisionMetadata);
|
|
5588
6245
|
runs.forEach((run) => insWrapper.appendChild(run));
|
|
5589
6246
|
parent.insertBefore(insWrapper, referenceNode);
|
|
5590
6247
|
} else {
|
|
@@ -5593,16 +6250,82 @@ function insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, fo
|
|
|
5593
6250
|
}
|
|
5594
6251
|
|
|
5595
6252
|
// engine/surgical-mode.js
|
|
5596
|
-
function
|
|
5597
|
-
|
|
6253
|
+
function checkSafeAdjacencyForPairing(spanIndex, startPos, endPos) {
|
|
6254
|
+
const spans = [];
|
|
6255
|
+
forEachOverlappingSpan(spanIndex, startPos, endPos, (span) => spans.push(span));
|
|
6256
|
+
if (spans.length === 0) return { safe: false };
|
|
6257
|
+
const firstRun = spans[0].runElement;
|
|
6258
|
+
const parent = firstRun?.parentNode;
|
|
6259
|
+
if (!parent) return { safe: false };
|
|
6260
|
+
const sameParent = spans.every((s) => s.runElement?.parentNode === parent);
|
|
6261
|
+
if (!sameParent) return { safe: false, structuralBoundary: true };
|
|
6262
|
+
const parentLocal = parent.localName || parent.nodeName.replace(/^.*:/, "");
|
|
6263
|
+
if (["hyperlink", "sdt", "ins", "del", "moveFrom", "moveTo"].includes(parentLocal)) {
|
|
6264
|
+
return { safe: false, structuralBoundary: true };
|
|
6265
|
+
}
|
|
6266
|
+
const structuralTags = /* @__PURE__ */ new Set([
|
|
6267
|
+
"hyperlink",
|
|
6268
|
+
"fldSimple",
|
|
6269
|
+
"sdt",
|
|
6270
|
+
"commentRangeStart",
|
|
6271
|
+
"commentRangeEnd",
|
|
6272
|
+
"commentReference",
|
|
6273
|
+
"bookmarkStart",
|
|
6274
|
+
"bookmarkEnd",
|
|
6275
|
+
"moveFrom",
|
|
6276
|
+
"moveTo",
|
|
6277
|
+
"ins",
|
|
6278
|
+
"del"
|
|
6279
|
+
]);
|
|
6280
|
+
for (const span of spans) {
|
|
6281
|
+
const run = span.runElement;
|
|
6282
|
+
for (const child of Array.from(run.childNodes || [])) {
|
|
6283
|
+
if (child.nodeType === 1) {
|
|
6284
|
+
const tag = child.localName || child.nodeName.replace(/^.*:/, "");
|
|
6285
|
+
if (structuralTags.has(tag) || tag === "fldChar") {
|
|
6286
|
+
return { safe: false, structuralBoundary: true };
|
|
6287
|
+
}
|
|
6288
|
+
}
|
|
6289
|
+
}
|
|
6290
|
+
}
|
|
6291
|
+
const lastRun = spans[spans.length - 1].runElement;
|
|
6292
|
+
let curr = firstRun;
|
|
6293
|
+
while (curr && curr !== lastRun) {
|
|
6294
|
+
if (curr !== firstRun) {
|
|
6295
|
+
const tag = curr.localName || curr.nodeName.replace(/^.*:/, "");
|
|
6296
|
+
if (structuralTags.has(tag)) {
|
|
6297
|
+
return { safe: false, structuralBoundary: true };
|
|
6298
|
+
}
|
|
6299
|
+
}
|
|
6300
|
+
curr = curr.nextSibling;
|
|
6301
|
+
}
|
|
6302
|
+
function hasStructuralDescendant(node) {
|
|
6303
|
+
if (!node || node.nodeType !== 1) return false;
|
|
6304
|
+
const tag = node.localName || node.nodeName.replace(/^.*:/, "");
|
|
6305
|
+
if (structuralTags.has(tag) || tag === "fldChar") return true;
|
|
6306
|
+
for (const child of Array.from(node.childNodes || [])) {
|
|
6307
|
+
if (child.nodeType === 1 && hasStructuralDescendant(child)) return true;
|
|
6308
|
+
}
|
|
6309
|
+
return false;
|
|
6310
|
+
}
|
|
6311
|
+
if (hasStructuralDescendant(firstRun.previousSibling) || hasStructuralDescendant(lastRun.nextSibling)) {
|
|
6312
|
+
return { safe: false, structuralBoundary: true };
|
|
6313
|
+
}
|
|
6314
|
+
return { safe: true };
|
|
6315
|
+
}
|
|
6316
|
+
function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true, targetParagraph = null, diffOptions = {}, options = {}) {
|
|
6317
|
+
void originalText;
|
|
5598
6318
|
const allParagraphs = targetParagraph ? [targetParagraph] : getDocumentParagraphs(xmlDoc);
|
|
5599
6319
|
const { fullText, textSpans } = buildSurgicalTextSpans(allParagraphs);
|
|
5600
6320
|
const diffs = computeWordDiffs(fullText, modifiedText, diffOptions);
|
|
5601
6321
|
const spanIndex = buildSpanIndex(textSpans);
|
|
6322
|
+
const pairReplacements = options.pairReplacements === true;
|
|
6323
|
+
const warnings = [];
|
|
5602
6324
|
let originalPos = 0;
|
|
5603
6325
|
let newPos = 0;
|
|
5604
6326
|
let hasChanges = false;
|
|
5605
|
-
for (
|
|
6327
|
+
for (let i = 0; i < diffs.length; i++) {
|
|
6328
|
+
const [op, text] = diffs[i];
|
|
5606
6329
|
if (op === 0) {
|
|
5607
6330
|
const len = text.length;
|
|
5608
6331
|
const startPos = originalPos;
|
|
@@ -5622,21 +6345,73 @@ function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, autho
|
|
|
5622
6345
|
originalPos += len;
|
|
5623
6346
|
newPos += len;
|
|
5624
6347
|
} else if (op === -1) {
|
|
5625
|
-
|
|
6348
|
+
const hasNextInsert = i + 1 < diffs.length && diffs[i + 1][0] === 1;
|
|
6349
|
+
let paired = false;
|
|
6350
|
+
let delMetadata = null;
|
|
6351
|
+
let insMetadata = null;
|
|
6352
|
+
if (pairReplacements && generateRedlines && hasNextInsert) {
|
|
6353
|
+
const nextText = diffs[i + 1][1];
|
|
6354
|
+
const textWithoutNewlines = nextText.replace(/\n/g, " ");
|
|
6355
|
+
if (textWithoutNewlines.trim().length > 0) {
|
|
6356
|
+
const checkResult = checkSafeAdjacencyForPairing(spanIndex, originalPos, originalPos + text.length);
|
|
6357
|
+
if (checkResult.safe) {
|
|
6358
|
+
const event = createReplacementRevisionEvent(author, xmlDoc);
|
|
6359
|
+
delMetadata = { id: event.deletionId, author: event.author, date: event.date };
|
|
6360
|
+
insMetadata = { id: event.insertionId, author: event.author, date: event.date };
|
|
6361
|
+
paired = true;
|
|
6362
|
+
} else if (checkResult.structuralBoundary) {
|
|
6363
|
+
warnings.push("PAIRING_SKIPPED_STRUCTURAL_BOUNDARY");
|
|
6364
|
+
}
|
|
6365
|
+
}
|
|
6366
|
+
}
|
|
6367
|
+
if (processDelete(xmlDoc, spanIndex, originalPos, originalPos + text.length, author, generateRedlines, delMetadata)) {
|
|
5626
6368
|
hasChanges = true;
|
|
5627
6369
|
}
|
|
5628
6370
|
originalPos += text.length;
|
|
6371
|
+
if (paired) {
|
|
6372
|
+
i++;
|
|
6373
|
+
const [, nextText] = diffs[i];
|
|
6374
|
+
const textWithoutNewlines = nextText.replace(/\n/g, " ");
|
|
6375
|
+
if (textWithoutNewlines.trim().length > 0) {
|
|
6376
|
+
const insertResult = processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null, insMetadata, options?.insertionAffinity || null);
|
|
6377
|
+
if (insertResult && typeof insertResult === "object" && insertResult.error) {
|
|
6378
|
+
return withOoxmlSourceType({
|
|
6379
|
+
oxml: serializer.serializeToString(xmlDoc),
|
|
6380
|
+
hasChanges: false,
|
|
6381
|
+
status: "error",
|
|
6382
|
+
error: insertResult.error
|
|
6383
|
+
});
|
|
6384
|
+
}
|
|
6385
|
+
if (insertResult === true) {
|
|
6386
|
+
hasChanges = true;
|
|
6387
|
+
}
|
|
6388
|
+
}
|
|
6389
|
+
newPos += nextText.length;
|
|
6390
|
+
}
|
|
5629
6391
|
} else if (op === 1) {
|
|
5630
6392
|
const textWithoutNewlines = text.replace(/\n/g, " ");
|
|
5631
6393
|
if (textWithoutNewlines.trim().length > 0) {
|
|
5632
|
-
|
|
6394
|
+
const insertResult = processInsert(xmlDoc, spanIndex, originalPos, textWithoutNewlines, author, formatHints, newPos, generateRedlines, allParagraphs[0] || null, null, options?.insertionAffinity || null);
|
|
6395
|
+
if (insertResult && typeof insertResult === "object" && insertResult.error) {
|
|
6396
|
+
return withOoxmlSourceType({
|
|
6397
|
+
oxml: serializer.serializeToString(xmlDoc),
|
|
6398
|
+
hasChanges: false,
|
|
6399
|
+
status: "error",
|
|
6400
|
+
error: insertResult.error
|
|
6401
|
+
});
|
|
6402
|
+
}
|
|
6403
|
+
if (insertResult === true) {
|
|
5633
6404
|
hasChanges = true;
|
|
5634
6405
|
}
|
|
5635
6406
|
}
|
|
5636
6407
|
newPos += text.length;
|
|
5637
6408
|
}
|
|
5638
6409
|
}
|
|
5639
|
-
return withOoxmlSourceType({
|
|
6410
|
+
return withOoxmlSourceType({
|
|
6411
|
+
oxml: serializer.serializeToString(xmlDoc),
|
|
6412
|
+
hasChanges,
|
|
6413
|
+
...warnings.length > 0 ? { warnings: [...new Set(warnings)] } : {}
|
|
6414
|
+
});
|
|
5640
6415
|
}
|
|
5641
6416
|
|
|
5642
6417
|
// engine/reconstruction-mapper.js
|
|
@@ -5645,8 +6420,8 @@ var DMP = new import_diff_match_patch2.diff_match_patch();
|
|
|
5645
6420
|
function localNameOf(node) {
|
|
5646
6421
|
return String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
|
|
5647
6422
|
}
|
|
5648
|
-
function wordAttribute(node,
|
|
5649
|
-
return node?.getAttributeNS?.(NS_W,
|
|
6423
|
+
function wordAttribute(node, localName2) {
|
|
6424
|
+
return node?.getAttributeNS?.(NS_W, localName2) || node?.getAttribute?.(`w:${localName2}`) || node?.getAttribute?.(localName2) || "";
|
|
5650
6425
|
}
|
|
5651
6426
|
function createRangeCursorLookup2(ranges) {
|
|
5652
6427
|
let cursor = 0;
|
|
@@ -6021,7 +6796,7 @@ function processHyperlinkForReconstruction(hyperlinkElement, originalFullText, p
|
|
|
6021
6796
|
}
|
|
6022
6797
|
|
|
6023
6798
|
// engine/reconstruction-writer.js
|
|
6024
|
-
function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, formatHints, generateRedlines = true) {
|
|
6799
|
+
function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, formatHints, generateRedlines = true, options = {}) {
|
|
6025
6800
|
const {
|
|
6026
6801
|
paragraphs,
|
|
6027
6802
|
containerFragments,
|
|
@@ -6035,7 +6810,14 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
|
|
|
6035
6810
|
} = context;
|
|
6036
6811
|
const createNewParagraph = (pPr) => {
|
|
6037
6812
|
const newParagraph = createWordElement(xmlDoc, "w:p");
|
|
6038
|
-
if (pPr)
|
|
6813
|
+
if (pPr) {
|
|
6814
|
+
const clonedPPr = pPr.cloneNode(true);
|
|
6815
|
+
const sectPr = getFirstElementByTagNSOrTag(clonedPPr, NS_W, "sectPr");
|
|
6816
|
+
if (sectPr) {
|
|
6817
|
+
clonedPPr.removeChild(sectPr);
|
|
6818
|
+
}
|
|
6819
|
+
newParagraph.appendChild(clonedPPr);
|
|
6820
|
+
}
|
|
6039
6821
|
return newParagraph;
|
|
6040
6822
|
};
|
|
6041
6823
|
const startInfo = getParagraphInfo(0);
|
|
@@ -6046,15 +6828,30 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
|
|
|
6046
6828
|
}
|
|
6047
6829
|
let currentOriginalIndex = 0;
|
|
6048
6830
|
let currentInsertOffset = 0;
|
|
6831
|
+
const pairReplacements = options?.pairReplacements === true;
|
|
6049
6832
|
let pendingReplacementStart = null;
|
|
6833
|
+
let pendingReplacementEvent = null;
|
|
6050
6834
|
const emittedCommentMarkers = /* @__PURE__ */ new WeakSet();
|
|
6051
|
-
for (
|
|
6835
|
+
for (let diffIndex = 0; diffIndex < diffs.length; diffIndex++) {
|
|
6836
|
+
const [op, text] = diffs[diffIndex];
|
|
6052
6837
|
if (op === 0 || op === -1) {
|
|
6053
6838
|
const type = op === 0 ? "equal" : "delete";
|
|
6054
6839
|
if (op === 0) {
|
|
6055
6840
|
pendingReplacementStart = null;
|
|
6841
|
+
pendingReplacementEvent = null;
|
|
6056
6842
|
} else if (pendingReplacementStart === null) {
|
|
6057
6843
|
pendingReplacementStart = currentOriginalIndex;
|
|
6844
|
+
let hasInsert = false;
|
|
6845
|
+
for (let k = diffIndex + 1; k < diffs.length; k++) {
|
|
6846
|
+
if (diffs[k][0] === 1) {
|
|
6847
|
+
hasInsert = true;
|
|
6848
|
+
break;
|
|
6849
|
+
}
|
|
6850
|
+
if (diffs[k][0] === 0) break;
|
|
6851
|
+
}
|
|
6852
|
+
if (pairReplacements && generateRedlines && hasInsert) {
|
|
6853
|
+
pendingReplacementEvent = createReplacementRevisionEvent(author, xmlDoc);
|
|
6854
|
+
}
|
|
6058
6855
|
}
|
|
6059
6856
|
let offset = 0;
|
|
6060
6857
|
while (offset < text.length) {
|
|
@@ -6080,7 +6877,8 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
|
|
|
6080
6877
|
formatHints,
|
|
6081
6878
|
currentInsertOffset,
|
|
6082
6879
|
generateRedlines,
|
|
6083
|
-
emittedCommentMarkers
|
|
6880
|
+
emittedCommentMarkers,
|
|
6881
|
+
pendingReplacementEvent
|
|
6084
6882
|
);
|
|
6085
6883
|
currentParagraph = appendResult.currentParagraph;
|
|
6086
6884
|
if (op === 0) {
|
|
@@ -6112,13 +6910,47 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
|
|
|
6112
6910
|
formatHints,
|
|
6113
6911
|
currentInsertOffset,
|
|
6114
6912
|
generateRedlines,
|
|
6115
|
-
emittedCommentMarkers
|
|
6913
|
+
emittedCommentMarkers,
|
|
6914
|
+
pendingReplacementEvent,
|
|
6915
|
+
pendingReplacementStart !== null
|
|
6116
6916
|
);
|
|
6117
6917
|
currentParagraph = appendResult.currentParagraph;
|
|
6118
6918
|
currentInsertOffset += text.length;
|
|
6119
6919
|
pendingReplacementStart = null;
|
|
6920
|
+
pendingReplacementEvent = null;
|
|
6120
6921
|
}
|
|
6121
6922
|
}
|
|
6923
|
+
if (generateRedlines) {
|
|
6924
|
+
containerFragments.forEach((fragment) => {
|
|
6925
|
+
Array.from(fragment.childNodes).forEach((node) => {
|
|
6926
|
+
if (isWordElement(node, "p")) {
|
|
6927
|
+
const hasVisibleText = Array.from(node.getElementsByTagNameNS(NS_W, "t")).length > 0;
|
|
6928
|
+
const hasDeletedText = Array.from(node.getElementsByTagNameNS(NS_W, "delText")).length > 0;
|
|
6929
|
+
if (paragraphs.length === 1 && !hasVisibleText && (hasDeletedText || context.originalFullText.trim() !== "")) {
|
|
6930
|
+
markParagraphMarkDeleted(xmlDoc, node, author);
|
|
6931
|
+
}
|
|
6932
|
+
}
|
|
6933
|
+
});
|
|
6934
|
+
});
|
|
6935
|
+
}
|
|
6936
|
+
containerFragments.forEach((fragment) => {
|
|
6937
|
+
const createdParagraphs = Array.from(fragment.childNodes).filter((node) => isWordElement(node, "p"));
|
|
6938
|
+
if (createdParagraphs.length > 1) {
|
|
6939
|
+
const firstP = createdParagraphs[0];
|
|
6940
|
+
const lastP = createdParagraphs[createdParagraphs.length - 1];
|
|
6941
|
+
const firstPPr = getFirstElementByTagNSOrTag(firstP, NS_W, "pPr");
|
|
6942
|
+
const sectPr = firstPPr ? getFirstElementByTagNSOrTag(firstPPr, NS_W, "sectPr") : null;
|
|
6943
|
+
if (sectPr) {
|
|
6944
|
+
firstPPr.removeChild(sectPr);
|
|
6945
|
+
let lastPPr = getFirstElementByTagNSOrTag(lastP, NS_W, "pPr");
|
|
6946
|
+
if (!lastPPr) {
|
|
6947
|
+
lastPPr = createWordElement(xmlDoc, "w:pPr");
|
|
6948
|
+
lastP.insertBefore(lastPPr, lastP.firstChild || null);
|
|
6949
|
+
}
|
|
6950
|
+
lastPPr.appendChild(sectPr);
|
|
6951
|
+
}
|
|
6952
|
+
}
|
|
6953
|
+
});
|
|
6122
6954
|
const paragraphSet = new Set(paragraphs);
|
|
6123
6955
|
const insertionAnchors = /* @__PURE__ */ new Map();
|
|
6124
6956
|
paragraphs.forEach((paragraph) => {
|
|
@@ -6157,16 +6989,23 @@ function applyReconstructionDiffs(xmlDoc, diffs, context, serializer, author, fo
|
|
|
6157
6989
|
const oxml = hasDocumentTarget && serializedDocumentOutput ? serializedDocumentOutput : serializer.serializeToString(xmlDoc);
|
|
6158
6990
|
return { oxml, hasChanges: true };
|
|
6159
6991
|
}
|
|
6160
|
-
function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, currentParagraphRef, containerFragments, sentinelMapByStart, referenceMap, replacementContainers, getParagraphInfo, createNewParagraph, author, formatHints = [], insertOffset = 0, generateRedlines = true, emittedCommentMarkers = /* @__PURE__ */ new WeakSet()) {
|
|
6992
|
+
function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, currentParagraphRef, containerFragments, sentinelMapByStart, referenceMap, replacementContainers, getParagraphInfo, createNewParagraph, author, formatHints = [], insertOffset = 0, generateRedlines = true, emittedCommentMarkers = /* @__PURE__ */ new WeakSet(), replacementEvent = null, deferClosingCommentMarkers = false) {
|
|
6161
6993
|
let localBaseIndex = baseIndex;
|
|
6162
6994
|
let localInsertOffset = insertOffset;
|
|
6163
6995
|
let localParagraph = currentParagraphRef;
|
|
6996
|
+
const deferredCommentMarkers = [];
|
|
6164
6997
|
const parts = text.split(/([\n\uFFFC]|[\uE000-\uF8FF])/);
|
|
6165
6998
|
parts.forEach((part) => {
|
|
6166
6999
|
const sentinelsAtOffset = sentinelMapByStart.get(localBaseIndex) || [];
|
|
6167
7000
|
const commentMarkers = sentinelsAtOffset.filter((sentinel) => sentinel.isCommentMarker && !emittedCommentMarkers.has(sentinel.node));
|
|
7001
|
+
const closingCommentIds = deferClosingCommentMarkers ? new Set(commentMarkers.filter((marker) => isWordElement(marker.node, "commentRangeEnd")).map((marker) => marker.node.getAttributeNS?.(NS_W, "id") || marker.node.getAttribute?.("w:id") || marker.node.getAttribute?.("id"))) : /* @__PURE__ */ new Set();
|
|
6168
7002
|
commentMarkers.forEach((marker) => {
|
|
6169
7003
|
emittedCommentMarkers.add(marker.node);
|
|
7004
|
+
const markerId = marker.node.getAttributeNS?.(NS_W, "id") || marker.node.getAttribute?.("w:id") || marker.node.getAttribute?.("id");
|
|
7005
|
+
if (deferClosingCommentMarkers && closingCommentIds.has(markerId) && (isWordElement(marker.node, "commentRangeEnd") || isWordElement(marker.node, "commentReference"))) {
|
|
7006
|
+
deferredCommentMarkers.push(marker.node);
|
|
7007
|
+
return;
|
|
7008
|
+
}
|
|
6170
7009
|
if (isWordElement(marker.node, "commentReference")) {
|
|
6171
7010
|
const run = createWordElement(xmlDoc, "w:r");
|
|
6172
7011
|
run.appendChild(marker.node.cloneNode(true));
|
|
@@ -6246,14 +7085,34 @@ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, curren
|
|
|
6246
7085
|
delText.textContent = part;
|
|
6247
7086
|
run.appendChild(delText);
|
|
6248
7087
|
if (generateRedlines) {
|
|
6249
|
-
|
|
7088
|
+
let metadata = null;
|
|
7089
|
+
if (replacementEvent) {
|
|
7090
|
+
const id = replacementEvent.usedDeletionId ? createRevisionMetadata(author, xmlDoc, "del").id : replacementEvent.deletionId;
|
|
7091
|
+
replacementEvent.usedDeletionId = true;
|
|
7092
|
+
metadata = {
|
|
7093
|
+
id,
|
|
7094
|
+
author: replacementEvent.author,
|
|
7095
|
+
date: replacementEvent.date
|
|
7096
|
+
};
|
|
7097
|
+
}
|
|
7098
|
+
const del = createTrackChange(xmlDoc, "del", run, author, metadata);
|
|
6250
7099
|
parent.appendChild(del);
|
|
6251
7100
|
}
|
|
6252
7101
|
} else {
|
|
6253
7102
|
const applicableHints = getApplicableFormatHints(formatHints, localInsertOffset, localInsertOffset + part.length);
|
|
6254
7103
|
const runs = createFormattedRuns(xmlDoc, part, rPr, applicableHints, localInsertOffset, author, generateRedlines);
|
|
6255
7104
|
if (type === "insert" && generateRedlines) {
|
|
6256
|
-
|
|
7105
|
+
let metadata = null;
|
|
7106
|
+
if (replacementEvent) {
|
|
7107
|
+
const id = replacementEvent.usedInsertionId ? createRevisionMetadata(author, xmlDoc, "ins").id : replacementEvent.insertionId;
|
|
7108
|
+
replacementEvent.usedInsertionId = true;
|
|
7109
|
+
metadata = {
|
|
7110
|
+
id,
|
|
7111
|
+
author: replacementEvent.author,
|
|
7112
|
+
date: replacementEvent.date
|
|
7113
|
+
};
|
|
7114
|
+
}
|
|
7115
|
+
const ins = createTrackChange(xmlDoc, "ins", null, author, metadata);
|
|
6257
7116
|
runs.forEach((run) => ins.appendChild(run));
|
|
6258
7117
|
parent.appendChild(ins);
|
|
6259
7118
|
} else {
|
|
@@ -6265,11 +7124,20 @@ function appendTextToCurrent(xmlDoc, text, type, rPr, wrapper, baseIndex, curren
|
|
|
6265
7124
|
}
|
|
6266
7125
|
localBaseIndex += part.length;
|
|
6267
7126
|
});
|
|
7127
|
+
deferredCommentMarkers.forEach((marker) => {
|
|
7128
|
+
if (isWordElement(marker, "commentReference")) {
|
|
7129
|
+
const run = createWordElement(xmlDoc, "w:r");
|
|
7130
|
+
run.appendChild(marker.cloneNode(true));
|
|
7131
|
+
localParagraph.appendChild(run);
|
|
7132
|
+
} else {
|
|
7133
|
+
localParagraph.appendChild(marker.cloneNode(true));
|
|
7134
|
+
}
|
|
7135
|
+
});
|
|
6268
7136
|
return { currentParagraph: localParagraph };
|
|
6269
7137
|
}
|
|
6270
7138
|
|
|
6271
7139
|
// engine/reconstruction-mode.js
|
|
6272
|
-
function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true, diffOptions = {}) {
|
|
7140
|
+
function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true, diffOptions = {}, options = {}) {
|
|
6273
7141
|
const selectedParagraphs = findReconstructionParagraphRange(xmlDoc, originalText);
|
|
6274
7142
|
if (selectedParagraphs === null) {
|
|
6275
7143
|
return withOoxmlSourceType({
|
|
@@ -6294,7 +7162,8 @@ function applyReconstructionMode(xmlDoc, originalText, modifiedText, serializer,
|
|
|
6294
7162
|
serializer,
|
|
6295
7163
|
author,
|
|
6296
7164
|
formatHints,
|
|
6297
|
-
generateRedlines
|
|
7165
|
+
generateRedlines,
|
|
7166
|
+
options
|
|
6298
7167
|
));
|
|
6299
7168
|
}
|
|
6300
7169
|
|
|
@@ -6399,7 +7268,7 @@ function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser
|
|
|
6399
7268
|
}
|
|
6400
7269
|
});
|
|
6401
7270
|
const del = createWordElement(workingDoc, "w:del");
|
|
6402
|
-
const metadata = createRevisionMetadata(author, workingDoc);
|
|
7271
|
+
const metadata = createRevisionMetadata(author, workingDoc, "del");
|
|
6403
7272
|
del.setAttribute("w:id", String(metadata.id));
|
|
6404
7273
|
del.setAttribute("w:author", metadata.author);
|
|
6405
7274
|
del.setAttribute("w:date", metadata.date);
|
|
@@ -6419,15 +7288,15 @@ function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser
|
|
|
6419
7288
|
}
|
|
6420
7289
|
|
|
6421
7290
|
// services/revision-comment-management.js
|
|
6422
|
-
function getAttributeByLocalName(node,
|
|
7291
|
+
function getAttributeByLocalName(node, localName2) {
|
|
6423
7292
|
if (!node || !node.attributes) return "";
|
|
6424
|
-
for (const
|
|
6425
|
-
if ((
|
|
6426
|
-
return String(
|
|
7293
|
+
for (const attr4 of Array.from(node.attributes)) {
|
|
7294
|
+
if ((attr4.localName || "").toLowerCase() === localName2.toLowerCase()) {
|
|
7295
|
+
return String(attr4.value || "");
|
|
6427
7296
|
}
|
|
6428
7297
|
}
|
|
6429
7298
|
return String(
|
|
6430
|
-
node.getAttribute?.(`w:${
|
|
7299
|
+
node.getAttribute?.(`w:${localName2}`) || node.getAttribute?.(localName2) || ""
|
|
6431
7300
|
);
|
|
6432
7301
|
}
|
|
6433
7302
|
function normalizeAuthor(author) {
|
|
@@ -6436,11 +7305,11 @@ function normalizeAuthor(author) {
|
|
|
6436
7305
|
function isElement(node) {
|
|
6437
7306
|
return !!node && node.nodeType === 1;
|
|
6438
7307
|
}
|
|
6439
|
-
function isWordElement5(node,
|
|
6440
|
-
return isElement(node) && node.namespaceURI === NS_W && String(node.localName || "").toLowerCase() ===
|
|
7308
|
+
function isWordElement5(node, localName2) {
|
|
7309
|
+
return isElement(node) && node.namespaceURI === NS_W && String(node.localName || "").toLowerCase() === localName2.toLowerCase();
|
|
6441
7310
|
}
|
|
6442
|
-
function getWordElementsByLocalName(xmlDoc,
|
|
6443
|
-
return Array.from(xmlDoc.getElementsByTagNameNS(NS_W,
|
|
7311
|
+
function getWordElementsByLocalName(xmlDoc, localName2) {
|
|
7312
|
+
return Array.from(xmlDoc.getElementsByTagNameNS(NS_W, localName2));
|
|
6444
7313
|
}
|
|
6445
7314
|
function resolveAuthorFilter(options = {}) {
|
|
6446
7315
|
if (options?.allAuthors === true) {
|
|
@@ -6463,19 +7332,37 @@ function authorMatchesNode(node, filter) {
|
|
|
6463
7332
|
return !!nodeAuthor && nodeAuthor === filter.normalizedAuthor;
|
|
6464
7333
|
}
|
|
6465
7334
|
function parseXmlWithWarnings(oxml, parseFailurePrefix) {
|
|
6466
|
-
|
|
7335
|
+
let rawOxml = typeof oxml === "string" ? oxml.replace(/^\uFEFF/, "").trim() : "";
|
|
7336
|
+
let isFragmentWrapped = false;
|
|
7337
|
+
let parsed = parseOoxmlSafe(rawOxml, "application/xml");
|
|
7338
|
+
if (parsed.error && (parsed.error.message.includes("HierarchyRequestError") || parsed.error.message.includes("Only one element"))) {
|
|
7339
|
+
const wrapped = `<w:body xmlns:w="${NS_W}">${rawOxml}</w:body>`;
|
|
7340
|
+
const wrappedParsed = parseOoxmlSafe(wrapped, "application/xml");
|
|
7341
|
+
if (!wrappedParsed.error) {
|
|
7342
|
+
parsed = wrappedParsed;
|
|
7343
|
+
isFragmentWrapped = true;
|
|
7344
|
+
}
|
|
7345
|
+
}
|
|
6467
7346
|
const parseError = parsed.doc ? getXmlParseError(parsed.doc) : null;
|
|
6468
7347
|
if (parsed.error || parseError) {
|
|
6469
7348
|
const message = parsed.error?.message || parseError?.textContent || "parse error";
|
|
6470
7349
|
return {
|
|
6471
7350
|
xmlDoc: null,
|
|
6472
7351
|
serializer: null,
|
|
7352
|
+
isFragmentWrapped: false,
|
|
6473
7353
|
warning: `${parseFailurePrefix}: ${message}`,
|
|
6474
7354
|
warnings: parsed.warnings,
|
|
6475
7355
|
error: { code: "PARSE_ERROR", message }
|
|
6476
7356
|
};
|
|
6477
7357
|
}
|
|
6478
|
-
return {
|
|
7358
|
+
return {
|
|
7359
|
+
xmlDoc: parsed.doc,
|
|
7360
|
+
serializer: createSerializer(),
|
|
7361
|
+
isFragmentWrapped,
|
|
7362
|
+
warning: null,
|
|
7363
|
+
warnings: parsed.warnings,
|
|
7364
|
+
error: null
|
|
7365
|
+
};
|
|
6479
7366
|
}
|
|
6480
7367
|
function removeNode(node) {
|
|
6481
7368
|
if (node?.parentNode) {
|
|
@@ -6518,10 +7405,16 @@ function mergeParagraphIntoNextAndRemove(paragraph) {
|
|
|
6518
7405
|
if (!paragraph?.parentNode) return false;
|
|
6519
7406
|
const nextParagraph = getNextWordParagraph(paragraph);
|
|
6520
7407
|
if (!nextParagraph) {
|
|
7408
|
+
if (isWordElement5(paragraph.parentNode, "tc") && getWordElementsByLocalName(paragraph.parentNode, "p").length <= 1) {
|
|
7409
|
+
return false;
|
|
7410
|
+
}
|
|
6521
7411
|
return removeNode(paragraph);
|
|
6522
7412
|
}
|
|
6523
7413
|
const childrenToMove = Array.from(paragraph.childNodes || []).filter((child) => !isWordElement5(child, "pPr"));
|
|
6524
|
-
|
|
7414
|
+
let insertionPoint = nextParagraph.firstChild || null;
|
|
7415
|
+
if (isWordElement5(insertionPoint, "pPr")) {
|
|
7416
|
+
insertionPoint = insertionPoint.nextSibling || null;
|
|
7417
|
+
}
|
|
6525
7418
|
for (const child of childrenToMove) {
|
|
6526
7419
|
nextParagraph.insertBefore(child, insertionPoint);
|
|
6527
7420
|
}
|
|
@@ -6583,14 +7476,15 @@ function acceptTrackedChangesInOoxml(oxml, options = {}) {
|
|
|
6583
7476
|
}
|
|
6584
7477
|
acceptedCount += removeMoveRangeMarkers(xmlDoc, filter);
|
|
6585
7478
|
const changeTags = ["rPrChange", "pPrChange", "tblPrChange", "trPrChange", "tcPrChange"];
|
|
6586
|
-
for (const
|
|
6587
|
-
for (const changeNode of getWordElementsByLocalName(xmlDoc,
|
|
7479
|
+
for (const localName2 of changeTags) {
|
|
7480
|
+
for (const changeNode of getWordElementsByLocalName(xmlDoc, localName2)) {
|
|
6588
7481
|
if (!changeNode.parentNode || !authorMatchesNode(changeNode, filter)) continue;
|
|
6589
7482
|
if (removeNode(changeNode)) acceptedCount += 1;
|
|
6590
7483
|
}
|
|
6591
7484
|
}
|
|
7485
|
+
const serializedOxml = parseResult.isFragmentWrapped ? Array.from(xmlDoc.documentElement.childNodes).map((n) => serializer.serializeToString(n)).join("") : serializer.serializeToString(xmlDoc);
|
|
6592
7486
|
return {
|
|
6593
|
-
oxml:
|
|
7487
|
+
oxml: serializedOxml,
|
|
6594
7488
|
hasChanges: acceptedCount > 0,
|
|
6595
7489
|
acceptedCount,
|
|
6596
7490
|
warnings
|
|
@@ -6609,10 +7503,10 @@ function convertDeletionTextNodes(xmlDoc, delNode) {
|
|
|
6609
7503
|
delTextNode.parentNode?.replaceChild(normalText, delTextNode);
|
|
6610
7504
|
}
|
|
6611
7505
|
}
|
|
6612
|
-
function rejectPropertyChangeNode(changeNode,
|
|
7506
|
+
function rejectPropertyChangeNode(changeNode, localName2) {
|
|
6613
7507
|
const parent = changeNode?.parentNode;
|
|
6614
7508
|
if (!parent) return false;
|
|
6615
|
-
const baseLocalName =
|
|
7509
|
+
const baseLocalName = localName2.endsWith("Change") ? localName2.slice(0, -"Change".length) : "";
|
|
6616
7510
|
if (!baseLocalName || String(parent.localName || "").toLowerCase() !== baseLocalName.toLowerCase() || parent.namespaceURI !== NS_W) {
|
|
6617
7511
|
return removeNode(changeNode);
|
|
6618
7512
|
}
|
|
@@ -6638,9 +7532,9 @@ function xmlDocImportNode(xmlDoc, node) {
|
|
|
6638
7532
|
}
|
|
6639
7533
|
return node.cloneNode(true);
|
|
6640
7534
|
}
|
|
6641
|
-
function collectMoveRangeStartIds(xmlDoc,
|
|
7535
|
+
function collectMoveRangeStartIds(xmlDoc, localName2, filter) {
|
|
6642
7536
|
const ids = /* @__PURE__ */ new Set();
|
|
6643
|
-
for (const node of getWordElementsByLocalName(xmlDoc,
|
|
7537
|
+
for (const node of getWordElementsByLocalName(xmlDoc, localName2)) {
|
|
6644
7538
|
if (!authorMatchesNode(node, filter)) continue;
|
|
6645
7539
|
const id = getAttributeByLocalName(node, "id");
|
|
6646
7540
|
if (id) ids.add(id);
|
|
@@ -6657,8 +7551,8 @@ function removeMoveRangeMarkers(xmlDoc, filter) {
|
|
|
6657
7551
|
["moveToRangeStart", moveToIds, true],
|
|
6658
7552
|
["moveToRangeEnd", moveToIds, false]
|
|
6659
7553
|
];
|
|
6660
|
-
for (const [
|
|
6661
|
-
for (const node of getWordElementsByLocalName(xmlDoc,
|
|
7554
|
+
for (const [localName2, ids, isStart] of markerSpecs) {
|
|
7555
|
+
for (const node of getWordElementsByLocalName(xmlDoc, localName2)) {
|
|
6662
7556
|
if (!node.parentNode) continue;
|
|
6663
7557
|
const id = getAttributeByLocalName(node, "id");
|
|
6664
7558
|
if (!id) continue;
|
|
@@ -6727,14 +7621,15 @@ function rejectTrackedChangesInOoxml(oxml, options = {}) {
|
|
|
6727
7621
|
}
|
|
6728
7622
|
rejectedCount += removeMoveRangeMarkers(xmlDoc, filter);
|
|
6729
7623
|
const changeTags = ["rPrChange", "pPrChange", "tblPrChange", "trPrChange", "tcPrChange"];
|
|
6730
|
-
for (const
|
|
6731
|
-
for (const changeNode of getWordElementsByLocalName(xmlDoc,
|
|
7624
|
+
for (const localName2 of changeTags) {
|
|
7625
|
+
for (const changeNode of getWordElementsByLocalName(xmlDoc, localName2)) {
|
|
6732
7626
|
if (!changeNode.parentNode || !authorMatchesNode(changeNode, filter)) continue;
|
|
6733
|
-
if (rejectPropertyChangeNode(changeNode,
|
|
7627
|
+
if (rejectPropertyChangeNode(changeNode, localName2)) rejectedCount += 1;
|
|
6734
7628
|
}
|
|
6735
7629
|
}
|
|
7630
|
+
const serializedOxml = parseResult.isFragmentWrapped ? Array.from(xmlDoc.documentElement.childNodes).map((n) => serializer.serializeToString(n)).join("") : serializer.serializeToString(xmlDoc);
|
|
6736
7631
|
return {
|
|
6737
|
-
oxml:
|
|
7632
|
+
oxml: serializedOxml,
|
|
6738
7633
|
hasChanges: rejectedCount > 0,
|
|
6739
7634
|
rejectedCount,
|
|
6740
7635
|
warnings
|
|
@@ -6773,12 +7668,12 @@ function runIsOnlyCommentReference(runNode) {
|
|
|
6773
7668
|
function removeCommentAnchors(xmlDoc, targetIds) {
|
|
6774
7669
|
let removed = 0;
|
|
6775
7670
|
const anchorTags = ["commentRangeStart", "commentRangeEnd", "commentReference"];
|
|
6776
|
-
for (const
|
|
6777
|
-
for (const node of getWordElementsByLocalName(xmlDoc,
|
|
7671
|
+
for (const localName2 of anchorTags) {
|
|
7672
|
+
for (const node of getWordElementsByLocalName(xmlDoc, localName2)) {
|
|
6778
7673
|
if (!node.parentNode) continue;
|
|
6779
7674
|
const id = getAttributeByLocalName(node, "id");
|
|
6780
7675
|
if (!id || !targetIds.has(id)) continue;
|
|
6781
|
-
if (
|
|
7676
|
+
if (localName2 === "commentReference" && runIsOnlyCommentReference(node.parentNode)) {
|
|
6782
7677
|
if (removeNode(node.parentNode)) {
|
|
6783
7678
|
removed += 1;
|
|
6784
7679
|
}
|
|
@@ -6819,8 +7714,8 @@ function deleteCommentsByAuthorInOoxml(oxml, options = {}) {
|
|
|
6819
7714
|
warnings.push(...parseResult.warnings || []);
|
|
6820
7715
|
const { targetIds, commentNodes } = collectCommentTargetIds(xmlDoc, filter);
|
|
6821
7716
|
if (filter.allAuthors) {
|
|
6822
|
-
for (const
|
|
6823
|
-
for (const node of getWordElementsByLocalName(xmlDoc,
|
|
7717
|
+
for (const localName2 of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
|
|
7718
|
+
for (const node of getWordElementsByLocalName(xmlDoc, localName2)) {
|
|
6824
7719
|
const id = getAttributeByLocalName(node, "id");
|
|
6825
7720
|
if (id) targetIds.add(id);
|
|
6826
7721
|
}
|
|
@@ -6837,7 +7732,36 @@ function deleteCommentsByAuthorInOoxml(oxml, options = {}) {
|
|
|
6837
7732
|
};
|
|
6838
7733
|
}
|
|
6839
7734
|
|
|
7735
|
+
// engine/route-selection.js
|
|
7736
|
+
var RECONCILIATION_CAPABILITY_MATRIX = Object.freeze({
|
|
7737
|
+
formatOnly: Object.freeze({ paragraphs: true, formatting: true, tables: "scoped", hyperlinks: "preserved", fields: "preserved", comments: "preserved" }),
|
|
7738
|
+
surgical: Object.freeze({ paragraphs: true, tables: "cell-scoped", hyperlinks: "preserved", fields: "preserved", comments: "preserved", notes: "preserved" }),
|
|
7739
|
+
reconstruction: Object.freeze({ paragraphs: true, hyperlinks: "sentinel-preserved", fields: "sentinel-preserved", comments: "marker-preserved", notes: "reference-preserved" }),
|
|
7740
|
+
table: Object.freeze({ tables: true, paragraphs: true, formatting: "cell-dependent", numbering: false }),
|
|
7741
|
+
listDirect: Object.freeze({ lists: true, numbering: true, paragraphs: "single-source expansion", tables: "embedded-markdown blocks", formatting: "markdown hints" }),
|
|
7742
|
+
listCompatibilityPipeline: Object.freeze({ lists: true, numbering: true, paragraphs: "multi-source patching", compatibility: true })
|
|
7743
|
+
});
|
|
7744
|
+
function recordRouteSelection(options, route, context = {}) {
|
|
7745
|
+
const callback = options?._routeInstrumentation?.onRoute;
|
|
7746
|
+
if (typeof callback !== "function") return;
|
|
7747
|
+
callback(Object.freeze({
|
|
7748
|
+
route,
|
|
7749
|
+
capabilities: RECONCILIATION_CAPABILITY_MATRIX[route] || null,
|
|
7750
|
+
...context
|
|
7751
|
+
}));
|
|
7752
|
+
}
|
|
7753
|
+
|
|
6840
7754
|
// engine/oxml-engine.js
|
|
7755
|
+
function getCommentIdsInOoxml(node) {
|
|
7756
|
+
const ids = /* @__PURE__ */ new Set();
|
|
7757
|
+
for (const localName2 of ["commentRangeStart", "commentRangeEnd", "commentReference"]) {
|
|
7758
|
+
for (const marker of getElementsByTagNSOrTag(node, NS_W, localName2)) {
|
|
7759
|
+
const id = marker.getAttribute?.("w:id") || marker.getAttribute?.("id");
|
|
7760
|
+
if (id !== "") ids.add(id);
|
|
7761
|
+
}
|
|
7762
|
+
}
|
|
7763
|
+
return [...ids].sort((a, b) => Number(a) - Number(b) || a.localeCompare(b));
|
|
7764
|
+
}
|
|
6841
7765
|
async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}) {
|
|
6842
7766
|
const inputOoxml = oxml;
|
|
6843
7767
|
let workingOoxml = oxml;
|
|
@@ -6849,11 +7773,19 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
6849
7773
|
let parseWarnings = [];
|
|
6850
7774
|
const operationWarnings = [];
|
|
6851
7775
|
let normalizedExistingRevisions = false;
|
|
6852
|
-
const
|
|
7776
|
+
const existingRevisionsPolicy = options.existingRevisions || "merge-same-author";
|
|
7777
|
+
const keepNormalizedNoOp = existingRevisionsPolicy === "accept-all-first-keep-normalized";
|
|
6853
7778
|
const finalize = (result) => {
|
|
6854
7779
|
const withStatus = { ...result };
|
|
6855
7780
|
if (normalizedExistingRevisions && withStatus.hasChanges === false && withStatus.status !== "error") {
|
|
6856
|
-
if (
|
|
7781
|
+
if (existingRevisionsPolicy === "merge-same-author") {
|
|
7782
|
+
withStatus.oxml = workingOoxml;
|
|
7783
|
+
withStatus.hasChanges = true;
|
|
7784
|
+
withStatus.warnings = [
|
|
7785
|
+
...Array.isArray(withStatus.warnings) ? withStatus.warnings : [],
|
|
7786
|
+
"Previous revisions by the same author were reverted to baseline."
|
|
7787
|
+
];
|
|
7788
|
+
} else if (keepNormalizedNoOp) {
|
|
6857
7789
|
withStatus.oxml = workingOoxml;
|
|
6858
7790
|
withStatus.hasChanges = true;
|
|
6859
7791
|
withStatus.warnings = [
|
|
@@ -6900,8 +7832,74 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
6900
7832
|
const revisionIdAllocator = options?._revisionIdAllocator instanceof RevisionIdAllocator ? options._revisionIdAllocator : new RevisionIdAllocator();
|
|
6901
7833
|
seedRevisionIdsFromDocument(xmlDoc, revisionIdAllocator);
|
|
6902
7834
|
if (containsTrackedChanges(xmlDoc)) {
|
|
6903
|
-
|
|
6904
|
-
|
|
7835
|
+
if (existingRevisionsPolicy === "merge-same-author") {
|
|
7836
|
+
const authors = getTrackedChangeAuthors(xmlDoc);
|
|
7837
|
+
const currentAuthor = String(author || "").trim().toLowerCase();
|
|
7838
|
+
const isSameAuthor = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === currentAuthor);
|
|
7839
|
+
if (isSameAuthor) {
|
|
7840
|
+
const commentIds = getCommentIdsInOoxml(xmlDoc);
|
|
7841
|
+
if (commentIds.length > 0) {
|
|
7842
|
+
return finalize({
|
|
7843
|
+
oxml: inputOoxml,
|
|
7844
|
+
hasChanges: false,
|
|
7845
|
+
status: "error",
|
|
7846
|
+
error: {
|
|
7847
|
+
code: "COMMENTED_CONTENT_MERGE",
|
|
7848
|
+
message: "Refusing to merge existing revisions in commented content because reverting the prior revisions could remove or orphan comment anchors.",
|
|
7849
|
+
commentIds
|
|
7850
|
+
}
|
|
7851
|
+
});
|
|
7852
|
+
}
|
|
7853
|
+
log("[OxmlEngine] Existing revisions from same author detected; rejecting previous changes to merge against baseline");
|
|
7854
|
+
const rejected = rejectTrackedChangesInOoxml(inputOoxml, { author });
|
|
7855
|
+
if (rejected.status === "error") return finalize(rejected);
|
|
7856
|
+
workingOoxml = rejected.oxml;
|
|
7857
|
+
normalizedExistingRevisions = true;
|
|
7858
|
+
const rejectedParsed = parseOoxmlSafe(workingOoxml, "text/xml");
|
|
7859
|
+
parseWarnings.push(...rejectedParsed.warnings);
|
|
7860
|
+
xmlDoc = rejectedParsed.doc;
|
|
7861
|
+
const rejectedParseError = xmlDoc ? getXmlParseError(xmlDoc) : null;
|
|
7862
|
+
if (rejectedParsed.error || rejectedParseError) {
|
|
7863
|
+
const message = rejectedParsed.error?.message || rejectedParseError?.textContent || "Could not parse OOXML after rejecting same-author revisions.";
|
|
7864
|
+
error("[OxmlEngine] XML parse error after rejecting same-author revisions:", message);
|
|
7865
|
+
return finalize({
|
|
7866
|
+
oxml: inputOoxml,
|
|
7867
|
+
hasChanges: false,
|
|
7868
|
+
status: "error",
|
|
7869
|
+
error: {
|
|
7870
|
+
code: "PARSE_ERROR",
|
|
7871
|
+
message
|
|
7872
|
+
}
|
|
7873
|
+
});
|
|
7874
|
+
}
|
|
7875
|
+
if (containsTrackedChanges(xmlDoc)) {
|
|
7876
|
+
return finalize({
|
|
7877
|
+
oxml: inputOoxml,
|
|
7878
|
+
hasChanges: false,
|
|
7879
|
+
status: "error",
|
|
7880
|
+
error: {
|
|
7881
|
+
code: "UNSAFE_REVISION_NESTING",
|
|
7882
|
+
message: "Existing same-author revisions could not be completely restored to baseline; refusing to layer new revisions over unsupported revision markup."
|
|
7883
|
+
}
|
|
7884
|
+
});
|
|
7885
|
+
}
|
|
7886
|
+
seedRevisionIdsFromDocument(xmlDoc, revisionIdAllocator);
|
|
7887
|
+
const paragraphsInDoc = xmlDoc.documentElement && String(xmlDoc.documentElement.localName || "").toLowerCase() === "p" ? [xmlDoc.documentElement] : getDocumentParagraphs(xmlDoc);
|
|
7888
|
+
const baselineText = paragraphsInDoc.length > 0 ? paragraphsInDoc.map((p) => extractCanonicalParagraphText(p)).join("\n") : "";
|
|
7889
|
+
originalText = baselineText;
|
|
7890
|
+
} else {
|
|
7891
|
+
log("[OxmlEngine] Existing revisions detected from another/unattributed author; refusing per merge-same-author policy");
|
|
7892
|
+
return finalize({
|
|
7893
|
+
oxml: inputOoxml,
|
|
7894
|
+
hasChanges: false,
|
|
7895
|
+
status: "error",
|
|
7896
|
+
error: {
|
|
7897
|
+
code: "EXISTING_REVISIONS",
|
|
7898
|
+
message: `Input OOXML contains tracked changes from another author (${authors.length ? authors.join(", ") : "unattributed"}). Pass existingRevisions: "accept-all-first" or resolve revisions first.`
|
|
7899
|
+
}
|
|
7900
|
+
});
|
|
7901
|
+
}
|
|
7902
|
+
} else if (existingRevisionsPolicy === "accept-all-first" || existingRevisionsPolicy === "accept-all-first-keep-normalized") {
|
|
6905
7903
|
log("[OxmlEngine] Existing revisions detected; accepting all input revisions before redlining");
|
|
6906
7904
|
const accepted = acceptTrackedChangesInOoxml(inputOoxml, { allAuthors: true });
|
|
6907
7905
|
if (accepted.status === "error") return finalize(accepted);
|
|
@@ -6933,7 +7931,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
6933
7931
|
status: "error",
|
|
6934
7932
|
error: {
|
|
6935
7933
|
code: "EXISTING_REVISIONS",
|
|
6936
|
-
message: 'Input OOXML contains existing tracked changes
|
|
7934
|
+
message: 'Input OOXML contains existing tracked changes and existingRevisions is "reject-input".'
|
|
6937
7935
|
}
|
|
6938
7936
|
});
|
|
6939
7937
|
}
|
|
@@ -6951,10 +7949,29 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
6951
7949
|
}
|
|
6952
7950
|
return isolatedResult;
|
|
6953
7951
|
}
|
|
6954
|
-
|
|
7952
|
+
let sanitizedText = options.sanitizeInput === true ? sanitizeAiResponse(modifiedText) : modifiedText;
|
|
6955
7953
|
if (sanitizedText !== modifiedText) {
|
|
6956
7954
|
operationWarnings.push("Input was sanitized; pass sanitizeInput: false to disable.");
|
|
6957
7955
|
}
|
|
7956
|
+
let structuredAnalysis = null;
|
|
7957
|
+
if (options.structuredContent !== false) {
|
|
7958
|
+
structuredAnalysis = analyzeStructuredContent(sanitizedText);
|
|
7959
|
+
if (!structuredAnalysis.valid) {
|
|
7960
|
+
if (options.explicitStructuredContent === true) {
|
|
7961
|
+
const message = structuredAnalysis.issues.map((issue) => `${issue.code}: ${issue.message}`).join(" ");
|
|
7962
|
+
return finalize({
|
|
7963
|
+
oxml: inputOoxml,
|
|
7964
|
+
hasChanges: false,
|
|
7965
|
+
status: "error",
|
|
7966
|
+
error: { code: "STRUCTURED_CONTENT_INVALID", message },
|
|
7967
|
+
warnings: structuredAnalysis.issues.map((issue) => issue.message)
|
|
7968
|
+
});
|
|
7969
|
+
}
|
|
7970
|
+
structuredAnalysis = null;
|
|
7971
|
+
} else if (structuredAnalysis.requiresStructuredContent) {
|
|
7972
|
+
sanitizedText = structuredAnalysis.normalizedMarkdown;
|
|
7973
|
+
}
|
|
7974
|
+
}
|
|
6958
7975
|
const { cleanText: cleanModifiedText, formatHints } = preprocessMarkdown(sanitizedText);
|
|
6959
7976
|
const hasTextChanges = cleanModifiedText.trim() !== originalText.trim();
|
|
6960
7977
|
const hasFormatHints = formatHints.length > 0;
|
|
@@ -7011,6 +8028,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
7011
8028
|
log(`[OxmlEngine] Text changes: ${hasTextChanges}, New format hints: ${formatHints.length}, Existing format hints: ${existingFormatHints.length}`);
|
|
7012
8029
|
const needsFormatRemoval = options.removeFormatting === true && !hasTextChanges && !hasFormatHints && hasExistingFormatting;
|
|
7013
8030
|
if (!hasTextChanges && !hasFormatHints && !hasExistingFormatting) {
|
|
8031
|
+
recordRouteSelection(options, "noChange");
|
|
7014
8032
|
log("[OxmlEngine] No text changes, no format hints, and no existing formatting detected");
|
|
7015
8033
|
return finalizeUnchanged();
|
|
7016
8034
|
}
|
|
@@ -7019,6 +8037,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
7019
8037
|
return finalizeUnchanged();
|
|
7020
8038
|
}
|
|
7021
8039
|
if (needsFormatRemoval) {
|
|
8040
|
+
recordRouteSelection(options, "formatOnly", { removeFormatting: true });
|
|
7022
8041
|
log("[OxmlEngine] Format REMOVAL detected: applying surgical replacement in OOXML");
|
|
7023
8042
|
const tableCellCtx = initialTableCellContext;
|
|
7024
8043
|
let targetParagraph = tableCellCtx.targetParagraph || null;
|
|
@@ -7052,6 +8071,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
7052
8071
|
return finalize(removalResult);
|
|
7053
8072
|
}
|
|
7054
8073
|
if (!hasTextChanges && hasFormatHints) {
|
|
8074
|
+
recordRouteSelection(options, "formatOnly", { removeFormatting: false });
|
|
7055
8075
|
log(`[OxmlEngine] Format-only change detected: ${formatHints.length} format hints`);
|
|
7056
8076
|
const tableCellCtx = initialTableCellContext;
|
|
7057
8077
|
const precomputedFormatContext = {
|
|
@@ -7074,17 +8094,21 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
7074
8094
|
const hasTables = tables.length > 0;
|
|
7075
8095
|
const isMarkdownTable = /^\|.+\|/.test(cleanModifiedText.trim()) && cleanModifiedText.includes("\n");
|
|
7076
8096
|
const isTargetList = isListTargetLoose(cleanModifiedText);
|
|
8097
|
+
const isStructuredContent = options.structuredContent !== false && structuredAnalysis?.requiresStructuredContent === true;
|
|
7077
8098
|
const tableCellContext = initialTableCellContext;
|
|
7078
8099
|
log(`[OxmlEngine] Mode: ${hasTables ? "SURGICAL" : "RECONSTRUCTION"}, formatHints: ${formatHints.length}, isMarkdownTable: ${isMarkdownTable}, isTargetList: ${isTargetList}, isTableCellParagraph: ${tableCellContext.isTableCellParagraph}`);
|
|
7079
8100
|
try {
|
|
7080
8101
|
if (isMarkdownTable && !hasTables) {
|
|
8102
|
+
recordRouteSelection(options, "table", { transformation: "text-to-table" });
|
|
7081
8103
|
log("[OxmlEngine] Text-to-table transformation: generating new table from Markdown");
|
|
7082
8104
|
return finalize(applyTextToTableTransformation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines));
|
|
7083
8105
|
}
|
|
7084
8106
|
if (hasTables && isMarkdownTable) {
|
|
8107
|
+
recordRouteSelection(options, "table", { transformation: "table-reconciliation" });
|
|
7085
8108
|
return finalize(applyTableReconciliation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines));
|
|
7086
8109
|
}
|
|
7087
8110
|
if (hasTables) {
|
|
8111
|
+
recordRouteSelection(options, "surgical", { tableScoped: true });
|
|
7088
8112
|
const surgicalTarget = tableCellContext.hasTableWrapper && tableCellContext.targetParagraph ? tableCellContext.targetParagraph : null;
|
|
7089
8113
|
if (surgicalTarget) {
|
|
7090
8114
|
log("[OxmlEngine] Table cell edit: scoping surgical mode to target paragraph");
|
|
@@ -7097,7 +8121,9 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
7097
8121
|
author,
|
|
7098
8122
|
formatHints,
|
|
7099
8123
|
generateRedlines,
|
|
7100
|
-
surgicalTarget
|
|
8124
|
+
surgicalTarget,
|
|
8125
|
+
{},
|
|
8126
|
+
options
|
|
7101
8127
|
);
|
|
7102
8128
|
if (tableCellContext.hasTableWrapper && result.hasChanges && tableCellContext.targetParagraph) {
|
|
7103
8129
|
log("[OxmlEngine] Stripping table wrapper for table cell paragraph (surgical mode)");
|
|
@@ -7105,14 +8131,36 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
7105
8131
|
}
|
|
7106
8132
|
return finalize(result);
|
|
7107
8133
|
}
|
|
7108
|
-
if (isTargetList) {
|
|
7109
|
-
|
|
7110
|
-
const
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
revisionIdAllocator
|
|
8134
|
+
if (isTargetList || isStructuredContent) {
|
|
8135
|
+
const sourceParagraphs = getElementsByTagNSOrTag(xmlDoc, NS_W, "p");
|
|
8136
|
+
const useDirectListGeneration = sourceParagraphs.length === 1;
|
|
8137
|
+
recordRouteSelection(options, useDirectListGeneration ? "listDirect" : "listCompatibilityPipeline", {
|
|
8138
|
+
sourceParagraphCount: sourceParagraphs.length
|
|
7114
8139
|
});
|
|
7115
|
-
|
|
8140
|
+
log(`[OxmlEngine] \u{1F3AF} Using ${useDirectListGeneration ? "direct list generation" : "compatibility pipeline"} for list reconciliation`);
|
|
8141
|
+
let result;
|
|
8142
|
+
if (useDirectListGeneration) {
|
|
8143
|
+
const ingested = ingestOoxml(workingOoxml, { xmlDoc });
|
|
8144
|
+
const numberingContext = detectNumberingContext(sourceParagraphs[0]);
|
|
8145
|
+
result = await executeListGeneration({
|
|
8146
|
+
cleanText: cleanModifiedText,
|
|
8147
|
+
numberingContext,
|
|
8148
|
+
originalRunModel: ingested.runModel,
|
|
8149
|
+
originalText: ingested.acceptedText,
|
|
8150
|
+
generateRedlines,
|
|
8151
|
+
author,
|
|
8152
|
+
font: options.font || null,
|
|
8153
|
+
revisionIdAllocator,
|
|
8154
|
+
numberingService: new NumberingService()
|
|
8155
|
+
});
|
|
8156
|
+
} else {
|
|
8157
|
+
const pipeline = new ReconciliationPipeline({
|
|
8158
|
+
author,
|
|
8159
|
+
generateRedlines,
|
|
8160
|
+
revisionIdAllocator
|
|
8161
|
+
});
|
|
8162
|
+
result = await pipeline.execute(workingOoxml, sanitizedText, { xmlDoc });
|
|
8163
|
+
}
|
|
7116
8164
|
if (result.error?.code === "DIFF_TOKEN_LIMIT") {
|
|
7117
8165
|
return finalize({ oxml: inputOoxml, hasChanges: false, status: "error", error: result.error });
|
|
7118
8166
|
}
|
|
@@ -7124,10 +8172,15 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
7124
8172
|
numberingXml: result.numberingXml
|
|
7125
8173
|
});
|
|
7126
8174
|
log(`[OxmlEngine] \u2705 Wrapped OOXML length: ${wrapped.length}`);
|
|
7127
|
-
return finalize({
|
|
8175
|
+
return finalize({
|
|
8176
|
+
oxml: wrapped,
|
|
8177
|
+
hasChanges: true,
|
|
8178
|
+
...Array.isArray(result.warnings) ? { warnings: result.warnings } : {}
|
|
8179
|
+
});
|
|
7128
8180
|
}
|
|
7129
8181
|
return finalizeUnchanged();
|
|
7130
8182
|
}
|
|
8183
|
+
recordRouteSelection(options, "reconstruction");
|
|
7131
8184
|
return finalize(applyReconstructionMode(
|
|
7132
8185
|
xmlDoc,
|
|
7133
8186
|
originalText,
|
|
@@ -7135,7 +8188,9 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
7135
8188
|
serializer,
|
|
7136
8189
|
author,
|
|
7137
8190
|
formatHints,
|
|
7138
|
-
generateRedlines
|
|
8191
|
+
generateRedlines,
|
|
8192
|
+
{},
|
|
8193
|
+
options
|
|
7139
8194
|
));
|
|
7140
8195
|
} catch (caught) {
|
|
7141
8196
|
if (isDiffTokenLimitError(caught)) {
|
|
@@ -7154,10 +8209,10 @@ function normalizeTargetText(text) {
|
|
|
7154
8209
|
}
|
|
7155
8210
|
function textSpanVisibleText(span) {
|
|
7156
8211
|
const node = span?.textElement;
|
|
7157
|
-
const
|
|
7158
|
-
if (
|
|
7159
|
-
if (
|
|
7160
|
-
if (
|
|
8212
|
+
const localName2 = String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
|
|
8213
|
+
if (localName2 === "tab") return " ";
|
|
8214
|
+
if (localName2 === "br" || localName2 === "cr") return "\n";
|
|
8215
|
+
if (localName2 === "noBreakHyphen") return "\u2011";
|
|
7161
8216
|
return node?.textContent || "";
|
|
7162
8217
|
}
|
|
7163
8218
|
function sanitizeAiResponse(text) {
|
|
@@ -7182,39 +8237,27 @@ function createTargetNotFoundError(message) {
|
|
|
7182
8237
|
error2.code = "TARGET_NOT_FOUND";
|
|
7183
8238
|
return error2;
|
|
7184
8239
|
}
|
|
8240
|
+
function createTargetError(code, message, candidates = null) {
|
|
8241
|
+
const error2 = new Error(message);
|
|
8242
|
+
error2.code = code;
|
|
8243
|
+
if (Array.isArray(candidates)) error2.candidates = candidates;
|
|
8244
|
+
return error2;
|
|
8245
|
+
}
|
|
7185
8246
|
var WORD_MAIN_NS = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
7186
|
-
function getElementsByLocalName(node,
|
|
8247
|
+
function getElementsByLocalName(node, localName2) {
|
|
7187
8248
|
if (!node) return [];
|
|
7188
8249
|
if (typeof node.getElementsByTagNameNS === "function") {
|
|
7189
|
-
const namespaced = toArray(node.getElementsByTagNameNS("*",
|
|
8250
|
+
const namespaced = toArray(node.getElementsByTagNameNS("*", localName2));
|
|
7190
8251
|
if (namespaced.length > 0) return namespaced;
|
|
7191
8252
|
}
|
|
7192
8253
|
if (typeof node.getElementsByTagName !== "function") return [];
|
|
7193
|
-
const prefixed = toArray(node.getElementsByTagName(`w:${
|
|
8254
|
+
const prefixed = toArray(node.getElementsByTagName(`w:${localName2}`));
|
|
7194
8255
|
if (prefixed.length > 0) return prefixed;
|
|
7195
|
-
return toArray(node.getElementsByTagName(
|
|
7196
|
-
}
|
|
7197
|
-
function toParagraphText(paragraph) {
|
|
7198
|
-
let text = "";
|
|
7199
|
-
const visit = (node) => {
|
|
7200
|
-
for (const child of toArray(node?.childNodes)) {
|
|
7201
|
-
if (child?.nodeType !== 1) continue;
|
|
7202
|
-
const localName = String(child.localName || child.nodeName || "").replace(/^.*:/, "");
|
|
7203
|
-
if (localName === "t") {
|
|
7204
|
-
text += child.textContent || "";
|
|
7205
|
-
} else if (localName === "tab") {
|
|
7206
|
-
text += " ";
|
|
7207
|
-
} else {
|
|
7208
|
-
visit(child);
|
|
7209
|
-
}
|
|
7210
|
-
}
|
|
7211
|
-
};
|
|
7212
|
-
visit(paragraph);
|
|
7213
|
-
return text;
|
|
8256
|
+
return toArray(node.getElementsByTagName(localName2));
|
|
7214
8257
|
}
|
|
7215
8258
|
function getParagraphText(paragraph) {
|
|
7216
8259
|
if (!paragraph) return "";
|
|
7217
|
-
return
|
|
8260
|
+
return extractCanonicalParagraphText(paragraph);
|
|
7218
8261
|
}
|
|
7219
8262
|
function getDocumentParagraphNodes(xmlDoc) {
|
|
7220
8263
|
if (!xmlDoc) return [];
|
|
@@ -7222,6 +8265,75 @@ function getDocumentParagraphNodes(xmlDoc) {
|
|
|
7222
8265
|
const searchRoot = bodies.length > 0 ? bodies[0] : xmlDoc;
|
|
7223
8266
|
return getElementsByLocalName(searchRoot, "p");
|
|
7224
8267
|
}
|
|
8268
|
+
function getParagraphId2(paragraph) {
|
|
8269
|
+
if (!paragraph) return null;
|
|
8270
|
+
const attribute = toArray(paragraph.attributes).find(
|
|
8271
|
+
(candidate) => String(candidate?.localName || candidate?.name || "").replace(/^.*:/, "") === "paraId"
|
|
8272
|
+
);
|
|
8273
|
+
return attribute?.value || null;
|
|
8274
|
+
}
|
|
8275
|
+
function createParagraphFingerprint(paragraph, metadata = {}) {
|
|
8276
|
+
if (!paragraph) return null;
|
|
8277
|
+
const revisionView = metadata.revisionView === "rejected" ? "rejected" : "accepted";
|
|
8278
|
+
const text = typeof metadata.text === "string" ? metadata.text : extractCanonicalParagraphText(paragraph, { revisionView });
|
|
8279
|
+
const documentIndex = Number.isInteger(metadata.index) ? metadata.index : getDocumentParagraphNodes(paragraph.ownerDocument || paragraph).indexOf(paragraph) + 1;
|
|
8280
|
+
const paragraphId = metadata.paragraphId === void 0 ? getParagraphId2(paragraph) : metadata.paragraphId;
|
|
8281
|
+
const inTable = typeof metadata.inTable === "boolean" ? metadata.inTable : isParagraphInTable(paragraph);
|
|
8282
|
+
const viewPart = revisionView === "rejected" ? "rejected" : "";
|
|
8283
|
+
const identity = `${paragraphId || ""}${documentIndex}${inTable ? "table" : "body"}${viewPart}${text}`;
|
|
8284
|
+
let hash = 2166136261;
|
|
8285
|
+
for (let i = 0; i < identity.length; i++) {
|
|
8286
|
+
hash ^= identity.charCodeAt(i);
|
|
8287
|
+
hash = Math.imul(hash, 16777619) >>> 0;
|
|
8288
|
+
}
|
|
8289
|
+
return `fnv1a32:${hash.toString(16).padStart(8, "0")}`;
|
|
8290
|
+
}
|
|
8291
|
+
function buildParagraphMetadataIndex(xmlDoc, options = {}) {
|
|
8292
|
+
const revisionView = options.revisionView === "rejected" ? "rejected" : "accepted";
|
|
8293
|
+
const paragraphs = getDocumentParagraphNodes(xmlDoc);
|
|
8294
|
+
const entries = [];
|
|
8295
|
+
const byParagraph = /* @__PURE__ */ new Map();
|
|
8296
|
+
const byId = /* @__PURE__ */ new Map();
|
|
8297
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
8298
|
+
for (let offset = 0; offset < paragraphs.length; offset++) {
|
|
8299
|
+
const paragraph = paragraphs[offset];
|
|
8300
|
+
const text = extractCanonicalParagraphText(paragraph, { revisionView });
|
|
8301
|
+
const paragraphId = getParagraphId2(paragraph);
|
|
8302
|
+
const inTable = isParagraphInTable(paragraph);
|
|
8303
|
+
const normalizedText = normalizeWhitespaceForTargeting(text);
|
|
8304
|
+
const entry = Object.freeze({
|
|
8305
|
+
paragraph,
|
|
8306
|
+
index: offset + 1,
|
|
8307
|
+
paragraphId,
|
|
8308
|
+
text,
|
|
8309
|
+
normalizedText,
|
|
8310
|
+
revisionView,
|
|
8311
|
+
fingerprint: createParagraphFingerprint(paragraph, {
|
|
8312
|
+
text,
|
|
8313
|
+
index: offset + 1,
|
|
8314
|
+
paragraphId,
|
|
8315
|
+
inTable,
|
|
8316
|
+
revisionView
|
|
8317
|
+
}),
|
|
8318
|
+
inTable
|
|
8319
|
+
});
|
|
8320
|
+
entries.push(entry);
|
|
8321
|
+
byParagraph.set(paragraph, entry);
|
|
8322
|
+
if (paragraphId && !byId.has(paragraphId)) byId.set(paragraphId, entry);
|
|
8323
|
+
if (normalizedText) {
|
|
8324
|
+
if (!grouped.has(normalizedText)) grouped.set(normalizedText, []);
|
|
8325
|
+
grouped.get(normalizedText).push(entry);
|
|
8326
|
+
}
|
|
8327
|
+
}
|
|
8328
|
+
for (const [key, values] of grouped) grouped.set(key, Object.freeze(values));
|
|
8329
|
+
return Object.freeze({
|
|
8330
|
+
revisionView,
|
|
8331
|
+
entries: Object.freeze(entries),
|
|
8332
|
+
byParagraph,
|
|
8333
|
+
byId,
|
|
8334
|
+
byNormalizedText: grouped
|
|
8335
|
+
});
|
|
8336
|
+
}
|
|
7225
8337
|
function normalizeWhitespaceForTargeting(text) {
|
|
7226
8338
|
return String(text || "").replace(/\s+/g, " ").trim();
|
|
7227
8339
|
}
|
|
@@ -7253,40 +8365,49 @@ function splitLeadingParagraphMarker(text) {
|
|
|
7253
8365
|
targetRef: Number.parseInt(marker[1], 10)
|
|
7254
8366
|
};
|
|
7255
8367
|
}
|
|
7256
|
-
function findParagraphByReference(xmlDoc, targetRef) {
|
|
8368
|
+
function findParagraphByReference(xmlDoc, targetRef, paragraphMetadataIndex = null) {
|
|
7257
8369
|
if (!Number.isInteger(targetRef) || targetRef < 1) return null;
|
|
7258
|
-
|
|
7259
|
-
|
|
8370
|
+
if (paragraphMetadataIndex?.entries) {
|
|
8371
|
+
return paragraphMetadataIndex.entries[targetRef - 1]?.paragraph || null;
|
|
8372
|
+
}
|
|
8373
|
+
return getDocumentParagraphNodes(xmlDoc)[targetRef - 1] || null;
|
|
7260
8374
|
}
|
|
7261
|
-
function findContainingWordElement(node,
|
|
8375
|
+
function findContainingWordElement(node, localName2, namespaceUri = WORD_MAIN_NS) {
|
|
7262
8376
|
let current = node;
|
|
7263
8377
|
while (current) {
|
|
7264
|
-
if (current.nodeType === 1 && current.namespaceURI === namespaceUri && current.localName ===
|
|
8378
|
+
if (current.nodeType === 1 && current.namespaceURI === namespaceUri && current.localName === localName2) {
|
|
7265
8379
|
return current;
|
|
7266
8380
|
}
|
|
7267
8381
|
current = current.parentNode;
|
|
7268
8382
|
}
|
|
7269
8383
|
return null;
|
|
7270
8384
|
}
|
|
7271
|
-
function findParagraphByStrictText(xmlDoc, targetText) {
|
|
7272
|
-
const
|
|
8385
|
+
function findParagraphByStrictText(xmlDoc, targetText, options = {}) {
|
|
8386
|
+
const metadataIndex = options.paragraphMetadataIndex || null;
|
|
8387
|
+
const entries = metadataIndex?.entries || null;
|
|
8388
|
+
const paragraphs = entries ? null : getDocumentParagraphNodes(xmlDoc);
|
|
7273
8389
|
const normalizedTarget = String(targetText || "").trim();
|
|
7274
8390
|
if (!normalizedTarget) return null;
|
|
7275
|
-
const exact = paragraphs.find((p) => getParagraphText(p).trim() === normalizedTarget);
|
|
8391
|
+
const exact = entries ? entries.find((entry) => entry.text.trim() === normalizedTarget)?.paragraph : paragraphs.find((p) => getParagraphText(p).trim() === normalizedTarget);
|
|
7276
8392
|
if (exact) return exact;
|
|
7277
8393
|
const normTarget = normalizeWhitespaceForTargeting(normalizedTarget);
|
|
8394
|
+
if (metadataIndex?.byNormalizedText) {
|
|
8395
|
+
return metadataIndex.byNormalizedText.get(normTarget)?.[0]?.paragraph || null;
|
|
8396
|
+
}
|
|
7278
8397
|
return paragraphs.find((p) => normalizeWhitespaceForTargeting(getParagraphText(p)) === normTarget) || null;
|
|
7279
8398
|
}
|
|
7280
8399
|
function findParagraphByBestTextMatch(xmlDoc, targetText, options = {}) {
|
|
7281
8400
|
const onInfo = typeof options.onInfo === "function" ? options.onInfo : () => {
|
|
7282
8401
|
};
|
|
7283
|
-
const
|
|
8402
|
+
const metadataIndex = options.paragraphMetadataIndex || null;
|
|
8403
|
+
const entries = metadataIndex?.entries || null;
|
|
8404
|
+
const paragraphs = entries ? null : getDocumentParagraphNodes(xmlDoc);
|
|
7284
8405
|
const normalizedTarget = String(targetText || "").trim();
|
|
7285
8406
|
if (!normalizedTarget) return null;
|
|
7286
|
-
const strictMatch = findParagraphByStrictText(xmlDoc, normalizedTarget);
|
|
8407
|
+
const strictMatch = findParagraphByStrictText(xmlDoc, normalizedTarget, { paragraphMetadataIndex: metadataIndex });
|
|
7287
8408
|
if (strictMatch) return strictMatch;
|
|
7288
8409
|
const normTarget = normalizeWhitespaceForTargeting(normalizedTarget);
|
|
7289
|
-
const startsWithMatch = paragraphs.find((p) => {
|
|
8410
|
+
const startsWithMatch = entries ? entries.find((entry) => entry.normalizedText.length > 10 && normTarget.startsWith(entry.normalizedText))?.paragraph || null : paragraphs.find((p) => {
|
|
7290
8411
|
const paragraphText = normalizeWhitespaceForTargeting(getParagraphText(p));
|
|
7291
8412
|
return paragraphText.length > 10 && normTarget.startsWith(paragraphText);
|
|
7292
8413
|
});
|
|
@@ -7294,7 +8415,7 @@ function findParagraphByBestTextMatch(xmlDoc, targetText, options = {}) {
|
|
|
7294
8415
|
onInfo(`[Fuzzy] Prefix match (target starts with paragraph): "${getParagraphText(startsWithMatch).trim().slice(0, 60)}..."`);
|
|
7295
8416
|
return startsWithMatch;
|
|
7296
8417
|
}
|
|
7297
|
-
const containsMatch = paragraphs.find((p) => {
|
|
8418
|
+
const containsMatch = entries ? entries.find((entry) => entry.normalizedText.length > 15 && normTarget.includes(entry.normalizedText))?.paragraph || null : paragraphs.find((p) => {
|
|
7298
8419
|
const paragraphText = normalizeWhitespaceForTargeting(getParagraphText(p));
|
|
7299
8420
|
return paragraphText.length > 15 && normTarget.includes(paragraphText);
|
|
7300
8421
|
});
|
|
@@ -7305,8 +8426,10 @@ function findParagraphByBestTextMatch(xmlDoc, targetText, options = {}) {
|
|
|
7305
8426
|
let bestScore = 0;
|
|
7306
8427
|
let bestParagraph = null;
|
|
7307
8428
|
const targetWords = new Set(normTarget.toLowerCase().split(/\s+/).filter((word) => word.length > 2));
|
|
7308
|
-
|
|
7309
|
-
|
|
8429
|
+
const candidateCount = entries?.length ?? paragraphs.length;
|
|
8430
|
+
for (let index = 0; index < candidateCount; index++) {
|
|
8431
|
+
const paragraph = entries?.[index]?.paragraph ?? paragraphs[index];
|
|
8432
|
+
const paragraphText = (entries?.[index]?.text ?? getParagraphText(paragraph)).trim();
|
|
7310
8433
|
if (!paragraphText) continue;
|
|
7311
8434
|
const paragraphWords = normalizeWhitespaceForTargeting(paragraphText).toLowerCase().split(/\s+/).filter((word) => word.length > 2);
|
|
7312
8435
|
const overlap = paragraphWords.filter((word) => targetWords.has(word)).length;
|
|
@@ -7328,14 +8451,154 @@ function resolveTargetParagraph(xmlDoc, options = {}) {
|
|
|
7328
8451
|
const onWarn = typeof options.onWarn === "function" ? options.onWarn : () => {
|
|
7329
8452
|
};
|
|
7330
8453
|
const opType = options.opType || "operation";
|
|
7331
|
-
const
|
|
7332
|
-
const
|
|
8454
|
+
const descriptor = options.targetDescriptor && typeof options.targetDescriptor === "object" ? options.targetDescriptor : null;
|
|
8455
|
+
const revisionView = descriptor?.revisionView === "rejected" ? "rejected" : "accepted";
|
|
8456
|
+
const cleanTargetText = String(descriptor?.exactText ?? descriptor?.text ?? options.targetText ?? "").trim();
|
|
8457
|
+
const parsedRef = parseParagraphReference(descriptor?.index ?? descriptor?.paragraphIndex ?? options.targetRef);
|
|
8458
|
+
const strictAmbiguity = options.strictAmbiguity === true;
|
|
8459
|
+
let paragraphMetadataIndex = options.paragraphMetadataIndex || null;
|
|
8460
|
+
if (paragraphMetadataIndex && paragraphMetadataIndex.revisionView !== revisionView) {
|
|
8461
|
+
paragraphMetadataIndex = options.metadataIndices?.[revisionView] || buildParagraphMetadataIndex(xmlDoc, { revisionView });
|
|
8462
|
+
} else if (!paragraphMetadataIndex) {
|
|
8463
|
+
paragraphMetadataIndex = buildParagraphMetadataIndex(xmlDoc, { revisionView });
|
|
8464
|
+
}
|
|
8465
|
+
if (descriptor?.paragraphId) {
|
|
8466
|
+
const byId = findParagraphById(xmlDoc, descriptor.paragraphId, paragraphMetadataIndex);
|
|
8467
|
+
if (!byId) {
|
|
8468
|
+
throw createTargetError(
|
|
8469
|
+
"TARGET_NOT_FOUND",
|
|
8470
|
+
`Target paragraphId not found: "${descriptor.paragraphId}".`
|
|
8471
|
+
);
|
|
8472
|
+
}
|
|
8473
|
+
const cachedEntry = paragraphMetadataIndex?.byParagraph?.get(byId) || null;
|
|
8474
|
+
const actualIndex = cachedEntry?.index ?? getDocumentParagraphNodes(xmlDoc).indexOf(byId) + 1;
|
|
8475
|
+
const actualFingerprint = cachedEntry?.fingerprint || createParagraphFingerprint(byId, { revisionView });
|
|
8476
|
+
const actualInTable = cachedEntry?.inTable ?? isParagraphInTable(byId);
|
|
8477
|
+
const actualText = cachedEntry?.normalizedText || normalizeWhitespaceForTargeting(extractCanonicalParagraphText(byId, { revisionView }));
|
|
8478
|
+
if (parsedRef != null && parsedRef !== actualIndex) {
|
|
8479
|
+
throw createTargetError(
|
|
8480
|
+
"TARGET_INDEX_MISMATCH",
|
|
8481
|
+
`Target paragraphId "${descriptor.paragraphId}" (index ${actualIndex}) does not match requested index ${parsedRef}.`,
|
|
8482
|
+
cachedEntry ? [serializeTargetCandidate(cachedEntry)] : null
|
|
8483
|
+
);
|
|
8484
|
+
}
|
|
8485
|
+
if (descriptor.fingerprint && descriptor.fingerprint !== actualFingerprint) {
|
|
8486
|
+
throw createTargetError(
|
|
8487
|
+
"TARGET_FINGERPRINT_MISMATCH",
|
|
8488
|
+
`Target paragraphId "${descriptor.paragraphId}" no longer matches its source fingerprint.`,
|
|
8489
|
+
cachedEntry ? [serializeTargetCandidate(cachedEntry)] : null
|
|
8490
|
+
);
|
|
8491
|
+
}
|
|
8492
|
+
if (typeof descriptor.inTable === "boolean" && descriptor.inTable !== actualInTable) {
|
|
8493
|
+
throw createTargetError(
|
|
8494
|
+
"TARGET_CONTEXT_MISMATCH",
|
|
8495
|
+
`Target paragraphId "${descriptor.paragraphId}" does not match the requested table context.`,
|
|
8496
|
+
cachedEntry ? [serializeTargetCandidate(cachedEntry)] : null
|
|
8497
|
+
);
|
|
8498
|
+
}
|
|
8499
|
+
if (cleanTargetText && actualText !== normalizeWhitespaceForTargeting(cleanTargetText)) {
|
|
8500
|
+
throw createTargetError(
|
|
8501
|
+
"TARGET_TEXT_MISMATCH",
|
|
8502
|
+
`Target paragraphId "${descriptor.paragraphId}" no longer matches the supplied text.`,
|
|
8503
|
+
cachedEntry ? [serializeTargetCandidate(cachedEntry)] : null
|
|
8504
|
+
);
|
|
8505
|
+
}
|
|
8506
|
+
if (descriptor.occurrence != null) {
|
|
8507
|
+
const textToFind = cleanTargetText || actualText;
|
|
8508
|
+
const textCandidates = findStrictTargetCandidates(xmlDoc, textToFind, paragraphMetadataIndex);
|
|
8509
|
+
const actualOccurrence = textCandidates.findIndex((c) => c.paragraph === byId) + 1;
|
|
8510
|
+
if (actualOccurrence === 0 || actualOccurrence !== descriptor.occurrence) {
|
|
8511
|
+
throw createTargetError(
|
|
8512
|
+
"TARGET_OCCURRENCE_MISMATCH",
|
|
8513
|
+
`Target paragraphId "${descriptor.paragraphId}" matches occurrence ${actualOccurrence}, not requested occurrence ${descriptor.occurrence}.`,
|
|
8514
|
+
textCandidates.map(serializeTargetCandidate)
|
|
8515
|
+
);
|
|
8516
|
+
}
|
|
8517
|
+
}
|
|
8518
|
+
return { paragraph: byId, resolvedBy: "paragraph_id" };
|
|
8519
|
+
}
|
|
8520
|
+
let candidates = [];
|
|
8521
|
+
if (cleanTargetText) {
|
|
8522
|
+
const unfilteredCandidates = findStrictTargetCandidates(xmlDoc, cleanTargetText, paragraphMetadataIndex);
|
|
8523
|
+
candidates = filterTargetCandidates(unfilteredCandidates, descriptor);
|
|
8524
|
+
if (descriptor?.fingerprint && unfilteredCandidates.length > 0 && candidates.length === 0) {
|
|
8525
|
+
throw createTargetError(
|
|
8526
|
+
"TARGET_FINGERPRINT_MISMATCH",
|
|
8527
|
+
"Target text matched, but no paragraph matched the supplied source fingerprint.",
|
|
8528
|
+
unfilteredCandidates.map(serializeTargetCandidate)
|
|
8529
|
+
);
|
|
8530
|
+
}
|
|
8531
|
+
if (typeof descriptor?.inTable === "boolean" && unfilteredCandidates.length > 0 && candidates.length === 0) {
|
|
8532
|
+
throw createTargetError(
|
|
8533
|
+
"TARGET_CONTEXT_MISMATCH",
|
|
8534
|
+
"Target text matched, but no paragraph matched the requested table context.",
|
|
8535
|
+
unfilteredCandidates.map(serializeTargetCandidate)
|
|
8536
|
+
);
|
|
8537
|
+
}
|
|
8538
|
+
if (descriptor?.occurrence) {
|
|
8539
|
+
const occurrenceMatch = candidates[descriptor.occurrence - 1] || null;
|
|
8540
|
+
if (!occurrenceMatch) {
|
|
8541
|
+
throw createTargetError(
|
|
8542
|
+
"TARGET_NOT_FOUND",
|
|
8543
|
+
`Target occurrence ${descriptor.occurrence} was not found.`,
|
|
8544
|
+
candidates.map(serializeTargetCandidate)
|
|
8545
|
+
);
|
|
8546
|
+
}
|
|
8547
|
+
return { paragraph: occurrenceMatch.paragraph, resolvedBy: "occurrence" };
|
|
8548
|
+
}
|
|
8549
|
+
if (strictAmbiguity) {
|
|
8550
|
+
if (parsedRef) {
|
|
8551
|
+
const byReference = candidates.find((candidate) => candidate.index === parsedRef) || null;
|
|
8552
|
+
if (byReference) return { paragraph: byReference.paragraph, resolvedBy: "ref" };
|
|
8553
|
+
if (descriptor?.fingerprint && candidates.length > 0) {
|
|
8554
|
+
throw createTargetError(
|
|
8555
|
+
"TARGET_FINGERPRINT_MISMATCH",
|
|
8556
|
+
`Target fingerprint does not match paragraph reference [P${parsedRef}].`,
|
|
8557
|
+
candidates.map(serializeTargetCandidate)
|
|
8558
|
+
);
|
|
8559
|
+
}
|
|
8560
|
+
if (candidates.length === 1) {
|
|
8561
|
+
return { paragraph: candidates[0].paragraph, resolvedBy: "strict_text_after_ref_drift" };
|
|
8562
|
+
}
|
|
8563
|
+
}
|
|
8564
|
+
if (candidates.length > 1) {
|
|
8565
|
+
throw createTargetError(
|
|
8566
|
+
"AMBIGUOUS_TARGET",
|
|
8567
|
+
`Target text matched ${candidates.length} paragraphs; provide paragraphId, index, occurrence, or fingerprint.`,
|
|
8568
|
+
candidates.map(serializeTargetCandidate)
|
|
8569
|
+
);
|
|
8570
|
+
}
|
|
8571
|
+
if (candidates.length === 0) {
|
|
8572
|
+
throw createTargetNotFoundError(`Target paragraph not found: "${cleanTargetText}"`);
|
|
8573
|
+
}
|
|
8574
|
+
}
|
|
8575
|
+
if (!parsedRef && candidates.length === 1) {
|
|
8576
|
+
const candidate = candidates[0];
|
|
8577
|
+
return {
|
|
8578
|
+
paragraph: candidate.paragraph,
|
|
8579
|
+
resolvedBy: descriptor?.fingerprint ? "fingerprint" : "strict_text"
|
|
8580
|
+
};
|
|
8581
|
+
}
|
|
8582
|
+
}
|
|
7333
8583
|
if (parsedRef) {
|
|
7334
|
-
const byRef = findParagraphByReference(xmlDoc, parsedRef);
|
|
8584
|
+
const byRef = findParagraphByReference(xmlDoc, parsedRef, paragraphMetadataIndex);
|
|
7335
8585
|
if (byRef) {
|
|
8586
|
+
const cached = paragraphMetadataIndex?.byParagraph?.get(byRef) || null;
|
|
8587
|
+
if (descriptor?.fingerprint && descriptor.fingerprint !== cached?.fingerprint) {
|
|
8588
|
+
throw createTargetError(
|
|
8589
|
+
"TARGET_FINGERPRINT_MISMATCH",
|
|
8590
|
+
`Target fingerprint does not match paragraph reference [P${parsedRef}].`
|
|
8591
|
+
);
|
|
8592
|
+
}
|
|
8593
|
+
if (typeof descriptor?.inTable === "boolean" && descriptor.inTable !== cached?.inTable) {
|
|
8594
|
+
throw createTargetError(
|
|
8595
|
+
"TARGET_CONTEXT_MISMATCH",
|
|
8596
|
+
`Target paragraph reference [P${parsedRef}] does not match requested table context.`
|
|
8597
|
+
);
|
|
8598
|
+
}
|
|
7336
8599
|
if (cleanTargetText) {
|
|
7337
|
-
const strictMatch = findParagraphByStrictText(xmlDoc, cleanTargetText);
|
|
7338
|
-
const byRefText =
|
|
8600
|
+
const strictMatch = findParagraphByStrictText(xmlDoc, cleanTargetText, { paragraphMetadataIndex });
|
|
8601
|
+
const byRefText = (cached?.text || extractCanonicalParagraphText(byRef, { revisionView })).trim();
|
|
7339
8602
|
const byRefNorm = normalizeWhitespaceForTargeting(byRefText);
|
|
7340
8603
|
const targetNorm = normalizeWhitespaceForTargeting(cleanTargetText);
|
|
7341
8604
|
const hasDrift = byRefNorm !== targetNorm;
|
|
@@ -7344,7 +8607,7 @@ function resolveTargetParagraph(xmlDoc, options = {}) {
|
|
|
7344
8607
|
return { paragraph: strictMatch, resolvedBy: "strict_text_after_ref_drift" };
|
|
7345
8608
|
}
|
|
7346
8609
|
if (hasDrift) {
|
|
7347
|
-
const fuzzyMatch = findParagraphByBestTextMatch(xmlDoc, cleanTargetText, { onInfo });
|
|
8610
|
+
const fuzzyMatch = findParagraphByBestTextMatch(xmlDoc, cleanTargetText, { onInfo, paragraphMetadataIndex });
|
|
7348
8611
|
if (fuzzyMatch && fuzzyMatch !== byRef) {
|
|
7349
8612
|
onInfo(`[Target] [P${parsedRef}] drifted for ${opType}; using fuzzy text rematch.`);
|
|
7350
8613
|
return { paragraph: fuzzyMatch, resolvedBy: "fuzzy_text_after_ref_drift" };
|
|
@@ -7360,10 +8623,22 @@ function resolveTargetParagraph(xmlDoc, options = {}) {
|
|
|
7360
8623
|
}
|
|
7361
8624
|
onWarn(`[WARN] Target reference [P${parsedRef}] not found; falling back to text matching for ${opType}.`);
|
|
7362
8625
|
}
|
|
7363
|
-
if (cleanTargetText) {
|
|
7364
|
-
const strictMatch = findParagraphByStrictText(xmlDoc, cleanTargetText);
|
|
7365
|
-
if (strictMatch)
|
|
7366
|
-
|
|
8626
|
+
if (cleanTargetText && !strictAmbiguity) {
|
|
8627
|
+
const strictMatch = findParagraphByStrictText(xmlDoc, cleanTargetText, { paragraphMetadataIndex });
|
|
8628
|
+
if (strictMatch) {
|
|
8629
|
+
const candidateCount = candidates.length;
|
|
8630
|
+
if (candidateCount > 1) {
|
|
8631
|
+
const warningMsg = `AMBIGUOUS_TARGET_HEURISTIC_USED: Target text matched ${candidateCount} paragraphs; permissive resolution chose candidate 1. Migrate to strict targeting (e.g. strictTargets: true with paragraphId, index, occurrence, or fingerprint) before v1.0.0.`;
|
|
8632
|
+
onWarn(warningMsg);
|
|
8633
|
+
return {
|
|
8634
|
+
paragraph: strictMatch,
|
|
8635
|
+
resolvedBy: "strict_text",
|
|
8636
|
+
warnings: [warningMsg]
|
|
8637
|
+
};
|
|
8638
|
+
}
|
|
8639
|
+
return { paragraph: strictMatch, resolvedBy: "strict_text" };
|
|
8640
|
+
}
|
|
8641
|
+
const fuzzyMatch = findParagraphByBestTextMatch(xmlDoc, cleanTargetText, { onInfo, paragraphMetadataIndex });
|
|
7367
8642
|
if (fuzzyMatch) return { paragraph: fuzzyMatch, resolvedBy: "fuzzy_text" };
|
|
7368
8643
|
}
|
|
7369
8644
|
if (cleanTargetText) throw createTargetNotFoundError(`Target paragraph not found: "${cleanTargetText}"`);
|
|
@@ -7373,24 +8648,65 @@ function resolveTargetParagraph(xmlDoc, options = {}) {
|
|
|
7373
8648
|
function isParagraphInTable(paragraph) {
|
|
7374
8649
|
return !!findContainingWordElement(paragraph, "tbl");
|
|
7375
8650
|
}
|
|
7376
|
-
function findStrictTargetCandidates(xmlDoc, targetText) {
|
|
8651
|
+
function findStrictTargetCandidates(xmlDoc, targetText, optionsOrIndex = null) {
|
|
7377
8652
|
const normalizedTarget = normalizeWhitespaceForTargeting(targetText);
|
|
7378
8653
|
if (!normalizedTarget) return [];
|
|
8654
|
+
const metadataIndex = optionsOrIndex?.byNormalizedText ? optionsOrIndex : optionsOrIndex?.paragraphMetadataIndex || null;
|
|
8655
|
+
const revisionView = optionsOrIndex?.revisionView || metadataIndex?.revisionView || "accepted";
|
|
8656
|
+
if (metadataIndex?.byNormalizedText && metadataIndex.revisionView === revisionView) {
|
|
8657
|
+
return Array.from(metadataIndex.byNormalizedText.get(normalizedTarget) || []);
|
|
8658
|
+
}
|
|
7379
8659
|
const paragraphs = getDocumentParagraphNodes(xmlDoc);
|
|
7380
8660
|
const candidates = [];
|
|
7381
8661
|
for (let i = 0; i < paragraphs.length; i++) {
|
|
7382
8662
|
const paragraph = paragraphs[i];
|
|
7383
|
-
const paragraphText =
|
|
8663
|
+
const paragraphText = extractCanonicalParagraphText(paragraph, { revisionView }).trim();
|
|
7384
8664
|
if (!paragraphText) continue;
|
|
7385
8665
|
if (normalizeWhitespaceForTargeting(paragraphText) !== normalizedTarget) continue;
|
|
7386
8666
|
candidates.push({
|
|
7387
8667
|
paragraph,
|
|
7388
8668
|
index: i + 1,
|
|
7389
|
-
inTable: isParagraphInTable(paragraph)
|
|
8669
|
+
inTable: isParagraphInTable(paragraph),
|
|
8670
|
+
paragraphId: getParagraphId2(paragraph),
|
|
8671
|
+
fingerprint: createParagraphFingerprint(paragraph, {
|
|
8672
|
+
text: paragraphText,
|
|
8673
|
+
index: i + 1,
|
|
8674
|
+
revisionView
|
|
8675
|
+
}),
|
|
8676
|
+
text: paragraphText,
|
|
8677
|
+
revisionView
|
|
7390
8678
|
});
|
|
7391
8679
|
}
|
|
7392
8680
|
return candidates;
|
|
7393
8681
|
}
|
|
8682
|
+
function serializeTargetCandidate(candidate) {
|
|
8683
|
+
return {
|
|
8684
|
+
index: candidate.index,
|
|
8685
|
+
paragraphId: candidate.paragraphId || null,
|
|
8686
|
+
text: candidate.text,
|
|
8687
|
+
inTable: candidate.inTable,
|
|
8688
|
+
fingerprint: candidate.fingerprint,
|
|
8689
|
+
revisionView: candidate.revisionView || "accepted"
|
|
8690
|
+
};
|
|
8691
|
+
}
|
|
8692
|
+
function filterTargetCandidates(candidates, descriptor) {
|
|
8693
|
+
let scoped = candidates.slice();
|
|
8694
|
+
if (descriptor?.paragraphId) {
|
|
8695
|
+
scoped = scoped.filter((candidate) => candidate.paragraphId === descriptor.paragraphId);
|
|
8696
|
+
}
|
|
8697
|
+
if (typeof descriptor?.inTable === "boolean") {
|
|
8698
|
+
scoped = scoped.filter((candidate) => candidate.inTable === descriptor.inTable);
|
|
8699
|
+
}
|
|
8700
|
+
if (descriptor?.fingerprint) {
|
|
8701
|
+
scoped = scoped.filter((candidate) => candidate.fingerprint === descriptor.fingerprint);
|
|
8702
|
+
}
|
|
8703
|
+
return scoped;
|
|
8704
|
+
}
|
|
8705
|
+
function findParagraphById(xmlDoc, paragraphId, paragraphMetadataIndex = null) {
|
|
8706
|
+
if (!paragraphId) return null;
|
|
8707
|
+
if (paragraphMetadataIndex?.byId) return paragraphMetadataIndex.byId.get(paragraphId)?.paragraph || null;
|
|
8708
|
+
return getDocumentParagraphNodes(xmlDoc).find((paragraph) => getParagraphId2(paragraph) === paragraphId) || null;
|
|
8709
|
+
}
|
|
7394
8710
|
function selectBestTargetCandidate(candidates, parsedRef, expectedInTable = null) {
|
|
7395
8711
|
if (!Array.isArray(candidates) || candidates.length === 0) return null;
|
|
7396
8712
|
let scoped = candidates.slice();
|
|
@@ -7403,16 +8719,18 @@ function selectBestTargetCandidate(candidates, parsedRef, expectedInTable = null
|
|
|
7403
8719
|
}
|
|
7404
8720
|
return scoped[0] || null;
|
|
7405
8721
|
}
|
|
7406
|
-
function buildTargetReferenceSnapshot(xmlDoc) {
|
|
7407
|
-
const
|
|
8722
|
+
function buildTargetReferenceSnapshot(xmlDoc, paragraphMetadataIndex = null) {
|
|
8723
|
+
const entries = paragraphMetadataIndex?.entries || null;
|
|
8724
|
+
const paragraphs = entries ? null : getDocumentParagraphNodes(xmlDoc);
|
|
7408
8725
|
const snapshot = /* @__PURE__ */ new Map();
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
const
|
|
8726
|
+
const paragraphCount = entries?.length ?? paragraphs.length;
|
|
8727
|
+
for (let i = 0; i < paragraphCount; i++) {
|
|
8728
|
+
const paragraph = entries?.[i]?.paragraph ?? paragraphs[i];
|
|
8729
|
+
const text = (entries?.[i]?.text ?? getParagraphText(paragraph)).trim();
|
|
7412
8730
|
snapshot.set(i + 1, {
|
|
7413
8731
|
text,
|
|
7414
|
-
normalizedText: normalizeWhitespaceForTargeting(text),
|
|
7415
|
-
inTable: isParagraphInTable(paragraph)
|
|
8732
|
+
normalizedText: entries?.[i]?.normalizedText ?? normalizeWhitespaceForTargeting(text),
|
|
8733
|
+
inTable: entries?.[i]?.inTable ?? isParagraphInTable(paragraph)
|
|
7416
8734
|
});
|
|
7417
8735
|
}
|
|
7418
8736
|
return snapshot;
|
|
@@ -7429,7 +8747,7 @@ function resolveTargetParagraphWithSnapshot(xmlDoc, options = {}) {
|
|
|
7429
8747
|
const expectedText = cleanTargetText || snapshotEntry.text || "";
|
|
7430
8748
|
const expectedNorm = normalizeWhitespaceForTargeting(expectedText);
|
|
7431
8749
|
if (!expectedNorm) return resolved;
|
|
7432
|
-
const resolvedNorm = normalizeWhitespaceForTargeting(getParagraphText(resolved.paragraph));
|
|
8750
|
+
const resolvedNorm = options.paragraphMetadataIndex?.byParagraph?.get(resolved.paragraph)?.normalizedText || normalizeWhitespaceForTargeting(getParagraphText(resolved.paragraph));
|
|
7433
8751
|
if (resolvedNorm === expectedNorm) return resolved;
|
|
7434
8752
|
const candidateTexts = [];
|
|
7435
8753
|
if (cleanTargetText) candidateTexts.push(cleanTargetText);
|
|
@@ -7441,7 +8759,7 @@ function resolveTargetParagraphWithSnapshot(xmlDoc, options = {}) {
|
|
|
7441
8759
|
}
|
|
7442
8760
|
let bestCandidate = null;
|
|
7443
8761
|
for (const candidateText of candidateTexts) {
|
|
7444
|
-
const candidates = findStrictTargetCandidates(xmlDoc, candidateText);
|
|
8762
|
+
const candidates = findStrictTargetCandidates(xmlDoc, candidateText, options.paragraphMetadataIndex || null);
|
|
7445
8763
|
const selected = selectBestTargetCandidate(candidates, parsedRef, snapshotEntry.inTable);
|
|
7446
8764
|
if (!selected) continue;
|
|
7447
8765
|
if (!bestCandidate) bestCandidate = selected;
|
|
@@ -7496,11 +8814,11 @@ function resolveParagraphRangeByRefs(xmlDoc, startRef, endRef, options = {}) {
|
|
|
7496
8814
|
}
|
|
7497
8815
|
|
|
7498
8816
|
// core/list-targeting.js
|
|
7499
|
-
function getFirstDescendantByLocalName(node,
|
|
8817
|
+
function getFirstDescendantByLocalName(node, localName2) {
|
|
7500
8818
|
if (!node || typeof node.getElementsByTagNameNS !== "function") return null;
|
|
7501
|
-
const namespaced = node.getElementsByTagNameNS(WORD_MAIN_NS,
|
|
8819
|
+
const namespaced = node.getElementsByTagNameNS(WORD_MAIN_NS, localName2);
|
|
7502
8820
|
if (namespaced.length > 0) return namespaced[0];
|
|
7503
|
-
const anyNs = node.getElementsByTagNameNS("*",
|
|
8821
|
+
const anyNs = node.getElementsByTagNameNS("*", localName2);
|
|
7504
8822
|
return anyNs.length > 0 ? anyNs[0] : null;
|
|
7505
8823
|
}
|
|
7506
8824
|
function readValAttribute(element) {
|
|
@@ -7511,18 +8829,13 @@ function readValAttribute(element) {
|
|
|
7511
8829
|
}
|
|
7512
8830
|
return element.getAttribute("w:val") || element.getAttribute("val") || null;
|
|
7513
8831
|
}
|
|
7514
|
-
function parseOutlineLevelFromMarker(marker) {
|
|
7515
|
-
const normalized = String(marker || "").trim();
|
|
7516
|
-
if (!/^\d+(?:\.\d+)+\.?$/.test(normalized)) return null;
|
|
7517
|
-
const parts = normalized.replace(/\.$/, "").split(".");
|
|
7518
|
-
return Math.max(0, parts.length - 1);
|
|
7519
|
-
}
|
|
7520
|
-
var REDUNDANT_LIST_PREFIX_REGEX = /^(?:(?:\d+(?:\.\d+)*\.?|\((?:\d+|[a-zA-Z]|[ivxlcIVXLC]+)\)|[a-zA-Z]\.|[ivxlcIVXLC]+\.|[-*+\u2022]))\s+/;
|
|
7521
8832
|
function stripRedundantLeadingListMarkers(text) {
|
|
7522
8833
|
let value = String(text || "").trim();
|
|
7523
8834
|
let passes = 0;
|
|
7524
|
-
while (passes < 4
|
|
7525
|
-
|
|
8835
|
+
while (passes < 4) {
|
|
8836
|
+
const stripped = stripListMarker(value);
|
|
8837
|
+
if (stripped === value) break;
|
|
8838
|
+
value = stripped.trimStart();
|
|
7526
8839
|
passes++;
|
|
7527
8840
|
}
|
|
7528
8841
|
return value.trim();
|
|
@@ -7534,19 +8847,16 @@ function parseModifiedListItems(modifiedText) {
|
|
|
7534
8847
|
for (const rawLine of rawLines) {
|
|
7535
8848
|
const line = rawLine.trimEnd();
|
|
7536
8849
|
if (!line.trim()) continue;
|
|
7537
|
-
const
|
|
7538
|
-
if (
|
|
8850
|
+
const parsed = parseListItem(line, { indentSpaces: 2 });
|
|
8851
|
+
if (parsed) {
|
|
7539
8852
|
hasListMarkers = true;
|
|
7540
|
-
const marker = markerMatch[2];
|
|
7541
|
-
const markerType = /^[-*+\u2022]$/.test(marker) ? "bullet" : "numbered";
|
|
7542
|
-
const level = Math.floor((markerMatch[1] || "").length / 2);
|
|
7543
8853
|
items.push({
|
|
7544
8854
|
kind: "list",
|
|
7545
|
-
markerType,
|
|
7546
|
-
level,
|
|
7547
|
-
marker,
|
|
7548
|
-
outlineLevel:
|
|
7549
|
-
text: stripRedundantLeadingListMarkers(
|
|
8855
|
+
markerType: parsed.markerType,
|
|
8856
|
+
level: parsed.level,
|
|
8857
|
+
marker: parsed.marker,
|
|
8858
|
+
outlineLevel: parsed.outlineLevel,
|
|
8859
|
+
text: stripRedundantLeadingListMarkers(parsed.text)
|
|
7550
8860
|
});
|
|
7551
8861
|
continue;
|
|
7552
8862
|
}
|
|
@@ -7573,7 +8883,7 @@ function resolveInsertionLevel(item, anchorLevel, baselineLevel) {
|
|
|
7573
8883
|
}
|
|
7574
8884
|
function shouldPromoteBulletInsertionsToChildDepth(parsedItems, normalizedTargetText, anchorLevel) {
|
|
7575
8885
|
if (!Array.isArray(parsedItems) || parsedItems.length < 2) return false;
|
|
7576
|
-
if (!Number.isInteger(anchorLevel) || anchorLevel <
|
|
8886
|
+
if (!Number.isInteger(anchorLevel) || anchorLevel < 0) return false;
|
|
7577
8887
|
const firstItem = parsedItems[0];
|
|
7578
8888
|
const trailingListItems = parsedItems.slice(1).filter((item) => item.kind === "list");
|
|
7579
8889
|
if (trailingListItems.length === 0) return false;
|
|
@@ -7633,7 +8943,7 @@ function getParagraphListInfo(paragraph) {
|
|
|
7633
8943
|
const numIdEl = getFirstDescendantByLocalName(numPr, "numId");
|
|
7634
8944
|
if (!numIdEl) return null;
|
|
7635
8945
|
const numId = readValAttribute(numIdEl);
|
|
7636
|
-
if (!numId) return null;
|
|
8946
|
+
if (!numId || numId === "0") return null;
|
|
7637
8947
|
const ilvlEl = getFirstDescendantByLocalName(numPr, "ilvl");
|
|
7638
8948
|
const ilvlRaw = readValAttribute(ilvlEl);
|
|
7639
8949
|
const ilvl = Number.parseInt(ilvlRaw || "0", 10);
|
|
@@ -7780,18 +9090,13 @@ function parseMarkdownListContent(content) {
|
|
|
7780
9090
|
const items = [];
|
|
7781
9091
|
for (const line of lines) {
|
|
7782
9092
|
if (!line.trim()) continue;
|
|
7783
|
-
const
|
|
7784
|
-
if (
|
|
7785
|
-
const indent = markerMatch[1] || "";
|
|
7786
|
-
const marker = markerMatch[2].trim();
|
|
7787
|
-
const text = stripListMarker(line, { allowZeroSpaceAfterMarker: false }).trim();
|
|
7788
|
-
const level = Math.floor(indent.length / 2);
|
|
7789
|
-
const isBullet = /^[-*+\u2022]$/.test(marker);
|
|
9093
|
+
const parsed = parseListItem(line, { allowZeroSpaceAfterMarker: false, indentSpaces: 2 });
|
|
9094
|
+
if (parsed) {
|
|
7790
9095
|
items.push({
|
|
7791
|
-
type:
|
|
7792
|
-
level,
|
|
7793
|
-
text,
|
|
7794
|
-
marker
|
|
9096
|
+
type: parsed.markerType,
|
|
9097
|
+
level: parsed.level,
|
|
9098
|
+
text: parsed.text.trim(),
|
|
9099
|
+
marker: parsed.marker
|
|
7795
9100
|
});
|
|
7796
9101
|
continue;
|
|
7797
9102
|
}
|
|
@@ -7815,16 +9120,6 @@ function hasListItems(parsedListData) {
|
|
|
7815
9120
|
}
|
|
7816
9121
|
|
|
7817
9122
|
// orchestration/list-markdown.js
|
|
7818
|
-
function inferNumberingStyleFromMarker(marker) {
|
|
7819
|
-
const m = (marker || "").trim();
|
|
7820
|
-
if (!m) return "decimal";
|
|
7821
|
-
if (/^\d+(?:\.\d+)*\.?$/.test(m) || /^\(\d+\)$/.test(m)) return "decimal";
|
|
7822
|
-
if (/^[ivxlcdm]+\.$/.test(m)) return "lowerRoman";
|
|
7823
|
-
if (/^[IVXLCDM]{2,}\.$/.test(m)) return "upperRoman";
|
|
7824
|
-
if (/^[a-z]\.$/.test(m)) return "lowerAlpha";
|
|
7825
|
-
if (/^[A-Z]\.$/.test(m)) return "upperAlpha";
|
|
7826
|
-
return "decimal";
|
|
7827
|
-
}
|
|
7828
9123
|
function buildListMarkdown(itemsWithLevels, listType, numberingStyle) {
|
|
7829
9124
|
const levelCounters = /* @__PURE__ */ new Map();
|
|
7830
9125
|
const lines = [];
|
|
@@ -7845,7 +9140,6 @@ function buildListMarkdown(itemsWithLevels, listType, numberingStyle) {
|
|
|
7845
9140
|
}
|
|
7846
9141
|
function normalizeListItemsWithLevels(rawItems, options = {}) {
|
|
7847
9142
|
const indentSpaces = Math.max(1, Number(options.indentSpaces) || 4);
|
|
7848
|
-
const markersRegex = /^((?:\d+(?:\.\d+)*\.?|\((?:\d+|[a-zA-Z]|[ivxlcIVXLC]+)\)|[a-zA-Z]\.|\d+\.|[ivxlcIVXLC]+\.|[-*•])\s*)/;
|
|
7849
9143
|
return (rawItems || []).map((rawItem) => {
|
|
7850
9144
|
const item = String(rawItem ?? "");
|
|
7851
9145
|
const indentMatch = item.match(/^(\s*)/);
|
|
@@ -7853,10 +9147,10 @@ function normalizeListItemsWithLevels(rawItems, options = {}) {
|
|
|
7853
9147
|
const level = Math.floor(indentSize / indentSpaces);
|
|
7854
9148
|
let stripped = item.trim();
|
|
7855
9149
|
let removedMarker = null;
|
|
7856
|
-
const markerMatch = stripped
|
|
9150
|
+
const markerMatch = matchListMarker(stripped, { allowZeroSpaceAfterMarker: true });
|
|
7857
9151
|
if (markerMatch) {
|
|
7858
|
-
removedMarker = markerMatch[
|
|
7859
|
-
stripped = stripped
|
|
9152
|
+
removedMarker = markerMatch[2].trim() || null;
|
|
9153
|
+
stripped = stripListMarker(stripped, { allowZeroSpaceAfterMarker: true });
|
|
7860
9154
|
}
|
|
7861
9155
|
return {
|
|
7862
9156
|
text: stripped.trim(),
|
|
@@ -8009,10 +9303,10 @@ function clearSingleLineListFallbackExplicitSequence(sequenceState, numberingKey
|
|
|
8009
9303
|
if (!numberingKey) return;
|
|
8010
9304
|
sequenceState.explicitByNumberingKey.delete(String(numberingKey));
|
|
8011
9305
|
}
|
|
8012
|
-
function getDirectWordChild2(element,
|
|
9306
|
+
function getDirectWordChild2(element, localName2) {
|
|
8013
9307
|
if (!element) return null;
|
|
8014
9308
|
return Array.from(element.childNodes || []).find(
|
|
8015
|
-
(node) => node && node.nodeType === 1 && node.namespaceURI === "http://schemas.openxmlformats.org/wordprocessingml/2006/main" && node.localName ===
|
|
9309
|
+
(node) => node && node.nodeType === 1 && node.namespaceURI === "http://schemas.openxmlformats.org/wordprocessingml/2006/main" && node.localName === localName2
|
|
8016
9310
|
) || null;
|
|
8017
9311
|
}
|
|
8018
9312
|
function enforceListBindingOnParagraphNodes(nodes, options = {}) {
|
|
@@ -8198,13 +9492,21 @@ async function executeSingleLineListStructuralFallback(plan, options = {}) {
|
|
|
8198
9492
|
}
|
|
8199
9493
|
const author = options.author || "AI";
|
|
8200
9494
|
const generateRedlines = options.generateRedlines ?? true;
|
|
8201
|
-
const
|
|
8202
|
-
const result = await pipeline.executeListGeneration(
|
|
9495
|
+
const result = options.pipeline ? await options.pipeline.executeListGeneration(
|
|
8203
9496
|
plan.listInput,
|
|
8204
9497
|
null,
|
|
8205
9498
|
null,
|
|
8206
9499
|
String(plan.originalText || "")
|
|
8207
|
-
)
|
|
9500
|
+
) : await executeListGeneration({
|
|
9501
|
+
cleanText: plan.listInput,
|
|
9502
|
+
numberingContext: null,
|
|
9503
|
+
originalRunModel: [],
|
|
9504
|
+
originalText: String(plan.originalText || ""),
|
|
9505
|
+
generateRedlines,
|
|
9506
|
+
author,
|
|
9507
|
+
revisionIdAllocator: options.revisionIdAllocator || null,
|
|
9508
|
+
numberingService: new NumberingService()
|
|
9509
|
+
});
|
|
8208
9510
|
const rawOxml = result?.oxml || result?.ooxml || "";
|
|
8209
9511
|
const oxml = trimTrailingBlankParagraph(rawOxml);
|
|
8210
9512
|
const generatedNumId = extractFirstParagraphNumIdFromOxml(oxml);
|
|
@@ -8379,10 +9681,10 @@ function validateRedlineOoxml(oxml) {
|
|
|
8379
9681
|
}
|
|
8380
9682
|
|
|
8381
9683
|
// core/table-targeting.js
|
|
8382
|
-
function getDirectWordChildren(element,
|
|
9684
|
+
function getDirectWordChildren(element, localName2) {
|
|
8383
9685
|
if (!element) return [];
|
|
8384
9686
|
return Array.from(element.childNodes || []).filter(
|
|
8385
|
-
(node) => node && node.nodeType === 1 && node.namespaceURI === WORD_MAIN_NS && node.localName ===
|
|
9687
|
+
(node) => node && node.nodeType === 1 && node.namespaceURI === WORD_MAIN_NS && node.localName === localName2
|
|
8386
9688
|
);
|
|
8387
9689
|
}
|
|
8388
9690
|
function escapeMarkdownCell(text) {
|
|
@@ -8627,8 +9929,8 @@ function hasXmlParseError(doc) {
|
|
|
8627
9929
|
if (doc.documentElement.localName === "parsererror") return true;
|
|
8628
9930
|
return doc.getElementsByTagName("parsererror").length > 0;
|
|
8629
9931
|
}
|
|
8630
|
-
function isDirectWordChild(node,
|
|
8631
|
-
return !!(node && node.nodeType === 1 && node.namespaceURI === WORD_MAIN_NS && node.localName ===
|
|
9932
|
+
function isDirectWordChild(node, localName2) {
|
|
9933
|
+
return !!(node && node.nodeType === 1 && node.namespaceURI === WORD_MAIN_NS && node.localName === localName2);
|
|
8632
9934
|
}
|
|
8633
9935
|
function insertNumberingNodeInSchemaOrder(root, node, kind) {
|
|
8634
9936
|
if (!root || !node) return;
|
|
@@ -8820,17 +10122,17 @@ function getWordParagraphs(doc) {
|
|
|
8820
10122
|
if (namespaced.length > 0) return namespaced;
|
|
8821
10123
|
return Array.from(doc.getElementsByTagNameNS("*", "p")).filter((node) => node?.localName === "p");
|
|
8822
10124
|
}
|
|
8823
|
-
function getDirectWordChild3(node,
|
|
10125
|
+
function getDirectWordChild3(node, localName2) {
|
|
8824
10126
|
const children = Array.from(node?.childNodes || []);
|
|
8825
10127
|
for (const child of children) {
|
|
8826
10128
|
if (child?.nodeType !== 1) continue;
|
|
8827
10129
|
if (child.namespaceURI !== NS_W) continue;
|
|
8828
|
-
if (child.localName ===
|
|
10130
|
+
if (child.localName === localName2) return child;
|
|
8829
10131
|
}
|
|
8830
10132
|
return null;
|
|
8831
10133
|
}
|
|
8832
|
-
function getWordDescendants(node,
|
|
8833
|
-
return Array.from(node?.getElementsByTagNameNS?.(NS_W,
|
|
10134
|
+
function getWordDescendants(node, localName2) {
|
|
10135
|
+
return Array.from(node?.getElementsByTagNameNS?.(NS_W, localName2) || []);
|
|
8834
10136
|
}
|
|
8835
10137
|
function getWordAttribute(element, names) {
|
|
8836
10138
|
if (!element) return "";
|
|
@@ -8840,32 +10142,6 @@ function getWordAttribute(element, names) {
|
|
|
8840
10142
|
}
|
|
8841
10143
|
return "";
|
|
8842
10144
|
}
|
|
8843
|
-
function hasWordAncestorWithin(node, localName, boundary) {
|
|
8844
|
-
let cursor = node?.parentNode || null;
|
|
8845
|
-
while (cursor && cursor !== boundary) {
|
|
8846
|
-
if (cursor.nodeType === 1 && cursor.namespaceURI === NS_W && cursor.localName === localName) {
|
|
8847
|
-
return true;
|
|
8848
|
-
}
|
|
8849
|
-
cursor = cursor.parentNode;
|
|
8850
|
-
}
|
|
8851
|
-
return false;
|
|
8852
|
-
}
|
|
8853
|
-
function readRunText(run) {
|
|
8854
|
-
let text = "";
|
|
8855
|
-
for (const child of Array.from(run?.childNodes || [])) {
|
|
8856
|
-
if (!child || child.nodeType !== 1 || child.namespaceURI !== NS_W) continue;
|
|
8857
|
-
if (child.localName === "t") {
|
|
8858
|
-
text += child.textContent || "";
|
|
8859
|
-
} else if (child.localName === "tab") {
|
|
8860
|
-
text += " ";
|
|
8861
|
-
} else if (child.localName === "br" || child.localName === "cr") {
|
|
8862
|
-
text += "\n";
|
|
8863
|
-
} else if (child.localName === "noBreakHyphen") {
|
|
8864
|
-
text += "\u2011";
|
|
8865
|
-
}
|
|
8866
|
-
}
|
|
8867
|
-
return text;
|
|
8868
|
-
}
|
|
8869
10145
|
function getRunFormatting(run) {
|
|
8870
10146
|
const rPr = getDirectWordChild3(run, "rPr");
|
|
8871
10147
|
if (!rPr) return { bold: false, italic: false };
|
|
@@ -8882,9 +10158,8 @@ function collectParagraphSegments(paragraph) {
|
|
|
8882
10158
|
const segments = [];
|
|
8883
10159
|
const runs = Array.from(paragraph?.getElementsByTagNameNS?.(NS_W, "r") || []);
|
|
8884
10160
|
for (const run of runs) {
|
|
8885
|
-
if (
|
|
8886
|
-
|
|
8887
|
-
const text = readRunText(run);
|
|
10161
|
+
if (!isNodeVisibleInRevisionView(run, paragraph, "accepted")) continue;
|
|
10162
|
+
const text = readCanonicalRunText(run, { boundary: paragraph, revisionView: "accepted" });
|
|
8888
10163
|
if (!text) continue;
|
|
8889
10164
|
segments.push({
|
|
8890
10165
|
text,
|
|
@@ -8955,9 +10230,9 @@ function paragraphToMarkdown(paragraph) {
|
|
|
8955
10230
|
const inline = segments.map((segment) => wrapRunMarkdown(segment.text, segment)).join("");
|
|
8956
10231
|
const normalizedInline = normalizeInlineWhitespace(inline);
|
|
8957
10232
|
if (!normalizedInline) return "";
|
|
8958
|
-
const
|
|
8959
|
-
if (
|
|
8960
|
-
return `${"#".repeat(
|
|
10233
|
+
const headingLevel2 = parseHeadingLevel(paragraph);
|
|
10234
|
+
if (headingLevel2 != null) {
|
|
10235
|
+
return `${"#".repeat(headingLevel2)} ${normalizedInline}`;
|
|
8961
10236
|
}
|
|
8962
10237
|
const list = parseListInfo(paragraph);
|
|
8963
10238
|
if (list) {
|
|
@@ -9000,13 +10275,512 @@ function ingestWordOoxmlToMarkdownResult(ooxml) {
|
|
|
9000
10275
|
return { text: lines.join("\n\n").trim(), status: "ok", warnings: parsed.warnings };
|
|
9001
10276
|
}
|
|
9002
10277
|
|
|
10278
|
+
// services/revision-token.js
|
|
10279
|
+
var textEncoder = new TextEncoder();
|
|
10280
|
+
function normalizeOpcEntryName(name) {
|
|
10281
|
+
if (typeof name !== "string") {
|
|
10282
|
+
throw new TypeError(`Entry name must be a string, got ${typeof name}`);
|
|
10283
|
+
}
|
|
10284
|
+
let normalized = name.replace(/\\/g, "/");
|
|
10285
|
+
normalized = normalized.replace(/^\/+/, "");
|
|
10286
|
+
while (normalized.startsWith("./")) {
|
|
10287
|
+
normalized = normalized.slice(2);
|
|
10288
|
+
}
|
|
10289
|
+
normalized = normalized.replace(/\/+/g, "/");
|
|
10290
|
+
if (!normalized) {
|
|
10291
|
+
throw new Error(`Invalid empty entry name: "${name}"`);
|
|
10292
|
+
}
|
|
10293
|
+
return normalized;
|
|
10294
|
+
}
|
|
10295
|
+
function buildRevisionTokenFraming({ scope, entries = [] }) {
|
|
10296
|
+
if (typeof scope !== "string" || !scope) {
|
|
10297
|
+
throw new TypeError("Revision token scope must be a non-empty string.");
|
|
10298
|
+
}
|
|
10299
|
+
const magicBytes = textEncoder.encode("docx-redline-revision-token\0");
|
|
10300
|
+
const version = 1;
|
|
10301
|
+
const scopeBytes = textEncoder.encode(scope);
|
|
10302
|
+
const normalizedEntries = [];
|
|
10303
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
10304
|
+
const rawList = Array.isArray(entries) ? entries : entries instanceof Map ? Array.from(entries.entries()) : Object.entries(entries || {});
|
|
10305
|
+
for (const item of rawList) {
|
|
10306
|
+
if (!item) continue;
|
|
10307
|
+
const rawName = Array.isArray(item) ? item[0] : item.name;
|
|
10308
|
+
const rawPayload = Array.isArray(item) ? item[1] : item.payload ?? item.bytes;
|
|
10309
|
+
if (rawName == null) continue;
|
|
10310
|
+
const normName = normalizeOpcEntryName(String(rawName));
|
|
10311
|
+
if (seenNames.has(normName)) {
|
|
10312
|
+
throw new Error(`Duplicate normalized entry path detected: "${normName}"`);
|
|
10313
|
+
}
|
|
10314
|
+
seenNames.add(normName);
|
|
10315
|
+
let payloadBytes;
|
|
10316
|
+
if (typeof rawPayload === "string") {
|
|
10317
|
+
payloadBytes = textEncoder.encode(rawPayload);
|
|
10318
|
+
} else if (rawPayload instanceof Uint8Array) {
|
|
10319
|
+
payloadBytes = rawPayload;
|
|
10320
|
+
} else if (rawPayload && typeof rawPayload.length === "number") {
|
|
10321
|
+
payloadBytes = new Uint8Array(rawPayload);
|
|
10322
|
+
} else {
|
|
10323
|
+
payloadBytes = new Uint8Array(0);
|
|
10324
|
+
}
|
|
10325
|
+
normalizedEntries.push({
|
|
10326
|
+
name: normName,
|
|
10327
|
+
nameBytes: textEncoder.encode(normName),
|
|
10328
|
+
payloadBytes
|
|
10329
|
+
});
|
|
10330
|
+
}
|
|
10331
|
+
normalizedEntries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
10332
|
+
let totalSize = magicBytes.length + 4 + 4 + scopeBytes.length + 4;
|
|
10333
|
+
for (const e of normalizedEntries) {
|
|
10334
|
+
totalSize += 4 + e.nameBytes.length + 4 + e.payloadBytes.length;
|
|
10335
|
+
}
|
|
10336
|
+
const buffer = new Uint8Array(totalSize);
|
|
10337
|
+
const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
|
|
10338
|
+
let offset = 0;
|
|
10339
|
+
buffer.set(magicBytes, offset);
|
|
10340
|
+
offset += magicBytes.length;
|
|
10341
|
+
view.setUint32(offset, version, false);
|
|
10342
|
+
offset += 4;
|
|
10343
|
+
view.setUint32(offset, scopeBytes.length, false);
|
|
10344
|
+
offset += 4;
|
|
10345
|
+
buffer.set(scopeBytes, offset);
|
|
10346
|
+
offset += scopeBytes.length;
|
|
10347
|
+
view.setUint32(offset, normalizedEntries.length, false);
|
|
10348
|
+
offset += 4;
|
|
10349
|
+
for (const e of normalizedEntries) {
|
|
10350
|
+
view.setUint32(offset, e.nameBytes.length, false);
|
|
10351
|
+
offset += 4;
|
|
10352
|
+
buffer.set(e.nameBytes, offset);
|
|
10353
|
+
offset += e.nameBytes.length;
|
|
10354
|
+
view.setUint32(offset, e.payloadBytes.length, false);
|
|
10355
|
+
offset += 4;
|
|
10356
|
+
buffer.set(e.payloadBytes, offset);
|
|
10357
|
+
offset += e.payloadBytes.length;
|
|
10358
|
+
}
|
|
10359
|
+
return {
|
|
10360
|
+
framing: buffer,
|
|
10361
|
+
scope,
|
|
10362
|
+
version,
|
|
10363
|
+
coveredParts: normalizedEntries.map((e) => e.name)
|
|
10364
|
+
};
|
|
10365
|
+
}
|
|
10366
|
+
async function computeRevisionToken({ scope, entries = [], digestFn = null }) {
|
|
10367
|
+
const { framing, coveredParts, version } = buildRevisionTokenFraming({ scope, entries });
|
|
10368
|
+
let hashHex = "";
|
|
10369
|
+
if (typeof digestFn === "function") {
|
|
10370
|
+
hashHex = await digestFn(framing);
|
|
10371
|
+
} else if (typeof globalThis.crypto?.subtle?.digest === "function") {
|
|
10372
|
+
const hashBuf = await globalThis.crypto.subtle.digest("SHA-256", framing);
|
|
10373
|
+
const hashBytes = new Uint8Array(hashBuf);
|
|
10374
|
+
hashHex = Array.from(hashBytes).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
10375
|
+
} else {
|
|
10376
|
+
throw new Error("No crypto provider available for SHA-256 revision token computation.");
|
|
10377
|
+
}
|
|
10378
|
+
return {
|
|
10379
|
+
algorithm: "sha256",
|
|
10380
|
+
version,
|
|
10381
|
+
scope,
|
|
10382
|
+
value: hashHex,
|
|
10383
|
+
coveredParts
|
|
10384
|
+
};
|
|
10385
|
+
}
|
|
10386
|
+
function computeRevisionTokenSync({ scope, entries = [], digestFn }) {
|
|
10387
|
+
if (typeof digestFn !== "function") {
|
|
10388
|
+
throw new TypeError("computeRevisionTokenSync requires a synchronous digestFn.");
|
|
10389
|
+
}
|
|
10390
|
+
const { framing, coveredParts, version } = buildRevisionTokenFraming({ scope, entries });
|
|
10391
|
+
const hashHex = digestFn(framing);
|
|
10392
|
+
return {
|
|
10393
|
+
algorithm: "sha256",
|
|
10394
|
+
version,
|
|
10395
|
+
scope,
|
|
10396
|
+
value: hashHex,
|
|
10397
|
+
coveredParts
|
|
10398
|
+
};
|
|
10399
|
+
}
|
|
10400
|
+
function extractDocumentPartsEntries(parts) {
|
|
10401
|
+
const entries = [];
|
|
10402
|
+
if (parts?.documentXml) {
|
|
10403
|
+
entries.push({ name: "word/document.xml", payload: parts.documentXml });
|
|
10404
|
+
}
|
|
10405
|
+
if (parts?.commentsXml) {
|
|
10406
|
+
entries.push({ name: "word/comments.xml", payload: parts.commentsXml });
|
|
10407
|
+
}
|
|
10408
|
+
if (parts?.commentsExtendedXml) {
|
|
10409
|
+
entries.push({ name: "word/commentsExtended.xml", payload: parts.commentsExtendedXml });
|
|
10410
|
+
}
|
|
10411
|
+
if (parts?.numberingXml) {
|
|
10412
|
+
entries.push({ name: "word/numbering.xml", payload: parts.numberingXml });
|
|
10413
|
+
}
|
|
10414
|
+
if (parts?.stylesXml) {
|
|
10415
|
+
entries.push({ name: "word/styles.xml", payload: parts.stylesXml });
|
|
10416
|
+
}
|
|
10417
|
+
if (parts?.parts instanceof Map) {
|
|
10418
|
+
for (const [name, payload] of parts.parts.entries()) {
|
|
10419
|
+
entries.push({ name, payload });
|
|
10420
|
+
}
|
|
10421
|
+
} else if (parts?.additionalParts && typeof parts.additionalParts === "object") {
|
|
10422
|
+
for (const [name, payload] of Object.entries(parts.additionalParts)) {
|
|
10423
|
+
entries.push({ name, payload });
|
|
10424
|
+
}
|
|
10425
|
+
}
|
|
10426
|
+
return entries;
|
|
10427
|
+
}
|
|
10428
|
+
async function computeDocumentPartsRevisionToken(parts, options = {}) {
|
|
10429
|
+
const entries = extractDocumentPartsEntries(parts);
|
|
10430
|
+
return computeRevisionToken({
|
|
10431
|
+
scope: "document-parts",
|
|
10432
|
+
entries,
|
|
10433
|
+
digestFn: options.digestFn
|
|
10434
|
+
});
|
|
10435
|
+
}
|
|
10436
|
+
function validateRevisionToken(token) {
|
|
10437
|
+
if (!token || typeof token !== "object") {
|
|
10438
|
+
return { valid: false, error: { code: "INVALID_REVISION_TOKEN", message: "Revision token must be an object." } };
|
|
10439
|
+
}
|
|
10440
|
+
if (token.algorithm !== "sha256") {
|
|
10441
|
+
return { valid: false, error: { code: "INVALID_REVISION_TOKEN", message: `Unsupported revision token algorithm: "${token.algorithm}". Expected "sha256".` } };
|
|
10442
|
+
}
|
|
10443
|
+
if (token.version !== 1) {
|
|
10444
|
+
return { valid: false, error: { code: "INVALID_REVISION_TOKEN", message: `Unsupported revision token version: "${token.version}". Expected 1.` } };
|
|
10445
|
+
}
|
|
10446
|
+
if (token.scope !== "document-parts" && token.scope !== "package") {
|
|
10447
|
+
return { valid: false, error: { code: "INVALID_REVISION_TOKEN", message: `Unsupported revision token scope: "${token.scope}". Expected "document-parts" or "package".` } };
|
|
10448
|
+
}
|
|
10449
|
+
if (typeof token.value !== "string" || !/^[0-9a-f]{64}$/i.test(token.value.trim())) {
|
|
10450
|
+
return { valid: false, error: { code: "INVALID_REVISION_TOKEN", message: "Revision token value must be a 64-character hex string." } };
|
|
10451
|
+
}
|
|
10452
|
+
return { valid: true };
|
|
10453
|
+
}
|
|
10454
|
+
function areRevisionTokensEqual(a, b) {
|
|
10455
|
+
if (typeof a !== "string" || typeof b !== "string") return false;
|
|
10456
|
+
const aNorm = a.trim().toLowerCase();
|
|
10457
|
+
const bNorm = b.trim().toLowerCase();
|
|
10458
|
+
if (aNorm.length !== bNorm.length) return false;
|
|
10459
|
+
const aBuf = textEncoder.encode(aNorm);
|
|
10460
|
+
const bBuf = textEncoder.encode(bNorm);
|
|
10461
|
+
let diff = 0;
|
|
10462
|
+
for (let i = 0; i < aBuf.length; i++) {
|
|
10463
|
+
diff |= aBuf[i] ^ bBuf[i];
|
|
10464
|
+
}
|
|
10465
|
+
return diff === 0;
|
|
10466
|
+
}
|
|
10467
|
+
|
|
10468
|
+
// services/document-inspection.js
|
|
10469
|
+
var attr2 = (node, name) => node?.getAttribute?.(`w:${name}`) || node?.getAttribute?.(name) || "";
|
|
10470
|
+
var descendants = (node, name) => Array.from(node?.getElementsByTagNameNS?.(NS_W, name) || []);
|
|
10471
|
+
var first = (node, name) => descendants(node, name)[0] || null;
|
|
10472
|
+
function hasAncestor(node, localName2) {
|
|
10473
|
+
let cursor = node?.parentNode;
|
|
10474
|
+
while (cursor) {
|
|
10475
|
+
if (cursor.localName === localName2 && (!cursor.namespaceURI || cursor.namespaceURI === NS_W)) return true;
|
|
10476
|
+
cursor = cursor.parentNode;
|
|
10477
|
+
}
|
|
10478
|
+
return false;
|
|
10479
|
+
}
|
|
10480
|
+
function parseXml2(xml, partName, required = false) {
|
|
10481
|
+
if (!xml) return required ? { error: { code: "MISSING_PART", message: `Missing ${partName}.` } } : { doc: null };
|
|
10482
|
+
const parsed = parseOoxmlSafe(xml, "application/xml");
|
|
10483
|
+
if (!parsed.doc || parsed.error) return { error: { code: "PARSE_ERROR", message: `Could not parse ${partName}: ${parsed.error?.message || "invalid XML"}` } };
|
|
10484
|
+
return { doc: parsed.doc, warnings: parsed.warnings || [] };
|
|
10485
|
+
}
|
|
10486
|
+
function paragraphListProperties(paragraph) {
|
|
10487
|
+
const numPr = first(first(paragraph, "pPr"), "numPr");
|
|
10488
|
+
if (!numPr) return null;
|
|
10489
|
+
const numId = attr2(first(numPr, "numId"), "val");
|
|
10490
|
+
if (!numId || numId === "0") return null;
|
|
10491
|
+
return { numId, level: Number.parseInt(attr2(first(numPr, "ilvl"), "val") || "0", 10) || 0 };
|
|
10492
|
+
}
|
|
10493
|
+
function parseNumbering(numberingDoc) {
|
|
10494
|
+
const abstracts = /* @__PURE__ */ new Map();
|
|
10495
|
+
for (const abstract of descendants(numberingDoc, "abstractNum")) {
|
|
10496
|
+
const levels = /* @__PURE__ */ new Map();
|
|
10497
|
+
for (const level of descendants(abstract, "lvl")) {
|
|
10498
|
+
const ilvl = Number.parseInt(attr2(level, "ilvl") || "0", 10) || 0;
|
|
10499
|
+
levels.set(ilvl, {
|
|
10500
|
+
start: Number.parseInt(attr2(first(level, "start"), "val") || "1", 10) || 1,
|
|
10501
|
+
format: attr2(first(level, "numFmt"), "val") || "decimal",
|
|
10502
|
+
text: attr2(first(level, "lvlText"), "val") || `%${ilvl + 1}.`
|
|
10503
|
+
});
|
|
10504
|
+
}
|
|
10505
|
+
abstracts.set(attr2(abstract, "abstractNumId"), levels);
|
|
10506
|
+
}
|
|
10507
|
+
const nums = /* @__PURE__ */ new Map();
|
|
10508
|
+
for (const num of descendants(numberingDoc, "num")) {
|
|
10509
|
+
const abstractId = attr2(first(num, "abstractNumId"), "val");
|
|
10510
|
+
const levels = new Map(abstracts.get(abstractId) || []);
|
|
10511
|
+
for (const override of descendants(num, "lvlOverride")) {
|
|
10512
|
+
const ilvl = Number.parseInt(attr2(override, "ilvl") || "0", 10) || 0;
|
|
10513
|
+
const embedded = first(override, "lvl");
|
|
10514
|
+
const base = { ...levels.get(ilvl) || { start: 1, format: "decimal", text: `%${ilvl + 1}.` } };
|
|
10515
|
+
if (embedded) {
|
|
10516
|
+
base.start = Number.parseInt(attr2(first(embedded, "start"), "val") || String(base.start), 10) || base.start;
|
|
10517
|
+
base.format = attr2(first(embedded, "numFmt"), "val") || base.format;
|
|
10518
|
+
base.text = attr2(first(embedded, "lvlText"), "val") || base.text;
|
|
10519
|
+
}
|
|
10520
|
+
const startOverride = first(override, "startOverride");
|
|
10521
|
+
if (startOverride) base.start = Number.parseInt(attr2(startOverride, "val") || String(base.start), 10) || base.start;
|
|
10522
|
+
levels.set(ilvl, base);
|
|
10523
|
+
}
|
|
10524
|
+
nums.set(attr2(num, "numId"), levels);
|
|
10525
|
+
}
|
|
10526
|
+
return nums;
|
|
10527
|
+
}
|
|
10528
|
+
function alpha(value, upper) {
|
|
10529
|
+
let n = Math.max(1, value);
|
|
10530
|
+
let out = "";
|
|
10531
|
+
while (n > 0) {
|
|
10532
|
+
n -= 1;
|
|
10533
|
+
out = String.fromCharCode(97 + n % 26) + out;
|
|
10534
|
+
n = Math.floor(n / 26);
|
|
10535
|
+
}
|
|
10536
|
+
return upper ? out.toUpperCase() : out;
|
|
10537
|
+
}
|
|
10538
|
+
function roman(value) {
|
|
10539
|
+
const pairs = [[1e3, "M"], [900, "CM"], [500, "D"], [400, "CD"], [100, "C"], [90, "XC"], [50, "L"], [40, "XL"], [10, "X"], [9, "IX"], [5, "V"], [4, "IV"], [1, "I"]];
|
|
10540
|
+
let n = value;
|
|
10541
|
+
let out = "";
|
|
10542
|
+
for (const [amount, glyph] of pairs) while (n >= amount) {
|
|
10543
|
+
out += glyph;
|
|
10544
|
+
n -= amount;
|
|
10545
|
+
}
|
|
10546
|
+
return out;
|
|
10547
|
+
}
|
|
10548
|
+
function formatCounter(value, format) {
|
|
10549
|
+
if (format === "lowerLetter") return alpha(value, false);
|
|
10550
|
+
if (format === "upperLetter") return alpha(value, true);
|
|
10551
|
+
if (format === "lowerRoman") return roman(value).toLowerCase();
|
|
10552
|
+
if (format === "upperRoman") return roman(value);
|
|
10553
|
+
return String(value);
|
|
10554
|
+
}
|
|
10555
|
+
function createNumberingResolver(numberingDoc) {
|
|
10556
|
+
const nums = numberingDoc ? parseNumbering(numberingDoc) : /* @__PURE__ */ new Map();
|
|
10557
|
+
const counters = /* @__PURE__ */ new Map();
|
|
10558
|
+
return (list) => {
|
|
10559
|
+
if (!list?.numId) return null;
|
|
10560
|
+
const levels = nums.get(String(list.numId));
|
|
10561
|
+
if (!levels) return { ...list, label: null, format: null };
|
|
10562
|
+
const state = counters.get(list.numId) || [];
|
|
10563
|
+
const definition = levels.get(list.level) || { start: 1, format: "decimal", text: `%${list.level + 1}.` };
|
|
10564
|
+
state[list.level] = state[list.level] == null ? definition.start : state[list.level] + 1;
|
|
10565
|
+
state.length = list.level + 1;
|
|
10566
|
+
counters.set(list.numId, state);
|
|
10567
|
+
const label = definition.text.replace(/%([1-9])/g, (_, raw) => {
|
|
10568
|
+
const level = Number(raw) - 1;
|
|
10569
|
+
const levelDef = levels.get(level) || definition;
|
|
10570
|
+
return formatCounter(state[level] ?? levelDef.start, levelDef.format);
|
|
10571
|
+
});
|
|
10572
|
+
return { ...list, label, format: definition.format };
|
|
10573
|
+
};
|
|
10574
|
+
}
|
|
10575
|
+
function headingLevel(paragraph) {
|
|
10576
|
+
const pPr = first(paragraph, "pPr");
|
|
10577
|
+
const style = attr2(first(pPr, "pStyle"), "val");
|
|
10578
|
+
const match = style.match(/^heading\s*([1-9])$/i);
|
|
10579
|
+
if (match) return Math.min(Number(match[1]), 6);
|
|
10580
|
+
const outline = Number.parseInt(attr2(first(pPr, "outlineLvl"), "val"), 10);
|
|
10581
|
+
return Number.isInteger(outline) ? Math.min(outline + 1, 6) : null;
|
|
10582
|
+
}
|
|
10583
|
+
function structuralContext(paragraph, text) {
|
|
10584
|
+
const references = [];
|
|
10585
|
+
for (const [name, type] of [["footnoteReference", "footnote"], ["endnoteReference", "endnote"], ["commentReference", "comment"]]) {
|
|
10586
|
+
for (const node of descendants(paragraph, name)) references.push({ type, id: attr2(node, "id") || null });
|
|
10587
|
+
}
|
|
10588
|
+
let cell = paragraph.parentNode;
|
|
10589
|
+
while (cell && cell.localName !== "tc") cell = cell.parentNode;
|
|
10590
|
+
let row = cell?.parentNode;
|
|
10591
|
+
while (row && row.localName !== "tr") row = row.parentNode;
|
|
10592
|
+
let table = row?.parentNode;
|
|
10593
|
+
while (table && table.localName !== "tbl") table = table.parentNode;
|
|
10594
|
+
const all = paragraph.ownerDocument;
|
|
10595
|
+
return {
|
|
10596
|
+
references,
|
|
10597
|
+
table: table ? {
|
|
10598
|
+
tableIndex: Array.from(all.getElementsByTagNameNS(NS_W, "tbl")).indexOf(table) + 1,
|
|
10599
|
+
rowIndex: Array.from(table.getElementsByTagNameNS(NS_W, "tr")).indexOf(row) + 1,
|
|
10600
|
+
cellIndex: Array.from(row.getElementsByTagNameNS(NS_W, "tc")).indexOf(cell) + 1
|
|
10601
|
+
} : null,
|
|
10602
|
+
empty: text.length === 0
|
|
10603
|
+
};
|
|
10604
|
+
}
|
|
10605
|
+
function revisionAuthors(paragraph) {
|
|
10606
|
+
const authors = /* @__PURE__ */ new Set();
|
|
10607
|
+
for (const name of ["ins", "del", "moveFrom", "moveTo", "rPrChange", "pPrChange"]) {
|
|
10608
|
+
for (const node of descendants(paragraph, name)) if (attr2(node, "author")) authors.add(attr2(node, "author"));
|
|
10609
|
+
}
|
|
10610
|
+
return [...authors].sort();
|
|
10611
|
+
}
|
|
10612
|
+
function readCommentDefinitions(commentsDoc) {
|
|
10613
|
+
const result = /* @__PURE__ */ new Map();
|
|
10614
|
+
for (const comment of descendants(commentsDoc, "comment")) {
|
|
10615
|
+
const paragraphs = descendants(comment, "p");
|
|
10616
|
+
result.set(attr2(comment, "id"), {
|
|
10617
|
+
id: attr2(comment, "id"),
|
|
10618
|
+
author: attr2(comment, "author") || null,
|
|
10619
|
+
date: attr2(comment, "date") || null,
|
|
10620
|
+
text: paragraphs.map((p) => extractCanonicalParagraphText(p)).join("\n"),
|
|
10621
|
+
paraId: paragraphs[0]?.getAttribute?.("w14:paraId") || paragraphs[0]?.getAttribute?.("paraId") || null
|
|
10622
|
+
});
|
|
10623
|
+
}
|
|
10624
|
+
return result;
|
|
10625
|
+
}
|
|
10626
|
+
function attachCommentThreadMetadata(comments, commentsExtendedDoc) {
|
|
10627
|
+
if (!commentsExtendedDoc) return;
|
|
10628
|
+
const byParaId = new Map([...comments.values()].filter((c) => c.paraId).map((c) => [c.paraId.toUpperCase(), c]));
|
|
10629
|
+
for (const entry of Array.from(commentsExtendedDoc.getElementsByTagNameNS("*", "commentEx"))) {
|
|
10630
|
+
const paraId = entry.getAttribute("w15:paraId") || entry.getAttribute("paraId") || "";
|
|
10631
|
+
const parentParaId = entry.getAttribute("w15:paraIdParent") || entry.getAttribute("paraIdParent") || "";
|
|
10632
|
+
const comment = byParaId.get(paraId.toUpperCase());
|
|
10633
|
+
if (!comment) continue;
|
|
10634
|
+
comment.done = (entry.getAttribute("w15:done") || entry.getAttribute("done")) === "1";
|
|
10635
|
+
if (parentParaId) {
|
|
10636
|
+
comment.parentParaId = parentParaId;
|
|
10637
|
+
comment.parentCommentId = byParaId.get(parentParaId.toUpperCase())?.id || null;
|
|
10638
|
+
}
|
|
10639
|
+
}
|
|
10640
|
+
}
|
|
10641
|
+
function collectDocumentCommentAnchors(paragraphNodes, revisionView) {
|
|
10642
|
+
const active = /* @__PURE__ */ new Map();
|
|
10643
|
+
const anchors = /* @__PURE__ */ new Map();
|
|
10644
|
+
let paragraphBoundary = null;
|
|
10645
|
+
const append = (value) => {
|
|
10646
|
+
for (const item of active.values()) item.text += value;
|
|
10647
|
+
};
|
|
10648
|
+
const visit = (node) => {
|
|
10649
|
+
for (const child of Array.from(node?.childNodes || [])) {
|
|
10650
|
+
if (child?.nodeType !== 1) continue;
|
|
10651
|
+
const name = child.localName;
|
|
10652
|
+
if (revisionView === "accepted" && (name === "del" || name === "moveFrom") || revisionView === "rejected" && (name === "ins" || name === "moveTo")) continue;
|
|
10653
|
+
if (name === "commentRangeStart") active.set(attr2(child, "id"), { text: "" });
|
|
10654
|
+
else if (name === "commentRangeEnd") {
|
|
10655
|
+
const id = attr2(child, "id");
|
|
10656
|
+
if (active.has(id)) {
|
|
10657
|
+
anchors.set(id, active.get(id).text);
|
|
10658
|
+
active.delete(id);
|
|
10659
|
+
}
|
|
10660
|
+
} else if (name === "r") append(readCanonicalRunText(child, { revisionView, boundary: paragraphBoundary }));
|
|
10661
|
+
else visit(child);
|
|
10662
|
+
}
|
|
10663
|
+
};
|
|
10664
|
+
paragraphNodes.forEach((paragraph, index) => {
|
|
10665
|
+
paragraphBoundary = paragraph;
|
|
10666
|
+
visit(paragraph);
|
|
10667
|
+
if (index < paragraphNodes.length - 1 && active.size) append("\n");
|
|
10668
|
+
});
|
|
10669
|
+
return anchors;
|
|
10670
|
+
}
|
|
10671
|
+
function inspectDocumentParts(parts, options = {}) {
|
|
10672
|
+
const documentPart = parseXml2(parts?.documentXml, "word/document.xml", true);
|
|
10673
|
+
if (documentPart.error) return { status: "error", error: documentPart.error, paragraphs: [], comments: [], warnings: [] };
|
|
10674
|
+
const commentsPart = parseXml2(parts?.commentsXml, "word/comments.xml");
|
|
10675
|
+
const commentsExtendedPart = parseXml2(parts?.commentsExtendedXml, "word/commentsExtended.xml");
|
|
10676
|
+
const numberingPart = parseXml2(parts?.numberingXml, "word/numbering.xml");
|
|
10677
|
+
const warnings = [...documentPart.warnings || []];
|
|
10678
|
+
if (commentsPart.error) warnings.push(commentsPart.error.message);
|
|
10679
|
+
if (commentsExtendedPart.error) warnings.push(commentsExtendedPart.error.message);
|
|
10680
|
+
if (numberingPart.error) warnings.push(numberingPart.error.message);
|
|
10681
|
+
const comments = readCommentDefinitions(commentsPart.doc);
|
|
10682
|
+
attachCommentThreadMetadata(comments, commentsExtendedPart.doc);
|
|
10683
|
+
const resolveNumbering = createNumberingResolver(numberingPart.doc);
|
|
10684
|
+
let nearestHeading = null;
|
|
10685
|
+
const paragraphNodes = getDocumentParagraphNodes(documentPart.doc);
|
|
10686
|
+
const commentAnchors = collectDocumentCommentAnchors(paragraphNodes, options.revisionView || "accepted");
|
|
10687
|
+
let paragraphs = paragraphNodes.map((paragraph, zeroIndex) => {
|
|
10688
|
+
const text = extractCanonicalParagraphText(paragraph, { revisionView: options.revisionView || "accepted" });
|
|
10689
|
+
const level = headingLevel(paragraph);
|
|
10690
|
+
if (level) nearestHeading = { level, text };
|
|
10691
|
+
const ids = [...new Set([...descendants(paragraph, "commentRangeStart"), ...descendants(paragraph, "commentReference")].map((node) => attr2(node, "id")).filter(Boolean))];
|
|
10692
|
+
const authors = revisionAuthors(paragraph);
|
|
10693
|
+
const list = resolveNumbering(paragraphListProperties(paragraph));
|
|
10694
|
+
const styleId = attr2(first(first(paragraph, "pPr"), "pStyle"), "val") || null;
|
|
10695
|
+
const structure = structuralContext(paragraph, text);
|
|
10696
|
+
const index = zeroIndex + 1;
|
|
10697
|
+
const provision = list?.label && list.format !== "bullet" ? list.label : null;
|
|
10698
|
+
const headingText = nearestHeading?.text || null;
|
|
10699
|
+
const humanReference = [provision, headingText, text.slice(0, options.excerptLength || 120)].filter(Boolean).join(" \u2014 ");
|
|
10700
|
+
const segments = extractParagraphRevisionSegments(paragraph);
|
|
10701
|
+
return {
|
|
10702
|
+
index,
|
|
10703
|
+
ref: `P${index}`,
|
|
10704
|
+
paragraphId: getParagraphId2(paragraph),
|
|
10705
|
+
fingerprint: createParagraphFingerprint(paragraph),
|
|
10706
|
+
text,
|
|
10707
|
+
exactText: text,
|
|
10708
|
+
excerpt: text.slice(0, options.excerptLength || 120),
|
|
10709
|
+
humanReference,
|
|
10710
|
+
inTable: hasAncestor(paragraph, "tc"),
|
|
10711
|
+
table: structure.table,
|
|
10712
|
+
styleId,
|
|
10713
|
+
headingLevel: level,
|
|
10714
|
+
nearestHeading,
|
|
10715
|
+
list,
|
|
10716
|
+
structuralReferences: structure.references,
|
|
10717
|
+
hasRevisions: authors.length > 0,
|
|
10718
|
+
revisionAuthors: authors,
|
|
10719
|
+
commentIds: ids,
|
|
10720
|
+
segments
|
|
10721
|
+
};
|
|
10722
|
+
});
|
|
10723
|
+
for (const paragraph of paragraphs) for (const id of paragraph.commentIds) {
|
|
10724
|
+
const definition = comments.get(id) || { id, author: null, date: null, text: "" };
|
|
10725
|
+
definition.paragraphIndex ?? (definition.paragraphIndex = paragraph.index);
|
|
10726
|
+
definition.targetRef ?? (definition.targetRef = paragraph.ref);
|
|
10727
|
+
definition.anchoredText ?? (definition.anchoredText = commentAnchors.get(id) || paragraph.text);
|
|
10728
|
+
comments.set(id, definition);
|
|
10729
|
+
}
|
|
10730
|
+
if (options.revisedOnly) paragraphs = paragraphs.filter((item) => item.hasRevisions);
|
|
10731
|
+
if (options.inTable != null) paragraphs = paragraphs.filter((item) => item.inTable === !!options.inTable);
|
|
10732
|
+
if (options.skipEmpty) paragraphs = paragraphs.filter((item) => item.text.length > 0);
|
|
10733
|
+
if (options.search) {
|
|
10734
|
+
const needle = String(options.search).toLowerCase();
|
|
10735
|
+
paragraphs = paragraphs.filter((item) => item.text.toLowerCase().includes(needle));
|
|
10736
|
+
}
|
|
10737
|
+
if (Array.isArray(options.indexes)) {
|
|
10738
|
+
const indexes = new Set(options.indexes);
|
|
10739
|
+
paragraphs = paragraphs.filter((item) => indexes.has(item.index));
|
|
10740
|
+
}
|
|
10741
|
+
if (options.range) {
|
|
10742
|
+
const start = Number(options.range.start ?? options.range[0]);
|
|
10743
|
+
const end = Number(options.range.end ?? options.range[1]);
|
|
10744
|
+
paragraphs = paragraphs.filter((item) => item.index >= start && item.index <= end);
|
|
10745
|
+
}
|
|
10746
|
+
const allRevisionAuthors = [...new Set(paragraphs.flatMap((item) => item.revisionAuthors))].sort();
|
|
10747
|
+
const coveredEntries = extractDocumentPartsEntries(parts);
|
|
10748
|
+
const coveredParts = coveredEntries.map((e) => e.name).sort();
|
|
10749
|
+
let revisionToken = null;
|
|
10750
|
+
if (typeof options.digestFn === "function") {
|
|
10751
|
+
revisionToken = computeRevisionTokenSync({
|
|
10752
|
+
scope: "document-parts",
|
|
10753
|
+
entries: coveredEntries,
|
|
10754
|
+
digestFn: options.digestFn
|
|
10755
|
+
});
|
|
10756
|
+
}
|
|
10757
|
+
return {
|
|
10758
|
+
status: "ok",
|
|
10759
|
+
revisionToken,
|
|
10760
|
+
coveredParts,
|
|
10761
|
+
paragraphs,
|
|
10762
|
+
comments: [...comments.values()],
|
|
10763
|
+
revisionAuthors: allRevisionAuthors,
|
|
10764
|
+
commentAuthors: [...new Set([...comments.values()].map((item) => item.author).filter(Boolean))].sort(),
|
|
10765
|
+
counts: { paragraphs: paragraphs.length, comments: comments.size, revisedParagraphs: paragraphs.filter((item) => item.hasRevisions).length },
|
|
10766
|
+
warnings
|
|
10767
|
+
};
|
|
10768
|
+
}
|
|
10769
|
+
|
|
9003
10770
|
// services/comment-builders.js
|
|
9004
|
-
|
|
10771
|
+
var NS_W14 = "http://schemas.microsoft.com/office/word/2010/wordml";
|
|
10772
|
+
var NS_W15 = "http://schemas.microsoft.com/office/word/2012/wordml";
|
|
10773
|
+
function createCommentParaId(commentId) {
|
|
10774
|
+
const numeric = Number.parseInt(String(commentId), 10);
|
|
10775
|
+
const value = Number.isFinite(numeric) ? 1879048192 + (numeric >>> 0) >>> 0 : 1879048192;
|
|
10776
|
+
return value.toString(16).toUpperCase().padStart(8, "0").slice(-8);
|
|
10777
|
+
}
|
|
10778
|
+
function buildCommentElement(commentId, author, content, date, paraId = createCommentParaId(commentId)) {
|
|
9005
10779
|
const initials = author.split(" ").map((word) => word[0]).join("").toUpperCase() || "AI";
|
|
9006
10780
|
const escapedContent = escapeXml(content);
|
|
9007
10781
|
const escapedAuthor = escapeXml(author);
|
|
9008
10782
|
return `<w:comment w:id="${commentId}" w:author="${escapedAuthor}" w:date="${date}" w:initials="${initials}">
|
|
9009
|
-
<w:p>
|
|
10783
|
+
<w:p w14:paraId="${escapeXml(paraId)}" xmlns:w14="${NS_W14}">
|
|
9010
10784
|
<w:r><w:t>${escapedContent}</w:t></w:r>
|
|
9011
10785
|
</w:p>
|
|
9012
10786
|
</w:comment>`;
|
|
@@ -9016,60 +10790,158 @@ function buildCommentsPartXml(comments) {
|
|
|
9016
10790
|
return `<w:comments xmlns:w="${NS_W}"></w:comments>`;
|
|
9017
10791
|
}
|
|
9018
10792
|
const commentElements = comments.map(
|
|
9019
|
-
(comment) => buildCommentElement(comment.id, comment.author, comment.content, comment.date)
|
|
10793
|
+
(comment) => buildCommentElement(comment.id, comment.author, comment.content, comment.date, comment.paraId)
|
|
9020
10794
|
).join("\n ");
|
|
9021
10795
|
return `<w:comments xmlns:w="${NS_W}">
|
|
9022
10796
|
${commentElements}
|
|
9023
10797
|
</w:comments>`;
|
|
9024
10798
|
}
|
|
10799
|
+
function buildCommentsExtendedPartXml(entries) {
|
|
10800
|
+
const body = (entries || []).map((entry) => {
|
|
10801
|
+
const parent = entry.paraIdParent ? ` w15:paraIdParent="${escapeXml(entry.paraIdParent)}"` : "";
|
|
10802
|
+
return `<w15:commentEx w15:paraId="${escapeXml(entry.paraId)}"${parent} w15:done="${entry.done ? "1" : "0"}"/>`;
|
|
10803
|
+
}).join("");
|
|
10804
|
+
return `<w15:commentsEx xmlns:w15="${NS_W15}">${body}</w15:commentsEx>`;
|
|
10805
|
+
}
|
|
10806
|
+
|
|
10807
|
+
// core/revision-cloning.js
|
|
10808
|
+
function refreshRunPropertyChangeIds(root, allocator = null) {
|
|
10809
|
+
if (!root) return root;
|
|
10810
|
+
const xmlDoc = root.nodeType === 9 ? root : root.ownerDocument;
|
|
10811
|
+
const resolvedAllocator = allocator instanceof RevisionIdAllocator ? allocator : getRevisionIdAllocatorForDocument(xmlDoc) || createRevisionIdAllocator(xmlDoc);
|
|
10812
|
+
const candidates = [root, ...Array.from(root.getElementsByTagName?.("*") || [])];
|
|
10813
|
+
for (const node of candidates) {
|
|
10814
|
+
if (!isWordElement(node, "rPrChange")) continue;
|
|
10815
|
+
const nextId = String(resolvedAllocator.next());
|
|
10816
|
+
if (typeof node.setAttributeNS === "function") {
|
|
10817
|
+
node.setAttributeNS(NS_W, "w:id", nextId);
|
|
10818
|
+
} else {
|
|
10819
|
+
node.setAttribute("w:id", nextId);
|
|
10820
|
+
}
|
|
10821
|
+
}
|
|
10822
|
+
return root;
|
|
10823
|
+
}
|
|
9025
10824
|
|
|
9026
10825
|
// services/comment-locator.js
|
|
9027
|
-
function createParagraphTextIndex(paragraph) {
|
|
10826
|
+
function createParagraphTextIndex(paragraph, options = {}) {
|
|
10827
|
+
const revisionView = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
|
|
9028
10828
|
const runs = getElementsByTag(paragraph, "w:r");
|
|
9029
10829
|
const runOffsets = [];
|
|
9030
10830
|
let fullText = "";
|
|
9031
10831
|
for (const run of runs) {
|
|
10832
|
+
if (!isNodeVisibleInRevisionView(run, paragraph, revisionView)) continue;
|
|
9032
10833
|
const start = fullText.length;
|
|
9033
|
-
const
|
|
9034
|
-
|
|
9035
|
-
fullText += textNode.textContent || "";
|
|
9036
|
-
}
|
|
10834
|
+
const text = readCanonicalRunText(run, { revisionView, boundary: paragraph });
|
|
10835
|
+
fullText += text;
|
|
9037
10836
|
runOffsets.push({ run, start, end: fullText.length });
|
|
9038
10837
|
}
|
|
9039
10838
|
return { fullText, runOffsets };
|
|
9040
10839
|
}
|
|
9041
|
-
function
|
|
9042
|
-
|
|
9043
|
-
|
|
9044
|
-
|
|
10840
|
+
function candidateOffsets(haystack, needle) {
|
|
10841
|
+
if (!needle) return [];
|
|
10842
|
+
const candidates = [];
|
|
10843
|
+
let offset = haystack.indexOf(needle);
|
|
10844
|
+
while (offset !== -1) {
|
|
10845
|
+
candidates.push({ start: offset, end: offset + needle.length });
|
|
10846
|
+
offset = haystack.indexOf(needle, offset + 1);
|
|
9045
10847
|
}
|
|
9046
|
-
|
|
10848
|
+
return candidates;
|
|
10849
|
+
}
|
|
10850
|
+
function spaceEquivalentText(value) {
|
|
10851
|
+
return String(value).replace(/[ \u00a0]/g, " ");
|
|
10852
|
+
}
|
|
10853
|
+
function anchorError(code, message, candidates = []) {
|
|
10854
|
+
return {
|
|
10855
|
+
code,
|
|
10856
|
+
message,
|
|
10857
|
+
...candidates.length > 0 ? { candidates } : {}
|
|
10858
|
+
};
|
|
10859
|
+
}
|
|
10860
|
+
function attachRunOffsets(paragraphIndex, candidate, resolvedBy) {
|
|
9047
10861
|
let startRun = null;
|
|
9048
10862
|
let endRun = null;
|
|
9049
10863
|
let startOffset = 0;
|
|
9050
10864
|
let endOffset = 0;
|
|
9051
10865
|
for (const { run, start, end } of paragraphIndex.runOffsets) {
|
|
9052
|
-
if (
|
|
10866
|
+
if (candidate.start >= start && candidate.start < end) {
|
|
9053
10867
|
startRun = run;
|
|
9054
|
-
startOffset =
|
|
10868
|
+
startOffset = candidate.start - start;
|
|
9055
10869
|
}
|
|
9056
|
-
if (
|
|
10870
|
+
if (candidate.end > start && candidate.end <= end) {
|
|
9057
10871
|
endRun = run;
|
|
9058
|
-
endOffset =
|
|
10872
|
+
endOffset = candidate.end - start;
|
|
10873
|
+
}
|
|
10874
|
+
}
|
|
10875
|
+
if (!startRun || !endRun) return null;
|
|
10876
|
+
return { found: true, resolvedBy, ...candidate, startRun, startOffset, endRun, endOffset };
|
|
10877
|
+
}
|
|
10878
|
+
function findAncestorTag(node, tagNames, boundary = null) {
|
|
10879
|
+
let current = node?.parentNode;
|
|
10880
|
+
while (current && current.nodeType === 1 && current !== boundary && (current.localName || current.nodeName.replace(/^.*:/, "")) !== "p") {
|
|
10881
|
+
const local = current.localName || current.nodeName.replace(/^.*:/, "");
|
|
10882
|
+
if (tagNames.includes(local)) return { node: current, tag: local };
|
|
10883
|
+
current = current.parentNode;
|
|
10884
|
+
}
|
|
10885
|
+
return null;
|
|
10886
|
+
}
|
|
10887
|
+
function validateAnchorLocation(paragraphIndex, location) {
|
|
10888
|
+
if (!location || !location.found) return location;
|
|
10889
|
+
const intersectingRuns = paragraphIndex.runOffsets.filter((entry) => entry.end > location.start && entry.start < location.end).map((entry) => entry.run);
|
|
10890
|
+
for (const run of intersectingRuns) {
|
|
10891
|
+
if (findAncestorTag(run, ["del"])) {
|
|
10892
|
+
return {
|
|
10893
|
+
found: false,
|
|
10894
|
+
error: anchorError("UNSAFE_REVISION_NESTING", "Refusing to attach comment to pending deletion.", [location])
|
|
10895
|
+
};
|
|
10896
|
+
}
|
|
10897
|
+
if (findAncestorTag(run, ["moveFrom", "moveTo"])) {
|
|
10898
|
+
return {
|
|
10899
|
+
found: false,
|
|
10900
|
+
error: anchorError("UNSAFE_REVISION_NESTING", "Refusing to comment on move revision until move lifecycle is designed.", [location])
|
|
10901
|
+
};
|
|
9059
10902
|
}
|
|
9060
10903
|
}
|
|
10904
|
+
return location;
|
|
10905
|
+
}
|
|
10906
|
+
function resolveTextInParagraphIndex(paragraphIndex, searchText) {
|
|
10907
|
+
const needle = String(searchText ?? "");
|
|
10908
|
+
const exactCandidates = candidateOffsets(paragraphIndex.fullText, needle);
|
|
10909
|
+
if (exactCandidates.length > 1) {
|
|
10910
|
+
return {
|
|
10911
|
+
found: false,
|
|
10912
|
+
error: anchorError("AMBIGUOUS_ANCHOR", `Anchor text matched ${exactCandidates.length} locations in the target paragraph.`, exactCandidates)
|
|
10913
|
+
};
|
|
10914
|
+
}
|
|
10915
|
+
if (exactCandidates.length === 1) {
|
|
10916
|
+
const resolved = attachRunOffsets(paragraphIndex, exactCandidates[0], "exact_anchor");
|
|
10917
|
+
if (resolved) return validateAnchorLocation(paragraphIndex, resolved);
|
|
10918
|
+
}
|
|
10919
|
+
const normalizedNeedle = spaceEquivalentText(needle);
|
|
10920
|
+
const normalizedParagraph = spaceEquivalentText(paragraphIndex.fullText);
|
|
10921
|
+
const equivalentCandidates = candidateOffsets(normalizedParagraph, normalizedNeedle);
|
|
10922
|
+
if (equivalentCandidates.length > 1) {
|
|
10923
|
+
return {
|
|
10924
|
+
found: false,
|
|
10925
|
+
error: anchorError("AMBIGUOUS_ANCHOR", `Space-equivalent anchor text matched ${equivalentCandidates.length} locations in the target paragraph.`, equivalentCandidates)
|
|
10926
|
+
};
|
|
10927
|
+
}
|
|
10928
|
+
if (equivalentCandidates.length === 1) {
|
|
10929
|
+
const resolved = attachRunOffsets(paragraphIndex, equivalentCandidates[0], "space_equivalent_anchor");
|
|
10930
|
+
if (resolved) return validateAnchorLocation(paragraphIndex, resolved);
|
|
10931
|
+
}
|
|
9061
10932
|
return {
|
|
9062
|
-
found:
|
|
9063
|
-
|
|
9064
|
-
startOffset,
|
|
9065
|
-
endRun,
|
|
9066
|
-
endOffset
|
|
10933
|
+
found: false,
|
|
10934
|
+
error: anchorError("ANCHOR_NOT_FOUND", `Could not find anchor text in the target paragraph: "${needle}".`)
|
|
9067
10935
|
};
|
|
9068
10936
|
}
|
|
9069
|
-
function cloneRunWithText(xmlDoc, rPr, newText) {
|
|
10937
|
+
function cloneRunWithText(xmlDoc, rPr, newText, revisionIdAllocator, preserveRevisionIds = false) {
|
|
9070
10938
|
const newRun = createWordElement(xmlDoc, "w:r");
|
|
9071
10939
|
if (rPr) {
|
|
9072
|
-
|
|
10940
|
+
const clonedRPr = rPr.cloneNode(true);
|
|
10941
|
+
if (!preserveRevisionIds) {
|
|
10942
|
+
refreshRunPropertyChangeIds(clonedRPr, revisionIdAllocator);
|
|
10943
|
+
}
|
|
10944
|
+
newRun.appendChild(clonedRPr);
|
|
9073
10945
|
}
|
|
9074
10946
|
const newTextNode = createWordElement(xmlDoc, "w:t");
|
|
9075
10947
|
newTextNode.setAttribute("xml:space", "preserve");
|
|
@@ -9077,9 +10949,9 @@ function cloneRunWithText(xmlDoc, rPr, newText) {
|
|
|
9077
10949
|
newRun.appendChild(newTextNode);
|
|
9078
10950
|
return newRun;
|
|
9079
10951
|
}
|
|
9080
|
-
function injectMarkersIntoParagraph(xmlDoc, paragraph, textToFind, commentId, paragraphIndex = null) {
|
|
10952
|
+
function injectMarkersIntoParagraph(xmlDoc, paragraph, textToFind, commentId, paragraphIndex = null, revisionIdAllocator = null, resolvedLocation = null) {
|
|
9081
10953
|
const activeIndex = paragraphIndex || createParagraphTextIndex(paragraph);
|
|
9082
|
-
const location =
|
|
10954
|
+
const location = resolvedLocation || resolveTextInParagraphIndex(activeIndex, textToFind);
|
|
9083
10955
|
if (!location.found || !location.startRun) {
|
|
9084
10956
|
return false;
|
|
9085
10957
|
}
|
|
@@ -9112,7 +10984,8 @@ function injectMarkersIntoParagraph(xmlDoc, paragraph, textToFind, commentId, pa
|
|
|
9112
10984
|
const rPr = getFirstElementByTag(run, "w:rPr");
|
|
9113
10985
|
const parent = run.parentNode;
|
|
9114
10986
|
if (beforeText) {
|
|
9115
|
-
parent.insertBefore(cloneRunWithText(xmlDoc, rPr, beforeText), run);
|
|
10987
|
+
parent.insertBefore(cloneRunWithText(xmlDoc, rPr, beforeText, revisionIdAllocator, true), run);
|
|
10988
|
+
refreshRunPropertyChangeIds(rPr, revisionIdAllocator);
|
|
9116
10989
|
}
|
|
9117
10990
|
parent.insertBefore(startMarker, run);
|
|
9118
10991
|
textNode.textContent = highlightedText;
|
|
@@ -9123,7 +10996,7 @@ function injectMarkersIntoParagraph(xmlDoc, paragraph, textToFind, commentId, pa
|
|
|
9123
10996
|
}
|
|
9124
10997
|
parent.insertBefore(referenceRun, endMarker.nextSibling || null);
|
|
9125
10998
|
if (afterText) {
|
|
9126
|
-
parent.insertBefore(cloneRunWithText(xmlDoc, rPr, afterText), referenceRun.nextSibling || null);
|
|
10999
|
+
parent.insertBefore(cloneRunWithText(xmlDoc, rPr, afterText, revisionIdAllocator), referenceRun.nextSibling || null);
|
|
9127
11000
|
}
|
|
9128
11001
|
return true;
|
|
9129
11002
|
}
|
|
@@ -9134,7 +11007,11 @@ function injectMarkersIntoParagraph(xmlDoc, paragraph, textToFind, commentId, pa
|
|
|
9134
11007
|
const highlightedStart = fullText.substring(location.startOffset);
|
|
9135
11008
|
if (beforeText) {
|
|
9136
11009
|
const rPr = getFirstElementByTag(location.startRun, "w:rPr");
|
|
9137
|
-
location.startRun.parentNode.insertBefore(
|
|
11010
|
+
location.startRun.parentNode.insertBefore(
|
|
11011
|
+
cloneRunWithText(xmlDoc, rPr, beforeText, revisionIdAllocator, true),
|
|
11012
|
+
location.startRun
|
|
11013
|
+
);
|
|
11014
|
+
refreshRunPropertyChangeIds(rPr, revisionIdAllocator);
|
|
9138
11015
|
}
|
|
9139
11016
|
startTextNode.textContent = highlightedStart;
|
|
9140
11017
|
}
|
|
@@ -9149,9 +11026,12 @@ function injectMarkersIntoParagraph(xmlDoc, paragraph, textToFind, commentId, pa
|
|
|
9149
11026
|
if (afterText) {
|
|
9150
11027
|
const rPr = getFirstElementByTag(endRun, "w:rPr");
|
|
9151
11028
|
if (endRun.nextSibling) {
|
|
9152
|
-
endRun.parentNode.insertBefore(
|
|
11029
|
+
endRun.parentNode.insertBefore(
|
|
11030
|
+
cloneRunWithText(xmlDoc, rPr, afterText, revisionIdAllocator),
|
|
11031
|
+
endRun.nextSibling
|
|
11032
|
+
);
|
|
9153
11033
|
} else {
|
|
9154
|
-
endRun.parentNode.appendChild(cloneRunWithText(xmlDoc, rPr, afterText));
|
|
11034
|
+
endRun.parentNode.appendChild(cloneRunWithText(xmlDoc, rPr, afterText, revisionIdAllocator));
|
|
9155
11035
|
}
|
|
9156
11036
|
}
|
|
9157
11037
|
}
|
|
@@ -9252,6 +11132,8 @@ function injectCommentsIntoOoxml(oxml, comments, options = {}) {
|
|
|
9252
11132
|
const date = getRevisionTimestamp();
|
|
9253
11133
|
const warnings = [];
|
|
9254
11134
|
const placedComments = [];
|
|
11135
|
+
const resolvedAnchors = [];
|
|
11136
|
+
const errors = [];
|
|
9255
11137
|
if (!comments || comments.length === 0) {
|
|
9256
11138
|
return {
|
|
9257
11139
|
oxml,
|
|
@@ -9278,19 +11160,22 @@ function injectCommentsIntoOoxml(oxml, comments, options = {}) {
|
|
|
9278
11160
|
};
|
|
9279
11161
|
}
|
|
9280
11162
|
const xmlDoc = parseResult.xmlDoc;
|
|
11163
|
+
const revisionIdAllocator = createRevisionIdAllocator(xmlDoc);
|
|
9281
11164
|
const paragraphs = getElementsByTag(xmlDoc, "w:p");
|
|
9282
11165
|
log(`[CommentEngine] Found ${paragraphs.length} paragraphs, processing ${comments.length} comment requests`);
|
|
9283
11166
|
const remainingRequestsByParagraph = /* @__PURE__ */ new Map();
|
|
9284
11167
|
for (const request of comments) {
|
|
9285
11168
|
const paragraphIndex = request.paragraphIndex - 1;
|
|
9286
11169
|
if (paragraphIndex < 0 || paragraphIndex >= paragraphs.length) {
|
|
9287
|
-
|
|
11170
|
+
const message = `Paragraph ${request.paragraphIndex} out of range (1-${paragraphs.length})`;
|
|
11171
|
+
warnings.push(message);
|
|
11172
|
+
errors.push({ code: "TARGET_NOT_FOUND", message, paragraphIndex: request.paragraphIndex });
|
|
9288
11173
|
continue;
|
|
9289
11174
|
}
|
|
9290
11175
|
remainingRequestsByParagraph.set(paragraphIndex, (remainingRequestsByParagraph.get(paragraphIndex) || 0) + 1);
|
|
9291
11176
|
}
|
|
9292
11177
|
const paragraphIndexes = /* @__PURE__ */ new Map();
|
|
9293
|
-
for (const request of comments) {
|
|
11178
|
+
for (const [requestIndex, request] of comments.entries()) {
|
|
9294
11179
|
const paragraphIndex = request.paragraphIndex - 1;
|
|
9295
11180
|
if (paragraphIndex < 0 || paragraphIndex >= paragraphs.length) {
|
|
9296
11181
|
continue;
|
|
@@ -9301,18 +11186,40 @@ function injectCommentsIntoOoxml(oxml, comments, options = {}) {
|
|
|
9301
11186
|
textIndex = createParagraphTextIndex(targetParagraph);
|
|
9302
11187
|
paragraphIndexes.set(paragraphIndex, textIndex);
|
|
9303
11188
|
}
|
|
9304
|
-
const
|
|
11189
|
+
const anchorText = String(request.textToFind ?? "");
|
|
11190
|
+
const resolution = resolveTextInParagraphIndex(textIndex, anchorText);
|
|
11191
|
+
const remaining = (remainingRequestsByParagraph.get(paragraphIndex) || 1) - 1;
|
|
11192
|
+
remainingRequestsByParagraph.set(paragraphIndex, remaining);
|
|
11193
|
+
if (!resolution.found) {
|
|
11194
|
+
const error2 = {
|
|
11195
|
+
...resolution.error,
|
|
11196
|
+
requestIndex: requestIndex + 1,
|
|
11197
|
+
paragraphIndex: request.paragraphIndex
|
|
11198
|
+
};
|
|
11199
|
+
errors.push(error2);
|
|
11200
|
+
warnings.push(error2.message);
|
|
11201
|
+
if (remaining === 0) paragraphIndexes.delete(paragraphIndex);
|
|
11202
|
+
continue;
|
|
11203
|
+
}
|
|
11204
|
+
const commentId = typeof options.commentIdAllocator === "function" ? options.commentIdAllocator() : getNextRevisionId();
|
|
9305
11205
|
const success = injectMarkersIntoParagraph(
|
|
9306
11206
|
xmlDoc,
|
|
9307
11207
|
targetParagraph,
|
|
9308
|
-
|
|
11208
|
+
anchorText,
|
|
9309
11209
|
commentId,
|
|
9310
|
-
textIndex
|
|
11210
|
+
textIndex,
|
|
11211
|
+
revisionIdAllocator,
|
|
11212
|
+
resolution
|
|
9311
11213
|
);
|
|
9312
|
-
const remaining = (remainingRequestsByParagraph.get(paragraphIndex) || 1) - 1;
|
|
9313
|
-
remainingRequestsByParagraph.set(paragraphIndex, remaining);
|
|
9314
11214
|
if (!success) {
|
|
9315
|
-
|
|
11215
|
+
const error2 = {
|
|
11216
|
+
code: "ANCHOR_INSERTION_FAILED",
|
|
11217
|
+
message: `Resolved comment anchor could not be inserted in paragraph ${request.paragraphIndex}.`,
|
|
11218
|
+
requestIndex: requestIndex + 1,
|
|
11219
|
+
paragraphIndex: request.paragraphIndex
|
|
11220
|
+
};
|
|
11221
|
+
errors.push(error2);
|
|
11222
|
+
warnings.push(error2.message);
|
|
9316
11223
|
if (remaining === 0) {
|
|
9317
11224
|
paragraphIndexes.delete(paragraphIndex);
|
|
9318
11225
|
}
|
|
@@ -9324,6 +11231,14 @@ function injectCommentsIntoOoxml(oxml, comments, options = {}) {
|
|
|
9324
11231
|
author,
|
|
9325
11232
|
date
|
|
9326
11233
|
});
|
|
11234
|
+
resolvedAnchors.push({
|
|
11235
|
+
requestIndex: requestIndex + 1,
|
|
11236
|
+
paragraphIndex: request.paragraphIndex,
|
|
11237
|
+
text: anchorText,
|
|
11238
|
+
resolvedBy: resolution.resolvedBy,
|
|
11239
|
+
start: resolution.start,
|
|
11240
|
+
end: resolution.end
|
|
11241
|
+
});
|
|
9327
11242
|
if (remaining > 0) {
|
|
9328
11243
|
paragraphIndexes.set(paragraphIndex, createParagraphTextIndex(targetParagraph));
|
|
9329
11244
|
} else {
|
|
@@ -9335,7 +11250,9 @@ function injectCommentsIntoOoxml(oxml, comments, options = {}) {
|
|
|
9335
11250
|
oxml,
|
|
9336
11251
|
hasChanges: false,
|
|
9337
11252
|
commentsApplied: 0,
|
|
9338
|
-
warnings
|
|
11253
|
+
warnings,
|
|
11254
|
+
resolvedAnchors,
|
|
11255
|
+
...errors.length > 0 ? { status: "error", error: errors[0], errors } : {}
|
|
9339
11256
|
};
|
|
9340
11257
|
}
|
|
9341
11258
|
return {
|
|
@@ -9343,13 +11260,107 @@ function injectCommentsIntoOoxml(oxml, comments, options = {}) {
|
|
|
9343
11260
|
hasChanges: true,
|
|
9344
11261
|
commentsXml: buildCommentsPartXml(placedComments),
|
|
9345
11262
|
commentsApplied: placedComments.length,
|
|
9346
|
-
|
|
11263
|
+
placedComments,
|
|
11264
|
+
warnings,
|
|
11265
|
+
resolvedAnchors,
|
|
11266
|
+
...errors.length > 0 ? { status: "error", error: errors[0], errors } : {}
|
|
9347
11267
|
};
|
|
9348
11268
|
}
|
|
9349
11269
|
function injectCommentsIntoPackage2(packageOxml, commentsXml) {
|
|
9350
11270
|
return injectCommentsIntoPackage(packageOxml, commentsXml);
|
|
9351
11271
|
}
|
|
9352
11272
|
|
|
11273
|
+
// services/comment-replies.js
|
|
11274
|
+
var NS_W6 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
11275
|
+
function attr3(node, qualified, local) {
|
|
11276
|
+
return node?.getAttribute?.(qualified) || node?.getAttribute?.(local) || "";
|
|
11277
|
+
}
|
|
11278
|
+
function parseRequired(xml, partName) {
|
|
11279
|
+
const parsed = parseOoxmlSafe(xml, "application/xml");
|
|
11280
|
+
if (!parsed.doc || parsed.error) {
|
|
11281
|
+
return { error: { code: "PARSE_ERROR", message: `Could not parse ${partName}: ${parsed.error?.message || "invalid XML"}` } };
|
|
11282
|
+
}
|
|
11283
|
+
return { doc: parsed.doc };
|
|
11284
|
+
}
|
|
11285
|
+
function usedParaIds(commentsDoc, extendedDoc) {
|
|
11286
|
+
const ids = /* @__PURE__ */ new Set();
|
|
11287
|
+
for (const p of Array.from(commentsDoc?.getElementsByTagNameNS("*", "p") || [])) {
|
|
11288
|
+
const id = attr3(p, "w14:paraId", "paraId");
|
|
11289
|
+
if (id) ids.add(id.toUpperCase());
|
|
11290
|
+
}
|
|
11291
|
+
for (const ex of Array.from(extendedDoc?.getElementsByTagNameNS("*", "commentEx") || [])) {
|
|
11292
|
+
const id = attr3(ex, "w15:paraId", "paraId");
|
|
11293
|
+
if (id) ids.add(id.toUpperCase());
|
|
11294
|
+
}
|
|
11295
|
+
return ids;
|
|
11296
|
+
}
|
|
11297
|
+
function allocateParaId(commentId, occupied) {
|
|
11298
|
+
let candidate = createCommentParaId(commentId);
|
|
11299
|
+
let value = Number.parseInt(candidate, 16) >>> 0;
|
|
11300
|
+
while (occupied.has(candidate)) {
|
|
11301
|
+
value = value + 1 >>> 0;
|
|
11302
|
+
candidate = value.toString(16).toUpperCase().padStart(8, "0");
|
|
11303
|
+
}
|
|
11304
|
+
occupied.add(candidate);
|
|
11305
|
+
return candidate;
|
|
11306
|
+
}
|
|
11307
|
+
function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, parentCommentId, commentId, commentContent, author, date = (/* @__PURE__ */ new Date()).toISOString() }) {
|
|
11308
|
+
if (!commentsXml) return { status: "error", error: { code: "COMMENTS_PART_MISSING", message: "A comment reply requires an existing word/comments.xml part." } };
|
|
11309
|
+
const commentsParsed = parseRequired(commentsXml, "word/comments.xml");
|
|
11310
|
+
if (commentsParsed.error) return { status: "error", error: commentsParsed.error };
|
|
11311
|
+
const commentsDoc = commentsParsed.doc;
|
|
11312
|
+
const parent = Array.from(commentsDoc.getElementsByTagNameNS("*", "comment")).find((node) => attr3(node, "w:id", "id") === String(parentCommentId));
|
|
11313
|
+
if (!parent) return { status: "error", error: { code: "PARENT_COMMENT_NOT_FOUND", message: `Parent comment '${parentCommentId}' was not found.` } };
|
|
11314
|
+
let extendedDoc = null;
|
|
11315
|
+
if (commentsExtendedXml) {
|
|
11316
|
+
const parsed = parseRequired(commentsExtendedXml, "word/commentsExtended.xml");
|
|
11317
|
+
if (parsed.error) return { status: "error", error: parsed.error };
|
|
11318
|
+
extendedDoc = parsed.doc;
|
|
11319
|
+
}
|
|
11320
|
+
const occupied = usedParaIds(commentsDoc, extendedDoc);
|
|
11321
|
+
const parentParagraph = Array.from(parent.getElementsByTagNameNS(NS_W6, "p"))[0] || Array.from(parent.getElementsByTagNameNS("*", "p"))[0];
|
|
11322
|
+
if (!parentParagraph) return { status: "error", error: { code: "PARENT_COMMENT_INVALID", message: `Parent comment '${parentCommentId}' has no paragraph.` } };
|
|
11323
|
+
let parentParaId = attr3(parentParagraph, "w14:paraId", "paraId");
|
|
11324
|
+
if (!parentParaId) {
|
|
11325
|
+
parentParaId = allocateParaId(parentCommentId, occupied);
|
|
11326
|
+
parentParagraph.setAttributeNS(NS_W14, "w14:paraId", parentParaId);
|
|
11327
|
+
} else {
|
|
11328
|
+
parentParaId = parentParaId.toUpperCase();
|
|
11329
|
+
}
|
|
11330
|
+
const replyParaId = allocateParaId(commentId, occupied);
|
|
11331
|
+
const replyParsed = parseRequired(`<w:comments xmlns:w="${NS_W6}" xmlns:w14="${NS_W14}">${buildCommentElement(commentId, author, commentContent, date, replyParaId)}</w:comments>`, "reply comment");
|
|
11332
|
+
commentsDoc.documentElement.appendChild(commentsDoc.importNode(replyParsed.doc.documentElement.firstChild, true));
|
|
11333
|
+
if (!extendedDoc) {
|
|
11334
|
+
extendedDoc = parseRequired(buildCommentsExtendedPartXml([]), "word/commentsExtended.xml").doc;
|
|
11335
|
+
}
|
|
11336
|
+
const root = extendedDoc.documentElement;
|
|
11337
|
+
const entries = Array.from(root.getElementsByTagNameNS("*", "commentEx"));
|
|
11338
|
+
if (!entries.some((node) => attr3(node, "w15:paraId", "paraId").toUpperCase() === parentParaId)) {
|
|
11339
|
+
const parentEx = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
|
|
11340
|
+
parentEx.setAttributeNS(NS_W15, "w15:paraId", parentParaId);
|
|
11341
|
+
parentEx.setAttributeNS(NS_W15, "w15:done", "0");
|
|
11342
|
+
root.appendChild(parentEx);
|
|
11343
|
+
}
|
|
11344
|
+
const replyEx = extendedDoc.createElementNS(NS_W15, "w15:commentEx");
|
|
11345
|
+
replyEx.setAttributeNS(NS_W15, "w15:paraId", replyParaId);
|
|
11346
|
+
replyEx.setAttributeNS(NS_W15, "w15:paraIdParent", parentParaId);
|
|
11347
|
+
replyEx.setAttributeNS(NS_W15, "w15:done", "0");
|
|
11348
|
+
root.appendChild(replyEx);
|
|
11349
|
+
const serializer = createSerializer();
|
|
11350
|
+
return {
|
|
11351
|
+
status: "ok",
|
|
11352
|
+
hasChanges: true,
|
|
11353
|
+
commentsXml: serializer.serializeToString(commentsDoc),
|
|
11354
|
+
commentsExtendedXml: serializer.serializeToString(extendedDoc),
|
|
11355
|
+
commentsXmlMode: "replace",
|
|
11356
|
+
commentsExtendedXmlMode: "replace",
|
|
11357
|
+
commentId,
|
|
11358
|
+
parentCommentId: String(parentCommentId),
|
|
11359
|
+
paraId: replyParaId,
|
|
11360
|
+
parentParaId
|
|
11361
|
+
};
|
|
11362
|
+
}
|
|
11363
|
+
|
|
9353
11364
|
// engine/formatting-removal.js
|
|
9354
11365
|
function removeNode2(node) {
|
|
9355
11366
|
if (node?.parentNode) {
|
|
@@ -9411,19 +11422,19 @@ function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
|
|
|
9411
11422
|
if (!targetText || !ooxmlString) return ooxmlString;
|
|
9412
11423
|
const doc = parseOoxml(ooxmlString);
|
|
9413
11424
|
if (!doc) return ooxmlString;
|
|
9414
|
-
const
|
|
9415
|
-
const runs = doc.getElementsByTagNameNS(
|
|
9416
|
-
const insertions = doc.getElementsByTagNameNS(
|
|
11425
|
+
const NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
11426
|
+
const runs = doc.getElementsByTagNameNS(NS_W8, "r");
|
|
11427
|
+
const insertions = doc.getElementsByTagNameNS(NS_W8, "ins");
|
|
9417
11428
|
const allRuns = [...Array.from(runs)];
|
|
9418
11429
|
for (const ins of insertions) {
|
|
9419
|
-
const insideRuns = ins.getElementsByTagNameNS(
|
|
11430
|
+
const insideRuns = ins.getElementsByTagNameNS(NS_W8, "r");
|
|
9420
11431
|
allRuns.push(...Array.from(insideRuns));
|
|
9421
11432
|
}
|
|
9422
11433
|
for (const run of allRuns) {
|
|
9423
|
-
const textNodes = run.getElementsByTagNameNS(
|
|
11434
|
+
const textNodes = run.getElementsByTagNameNS(NS_W8, "t");
|
|
9424
11435
|
const runText = Array.from(textNodes).map((t) => t.textContent).join("");
|
|
9425
11436
|
if (runText.includes(targetText) || runText === targetText) {
|
|
9426
|
-
const rPrElements = run.getElementsByTagNameNS(
|
|
11437
|
+
const rPrElements = run.getElementsByTagNameNS(NS_W8, "rPr");
|
|
9427
11438
|
if (rPrElements.length > 0) {
|
|
9428
11439
|
const rPr = rPrElements[0];
|
|
9429
11440
|
const newRPr = removeFormattingFromRPr(rPr, formatTypes);
|
|
@@ -9458,7 +11469,7 @@ var HIGHLIGHT_COLOR_MAP = {
|
|
|
9458
11469
|
"white": "white"
|
|
9459
11470
|
};
|
|
9460
11471
|
function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
|
|
9461
|
-
const
|
|
11472
|
+
const NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
9462
11473
|
const ooxmlColor = HIGHLIGHT_COLOR_MAP[color.toLowerCase()] || "yellow";
|
|
9463
11474
|
const generateRedlines = options?.generateRedlines ?? false;
|
|
9464
11475
|
const author = options?.author || getDefaultAuthor();
|
|
@@ -9477,10 +11488,10 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
|
|
|
9477
11488
|
}
|
|
9478
11489
|
});
|
|
9479
11490
|
}
|
|
9480
|
-
const existingHighlight = rPrElement.getElementsByTagNameNS(
|
|
11491
|
+
const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W8, "highlight");
|
|
9481
11492
|
Array.from(existingHighlight).forEach(removeNode2);
|
|
9482
11493
|
const highlightEl = createWordElement(doc, "w:highlight");
|
|
9483
|
-
highlightEl.setAttributeNS(
|
|
11494
|
+
highlightEl.setAttributeNS(NS_W8, "w:val", ooxmlColor);
|
|
9484
11495
|
rPrElement.appendChild(highlightEl);
|
|
9485
11496
|
if (generateRedlines && previousRPrState) {
|
|
9486
11497
|
const rPrChange = createWordElement(doc, "w:rPrChange");
|
|
@@ -9489,7 +11500,7 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
|
|
|
9489
11500
|
rPrChange.setAttribute("w:author", metadata.author);
|
|
9490
11501
|
rPrChange.setAttribute("w:date", metadata.date);
|
|
9491
11502
|
rPrChange.appendChild(previousRPrState);
|
|
9492
|
-
const existingChange = rPrElement.getElementsByTagNameNS(
|
|
11503
|
+
const existingChange = rPrElement.getElementsByTagNameNS(NS_W8, "rPrChange");
|
|
9493
11504
|
Array.from(existingChange).forEach(removeNode2);
|
|
9494
11505
|
rPrElement.appendChild(rPrChange);
|
|
9495
11506
|
}
|
|
@@ -9499,27 +11510,35 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
|
|
|
9499
11510
|
if (!targetText || !ooxmlString) return ooxmlString;
|
|
9500
11511
|
const doc = parseOoxml(ooxmlString);
|
|
9501
11512
|
if (!doc) return ooxmlString;
|
|
11513
|
+
let revisionIdAllocator;
|
|
9502
11514
|
if (options?._revisionIdAllocator instanceof RevisionIdAllocator) {
|
|
9503
|
-
|
|
11515
|
+
revisionIdAllocator = options._revisionIdAllocator;
|
|
11516
|
+
seedRevisionIdsFromDocument(doc, revisionIdAllocator);
|
|
9504
11517
|
} else {
|
|
9505
|
-
createRevisionIdAllocator(doc);
|
|
11518
|
+
revisionIdAllocator = createRevisionIdAllocator(doc);
|
|
9506
11519
|
}
|
|
9507
|
-
const
|
|
11520
|
+
const NS_W8 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
9508
11521
|
const getRunText = (run) => {
|
|
9509
|
-
const textNodes = run.getElementsByTagNameNS(
|
|
11522
|
+
const textNodes = run.getElementsByTagNameNS(NS_W8, "t");
|
|
9510
11523
|
return Array.from(textNodes).map((t) => t.textContent).join("");
|
|
9511
11524
|
};
|
|
9512
|
-
const allRuns = Array.from(doc.getElementsByTagNameNS(
|
|
11525
|
+
const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W8, "r"));
|
|
11526
|
+
const sourceRunsWithClaimedRevisionIds = /* @__PURE__ */ new WeakSet();
|
|
9513
11527
|
const cloneRunWithText2 = (sourceRun, text, shouldHighlight) => {
|
|
9514
11528
|
const clonedRun = sourceRun.cloneNode(true);
|
|
9515
|
-
|
|
11529
|
+
if (sourceRunsWithClaimedRevisionIds.has(sourceRun)) {
|
|
11530
|
+
refreshRunPropertyChangeIds(clonedRun, revisionIdAllocator);
|
|
11531
|
+
} else {
|
|
11532
|
+
sourceRunsWithClaimedRevisionIds.add(sourceRun);
|
|
11533
|
+
}
|
|
11534
|
+
const textNodes = clonedRun.getElementsByTagNameNS(NS_W8, "t");
|
|
9516
11535
|
Array.from(textNodes).forEach(removeNode2);
|
|
9517
11536
|
const newText = createWordElement(doc, "w:t");
|
|
9518
11537
|
newText.setAttribute("xml:space", "preserve");
|
|
9519
11538
|
newText.textContent = text;
|
|
9520
11539
|
clonedRun.appendChild(newText);
|
|
9521
11540
|
if (shouldHighlight) {
|
|
9522
|
-
const rPrElements = clonedRun.getElementsByTagNameNS(
|
|
11541
|
+
const rPrElements = clonedRun.getElementsByTagNameNS(NS_W8, "rPr");
|
|
9523
11542
|
const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
|
|
9524
11543
|
const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
|
|
9525
11544
|
if (existingRPr) {
|
|
@@ -9569,16 +11588,19 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
|
|
|
9569
11588
|
}
|
|
9570
11589
|
|
|
9571
11590
|
// services/standalone-docx-plumbing.js
|
|
9572
|
-
var
|
|
11591
|
+
var NS_W7 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
9573
11592
|
var NS_CT = "http://schemas.openxmlformats.org/package/2006/content-types";
|
|
9574
11593
|
var NS_RELS = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
9575
11594
|
var NUMBERING_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering";
|
|
9576
11595
|
var NUMBERING_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml";
|
|
9577
11596
|
var COMMENTS_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments";
|
|
9578
11597
|
var COMMENTS_CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml";
|
|
11598
|
+
var COMMENTS_EXTENDED_REL_TYPE = "http://schemas.microsoft.com/office/2011/relationships/commentsExtended";
|
|
11599
|
+
var COMMENTS_EXTENDED_CONTENT_TYPE = "application/vnd.ms-word.commentsExtended+xml";
|
|
9579
11600
|
var DOCUMENT_PATH = "word/document.xml";
|
|
9580
11601
|
var NUMBERING_PATH = "word/numbering.xml";
|
|
9581
11602
|
var COMMENTS_PATH = "word/comments.xml";
|
|
11603
|
+
var COMMENTS_EXTENDED_PATH = "word/commentsExtended.xml";
|
|
9582
11604
|
var CONTENT_TYPES_PATH = "[Content_Types].xml";
|
|
9583
11605
|
var DOCUMENT_RELS_PATH = "word/_rels/document.xml.rels";
|
|
9584
11606
|
function parseXmlStrictStandalone(xmlText, label = "xml") {
|
|
@@ -9591,7 +11613,7 @@ function parseXmlStrictStandalone(xmlText, label = "xml") {
|
|
|
9591
11613
|
return parsed.doc;
|
|
9592
11614
|
}
|
|
9593
11615
|
function isSectionPropertiesElement(node) {
|
|
9594
|
-
return !!node && node.nodeType === 1 && node.namespaceURI ===
|
|
11616
|
+
return !!node && node.nodeType === 1 && node.namespaceURI === NS_W7 && node.localName === "sectPr";
|
|
9595
11617
|
}
|
|
9596
11618
|
function getBodyElementFromDocument(xmlDoc) {
|
|
9597
11619
|
return xmlDoc.getElementsByTagNameNS("*", "body")[0] || null;
|
|
@@ -9627,15 +11649,15 @@ function normalizeBodySectionOrderStandalone(xmlDoc) {
|
|
|
9627
11649
|
function sanitizeNestedParagraphsInTables(xmlDoc, options = {}) {
|
|
9628
11650
|
const onInfo = typeof options?.onInfo === "function" ? options.onInfo : () => {
|
|
9629
11651
|
};
|
|
9630
|
-
const tcs = xmlDoc.getElementsByTagNameNS(
|
|
11652
|
+
const tcs = xmlDoc.getElementsByTagNameNS(NS_W7, "tc");
|
|
9631
11653
|
let fixed = 0;
|
|
9632
11654
|
for (const tc of Array.from(tcs)) {
|
|
9633
11655
|
const outerParagraphs = Array.from(tc.childNodes || []).filter(
|
|
9634
|
-
(node) => node.nodeType === 1 && node.namespaceURI ===
|
|
11656
|
+
(node) => node.nodeType === 1 && node.namespaceURI === NS_W7 && node.localName === "p"
|
|
9635
11657
|
);
|
|
9636
11658
|
for (const outerParagraph of outerParagraphs) {
|
|
9637
11659
|
const innerParagraphs = Array.from(outerParagraph.childNodes || []).filter(
|
|
9638
|
-
(node) => node.nodeType === 1 && node.namespaceURI ===
|
|
11660
|
+
(node) => node.nodeType === 1 && node.namespaceURI === NS_W7 && node.localName === "p"
|
|
9639
11661
|
);
|
|
9640
11662
|
for (const innerParagraph of innerParagraphs) {
|
|
9641
11663
|
tc.insertBefore(innerParagraph, outerParagraph);
|
|
@@ -9704,7 +11726,7 @@ function extractReplacementNodesFromOoxml(outputOxml) {
|
|
|
9704
11726
|
const replacementNodes2 = body ? Array.from(body.childNodes || []).filter((node) => node.nodeType === 1 && !isSectionPropertiesElement(node)) : Array.from(doc.childNodes || []).filter((node) => node.nodeType === 1);
|
|
9705
11727
|
return { replacementNodes: replacementNodes2, numberingXml: null, sourceType: "document" };
|
|
9706
11728
|
}
|
|
9707
|
-
const wrapped = `<root xmlns:w="${
|
|
11729
|
+
const wrapped = `<root xmlns:w="${NS_W7}">${outputOxml}</root>`;
|
|
9708
11730
|
const fragmentDoc = parseXmlStrictStandalone(wrapped, "OOXML fragment");
|
|
9709
11731
|
const replacementNodes = Array.from(fragmentDoc.documentElement.childNodes || []).filter((node) => node.nodeType === 1);
|
|
9710
11732
|
return { replacementNodes, numberingXml: null, sourceType: "fragment" };
|
|
@@ -9720,17 +11742,21 @@ function extractReplacementNodesFromOoxml(outputOxml) {
|
|
|
9720
11742
|
}
|
|
9721
11743
|
function upsertContentTypeOverride(ctDoc, partName, contentType) {
|
|
9722
11744
|
const overrides = Array.from(ctDoc.getElementsByTagNameNS("*", "Override"));
|
|
9723
|
-
const
|
|
11745
|
+
const existingOverride = overrides.find(
|
|
9724
11746
|
(override2) => (override2.getAttribute("PartName") || "").toLowerCase() === String(partName).toLowerCase()
|
|
9725
11747
|
);
|
|
9726
|
-
if (
|
|
11748
|
+
if (existingOverride) {
|
|
11749
|
+
if ((existingOverride.getAttribute("ContentType") || "") === contentType) return false;
|
|
11750
|
+
existingOverride.setAttribute("ContentType", contentType);
|
|
11751
|
+
return true;
|
|
11752
|
+
}
|
|
9727
11753
|
const override = ctDoc.createElementNS(NS_CT, "Override");
|
|
9728
11754
|
override.setAttribute("PartName", partName);
|
|
9729
11755
|
override.setAttribute("ContentType", contentType);
|
|
9730
11756
|
ctDoc.documentElement.appendChild(override);
|
|
9731
11757
|
return true;
|
|
9732
11758
|
}
|
|
9733
|
-
function upsertDocumentRelationship(relsDoc, relType, target) {
|
|
11759
|
+
function upsertDocumentRelationship(relsDoc, relType, target, options = {}) {
|
|
9734
11760
|
const relsRoot = relsDoc.getElementsByTagNameNS("*", "Relationships")[0] || relsDoc.documentElement;
|
|
9735
11761
|
const rels = Array.from(relsRoot.getElementsByTagNameNS("*", "Relationship"));
|
|
9736
11762
|
const hasRel = rels.some((rel2) => (rel2.getAttribute("Type") || "") === relType);
|
|
@@ -9743,11 +11769,17 @@ function upsertDocumentRelationship(relsDoc, relType, target) {
|
|
|
9743
11769
|
maxId = Math.max(maxId, idNum);
|
|
9744
11770
|
}
|
|
9745
11771
|
}
|
|
11772
|
+
const newId = `rId${maxId + 1}`;
|
|
9746
11773
|
const rel = relsDoc.createElementNS(NS_RELS, "Relationship");
|
|
9747
|
-
rel.setAttribute("Id",
|
|
11774
|
+
rel.setAttribute("Id", newId);
|
|
9748
11775
|
rel.setAttribute("Type", relType);
|
|
9749
11776
|
rel.setAttribute("Target", target);
|
|
9750
11777
|
relsRoot.appendChild(rel);
|
|
11778
|
+
if (options?._receiptCollector) {
|
|
11779
|
+
options._receiptCollector.recordRelationship(newId);
|
|
11780
|
+
} else if (options?._documentOperationSession?.receiptCollector) {
|
|
11781
|
+
options._documentOperationSession.receiptCollector.recordRelationship(newId);
|
|
11782
|
+
}
|
|
9751
11783
|
return true;
|
|
9752
11784
|
}
|
|
9753
11785
|
async function readZipText(zip, filePath) {
|
|
@@ -9759,9 +11791,13 @@ async function ensureNumberingArtifactsInZip(zip, numberingXmlList, options = {}
|
|
|
9759
11791
|
const onInfo = typeof options?.onInfo === "function" ? options.onInfo : () => {
|
|
9760
11792
|
};
|
|
9761
11793
|
const mergeNumberingXml = typeof options?.mergeNumberingXml === "function" ? options.mergeNumberingXml : null;
|
|
11794
|
+
const onWarn = typeof options?.onWarn === "function" ? options.onWarn : warn;
|
|
9762
11795
|
const incomingPayloads = (Array.isArray(numberingXmlList) ? numberingXmlList : [numberingXmlList]).filter(Boolean);
|
|
9763
11796
|
if (incomingPayloads.length === 0) return;
|
|
9764
11797
|
const existing = await readZipText(zip, NUMBERING_PATH);
|
|
11798
|
+
if (existing && !mergeNumberingXml) {
|
|
11799
|
+
onWarn("[Deprecation] Replacing an existing numbering.xml without mergeNumberingXml is deprecated and will throw in the next major version.");
|
|
11800
|
+
}
|
|
9765
11801
|
let mergedNumberingXml = existing || null;
|
|
9766
11802
|
for (const incomingNumberingXml of incomingPayloads) {
|
|
9767
11803
|
if (!mergedNumberingXml) {
|
|
@@ -9787,7 +11823,7 @@ async function ensureNumberingArtifactsInZip(zip, numberingXmlList, options = {}
|
|
|
9787
11823
|
const relsText = await readZipText(zip, DOCUMENT_RELS_PATH);
|
|
9788
11824
|
if (relsText) {
|
|
9789
11825
|
const relsDoc = parseXmlStrictStandalone(relsText, DOCUMENT_RELS_PATH);
|
|
9790
|
-
if (upsertDocumentRelationship(relsDoc, NUMBERING_REL_TYPE, "numbering.xml")) {
|
|
11826
|
+
if (upsertDocumentRelationship(relsDoc, NUMBERING_REL_TYPE, "numbering.xml", options)) {
|
|
9791
11827
|
zip.file(DOCUMENT_RELS_PATH, serializer.serializeToString(relsDoc));
|
|
9792
11828
|
}
|
|
9793
11829
|
}
|
|
@@ -9798,7 +11834,7 @@ async function ensureCommentsArtifactsInZip(zip, commentsXml, options = {}) {
|
|
|
9798
11834
|
if (!commentsXml) return;
|
|
9799
11835
|
const serializer = createSerializer();
|
|
9800
11836
|
const existingText = await readZipText(zip, COMMENTS_PATH);
|
|
9801
|
-
if (!existingText) {
|
|
11837
|
+
if (!existingText || options.replaceExisting === true) {
|
|
9802
11838
|
onInfo("[Demo] Adding comments.xml");
|
|
9803
11839
|
zip.file(COMMENTS_PATH, commentsXml);
|
|
9804
11840
|
} else {
|
|
@@ -9806,9 +11842,9 @@ async function ensureCommentsArtifactsInZip(zip, commentsXml, options = {}) {
|
|
|
9806
11842
|
const incomingDoc = parseXmlStrictStandalone(commentsXml, "word/comments.xml (incoming)");
|
|
9807
11843
|
const existingRoot = existingDoc.documentElement;
|
|
9808
11844
|
const existingIds = new Set(
|
|
9809
|
-
Array.from(existingRoot.getElementsByTagNameNS(
|
|
11845
|
+
Array.from(existingRoot.getElementsByTagNameNS(NS_W7, "comment")).map((comment) => comment.getAttribute("w:id") || comment.getAttribute("id")).filter(Boolean)
|
|
9810
11846
|
);
|
|
9811
|
-
for (const incomingComment of Array.from(incomingDoc.documentElement.getElementsByTagNameNS(
|
|
11847
|
+
for (const incomingComment of Array.from(incomingDoc.documentElement.getElementsByTagNameNS(NS_W7, "comment"))) {
|
|
9812
11848
|
const id = incomingComment.getAttribute("w:id") || incomingComment.getAttribute("id");
|
|
9813
11849
|
if (id && existingIds.has(id)) {
|
|
9814
11850
|
throw new Error(`Duplicate comment id: ${id}`);
|
|
@@ -9827,7 +11863,27 @@ async function ensureCommentsArtifactsInZip(zip, commentsXml, options = {}) {
|
|
|
9827
11863
|
const relsText = await readZipText(zip, DOCUMENT_RELS_PATH);
|
|
9828
11864
|
if (relsText) {
|
|
9829
11865
|
const relsDoc = parseXmlStrictStandalone(relsText, DOCUMENT_RELS_PATH);
|
|
9830
|
-
if (upsertDocumentRelationship(relsDoc, COMMENTS_REL_TYPE, "comments.xml")) {
|
|
11866
|
+
if (upsertDocumentRelationship(relsDoc, COMMENTS_REL_TYPE, "comments.xml", options)) {
|
|
11867
|
+
zip.file(DOCUMENT_RELS_PATH, serializer.serializeToString(relsDoc));
|
|
11868
|
+
}
|
|
11869
|
+
}
|
|
11870
|
+
}
|
|
11871
|
+
async function ensureCommentsExtendedArtifactsInZip(zip, commentsExtendedXml, options = {}) {
|
|
11872
|
+
if (!commentsExtendedXml) return;
|
|
11873
|
+
parseXmlStrictStandalone(commentsExtendedXml, "word/commentsExtended.xml");
|
|
11874
|
+
zip.file(COMMENTS_EXTENDED_PATH, commentsExtendedXml);
|
|
11875
|
+
const serializer = createSerializer();
|
|
11876
|
+
const ctText = await readZipText(zip, CONTENT_TYPES_PATH);
|
|
11877
|
+
if (ctText) {
|
|
11878
|
+
const ctDoc = parseXmlStrictStandalone(ctText, CONTENT_TYPES_PATH);
|
|
11879
|
+
if (upsertContentTypeOverride(ctDoc, "/word/commentsExtended.xml", COMMENTS_EXTENDED_CONTENT_TYPE)) {
|
|
11880
|
+
zip.file(CONTENT_TYPES_PATH, serializer.serializeToString(ctDoc));
|
|
11881
|
+
}
|
|
11882
|
+
}
|
|
11883
|
+
const relsText = await readZipText(zip, DOCUMENT_RELS_PATH);
|
|
11884
|
+
if (relsText) {
|
|
11885
|
+
const relsDoc = parseXmlStrictStandalone(relsText, DOCUMENT_RELS_PATH);
|
|
11886
|
+
if (upsertDocumentRelationship(relsDoc, COMMENTS_EXTENDED_REL_TYPE, "commentsExtended.xml", options)) {
|
|
9831
11887
|
zip.file(DOCUMENT_RELS_PATH, serializer.serializeToString(relsDoc));
|
|
9832
11888
|
}
|
|
9833
11889
|
}
|
|
@@ -9851,12 +11907,12 @@ async function validateDocxPackage(zip) {
|
|
|
9851
11907
|
if (sectPrIndexes.length === 1 && sectPrIndexes[0] !== directBodyElements.length - 1) {
|
|
9852
11908
|
throw new Error("Validation failed: w:sectPr not last");
|
|
9853
11909
|
}
|
|
9854
|
-
const tcs = documentDoc.getElementsByTagNameNS(
|
|
11910
|
+
const tcs = documentDoc.getElementsByTagNameNS(NS_W7, "tc");
|
|
9855
11911
|
for (const tc of Array.from(tcs)) {
|
|
9856
11912
|
for (const child of Array.from(tc.childNodes || []).filter((node) => node.nodeType === 1)) {
|
|
9857
|
-
if (child.namespaceURI ===
|
|
11913
|
+
if (child.namespaceURI === NS_W7 && child.localName === "p") {
|
|
9858
11914
|
const hasNestedParagraph = Array.from(child.childNodes || []).some(
|
|
9859
|
-
(node) => node.nodeType === 1 && node.namespaceURI ===
|
|
11915
|
+
(node) => node.nodeType === 1 && node.namespaceURI === NS_W7 && node.localName === "p"
|
|
9860
11916
|
);
|
|
9861
11917
|
if (hasNestedParagraph) {
|
|
9862
11918
|
throw new Error("Validation failed: nested w:p");
|
|
@@ -9864,17 +11920,77 @@ async function validateDocxPackage(zip) {
|
|
|
9864
11920
|
}
|
|
9865
11921
|
}
|
|
9866
11922
|
}
|
|
9867
|
-
const hasNumberingUsage = documentDoc.getElementsByTagNameNS(
|
|
9868
|
-
const hasCommentUsage = documentDoc.getElementsByTagNameNS(
|
|
11923
|
+
const hasNumberingUsage = documentDoc.getElementsByTagNameNS(NS_W7, "numPr").length > 0;
|
|
11924
|
+
const hasCommentUsage = documentDoc.getElementsByTagNameNS(NS_W7, "commentRangeStart").length > 0 || documentDoc.getElementsByTagNameNS(NS_W7, "commentRangeEnd").length > 0 || documentDoc.getElementsByTagNameNS(NS_W7, "commentReference").length > 0;
|
|
9869
11925
|
const numberingXml = await readZipText(zip, NUMBERING_PATH);
|
|
9870
11926
|
const commentsXml = await readZipText(zip, COMMENTS_PATH);
|
|
11927
|
+
const commentsExtendedXml = await readZipText(zip, COMMENTS_EXTENDED_PATH);
|
|
11928
|
+
if (commentsExtendedXml && !commentsXml) {
|
|
11929
|
+
throw new Error("Validation failed: commentsExtended part exists but comments part is missing");
|
|
11930
|
+
}
|
|
9871
11931
|
if (numberingXml) {
|
|
9872
11932
|
parseXmlStrictStandalone(numberingXml, NUMBERING_PATH);
|
|
9873
11933
|
} else if (hasNumberingUsage) {
|
|
9874
11934
|
throw new Error("Validation failed: numbering used but part missing");
|
|
9875
11935
|
}
|
|
9876
11936
|
if (commentsXml) {
|
|
9877
|
-
parseXmlStrictStandalone(commentsXml, COMMENTS_PATH);
|
|
11937
|
+
const commentsDoc = parseXmlStrictStandalone(commentsXml, COMMENTS_PATH);
|
|
11938
|
+
const idsFor = (doc, localName2) => Array.from(doc.getElementsByTagNameNS(NS_W7, localName2)).map((node) => node.getAttribute("w:id") || node.getAttribute("id")).filter((id) => id !== "");
|
|
11939
|
+
const starts = new Set(idsFor(documentDoc, "commentRangeStart"));
|
|
11940
|
+
const ends = new Set(idsFor(documentDoc, "commentRangeEnd"));
|
|
11941
|
+
const references = new Set(idsFor(documentDoc, "commentReference"));
|
|
11942
|
+
const definitionIds = idsFor(commentsDoc, "comment");
|
|
11943
|
+
const definitions = new Set(definitionIds);
|
|
11944
|
+
const sorted = (ids) => Array.from(ids).sort((a, b) => Number(a) - Number(b) || a.localeCompare(b));
|
|
11945
|
+
const difference = (left, right) => sorted(new Set(Array.from(left).filter((id) => !right.has(id))));
|
|
11946
|
+
const duplicateDefinitions = sorted(new Set(definitionIds.filter((id, index) => definitionIds.indexOf(id) !== index)));
|
|
11947
|
+
if (duplicateDefinitions.length > 0) {
|
|
11948
|
+
throw new Error(`Validation failed: duplicate comment definitions for id(s): ${duplicateDefinitions.join(", ")}`);
|
|
11949
|
+
}
|
|
11950
|
+
const startsWithoutEnds = difference(starts, ends);
|
|
11951
|
+
const endsWithoutStarts = difference(ends, starts);
|
|
11952
|
+
if (startsWithoutEnds.length > 0 || endsWithoutStarts.length > 0) {
|
|
11953
|
+
const parts = [];
|
|
11954
|
+
if (startsWithoutEnds.length > 0) parts.push(`start without end: ${startsWithoutEnds.join(", ")}`);
|
|
11955
|
+
if (endsWithoutStarts.length > 0) parts.push(`end without start: ${endsWithoutStarts.join(", ")}`);
|
|
11956
|
+
throw new Error(`Validation failed: unbalanced comment range marker(s) (${parts.join("; ")})`);
|
|
11957
|
+
}
|
|
11958
|
+
const rangesWithoutReferences = difference(/* @__PURE__ */ new Set([...starts, ...ends]), references);
|
|
11959
|
+
if (rangesWithoutReferences.length > 0) {
|
|
11960
|
+
throw new Error(`Validation failed: comment range has no reference for id(s): ${rangesWithoutReferences.join(", ")}`);
|
|
11961
|
+
}
|
|
11962
|
+
const usagesWithoutDefinitions = difference(/* @__PURE__ */ new Set([...starts, ...ends, ...references]), definitions);
|
|
11963
|
+
if (usagesWithoutDefinitions.length > 0) {
|
|
11964
|
+
throw new Error(`Validation failed: comment usage has no definition for id(s): ${usagesWithoutDefinitions.join(", ")}`);
|
|
11965
|
+
}
|
|
11966
|
+
const replyCommentIds = /* @__PURE__ */ new Set();
|
|
11967
|
+
if (commentsExtendedXml) {
|
|
11968
|
+
const extendedDoc = parseXmlStrictStandalone(commentsExtendedXml, COMMENTS_EXTENDED_PATH);
|
|
11969
|
+
const paraIdToCommentId = /* @__PURE__ */ new Map();
|
|
11970
|
+
for (const comment of Array.from(commentsDoc.getElementsByTagNameNS("*", "comment"))) {
|
|
11971
|
+
const id = comment.getAttribute("w:id") || comment.getAttribute("id");
|
|
11972
|
+
const paragraph = Array.from(comment.getElementsByTagNameNS("*", "p"))[0];
|
|
11973
|
+
const paraId = paragraph?.getAttribute("w14:paraId") || paragraph?.getAttribute("paraId");
|
|
11974
|
+
if (paraId) paraIdToCommentId.set(paraId.toUpperCase(), id);
|
|
11975
|
+
}
|
|
11976
|
+
const knownParaIds = new Set(paraIdToCommentId.keys());
|
|
11977
|
+
const extendedParaIds = /* @__PURE__ */ new Set();
|
|
11978
|
+
for (const entry of Array.from(extendedDoc.getElementsByTagNameNS("*", "commentEx"))) {
|
|
11979
|
+
const paraId = entry.getAttribute("w15:paraId") || entry.getAttribute("paraId");
|
|
11980
|
+
const parentParaId = entry.getAttribute("w15:paraIdParent") || entry.getAttribute("paraIdParent");
|
|
11981
|
+
if (!paraId || !knownParaIds.has(paraId.toUpperCase())) throw new Error(`Validation failed: commentsExtended entry has no matching comment paragraph: ${paraId || "(missing)"}`);
|
|
11982
|
+
if (extendedParaIds.has(paraId.toUpperCase())) throw new Error(`Validation failed: duplicate commentsExtended entry for paraId: ${paraId}`);
|
|
11983
|
+
extendedParaIds.add(paraId.toUpperCase());
|
|
11984
|
+
if (parentParaId) {
|
|
11985
|
+
if (!knownParaIds.has(parentParaId.toUpperCase())) throw new Error(`Validation failed: commentsExtended parent paragraph was not found: ${parentParaId}`);
|
|
11986
|
+
replyCommentIds.add(paraIdToCommentId.get(paraId.toUpperCase()));
|
|
11987
|
+
}
|
|
11988
|
+
}
|
|
11989
|
+
}
|
|
11990
|
+
const definitionsWithoutReferences = difference(new Set([...definitions].filter((id) => !replyCommentIds.has(id))), references);
|
|
11991
|
+
if (definitionsWithoutReferences.length > 0) {
|
|
11992
|
+
throw new Error(`Validation failed: comment definition has no document reference for id(s): ${definitionsWithoutReferences.join(", ")}`);
|
|
11993
|
+
}
|
|
9878
11994
|
} else if (hasCommentUsage) {
|
|
9879
11995
|
throw new Error("Validation failed: comments used but part missing");
|
|
9880
11996
|
}
|
|
@@ -9916,6 +12032,16 @@ async function validateDocxPackage(zip) {
|
|
|
9916
12032
|
throw new Error("Validation failed: comments rel missing");
|
|
9917
12033
|
}
|
|
9918
12034
|
}
|
|
12035
|
+
if (commentsExtendedXml) {
|
|
12036
|
+
const hasContentType = Array.from(ctDoc.getElementsByTagNameNS("*", "Override")).some(
|
|
12037
|
+
(override) => (override.getAttribute("PartName") || "").toLowerCase() === "/word/commentsextended.xml" && (override.getAttribute("ContentType") || "") === COMMENTS_EXTENDED_CONTENT_TYPE
|
|
12038
|
+
);
|
|
12039
|
+
const hasRelationship = Array.from(relsDoc.getElementsByTagNameNS("*", "Relationship")).some(
|
|
12040
|
+
(rel) => (rel.getAttribute("Type") || "") === COMMENTS_EXTENDED_REL_TYPE
|
|
12041
|
+
);
|
|
12042
|
+
if (!hasContentType) throw new Error("Validation failed: commentsExtended CT override missing");
|
|
12043
|
+
if (!hasRelationship) throw new Error("Validation failed: commentsExtended rel missing");
|
|
12044
|
+
}
|
|
9919
12045
|
}
|
|
9920
12046
|
|
|
9921
12047
|
// orchestration/route-plan.js
|
|
@@ -10040,6 +12166,239 @@ function extractParagraphIdFromOoxml(ooxml) {
|
|
|
10040
12166
|
return match ? match[1] : null;
|
|
10041
12167
|
}
|
|
10042
12168
|
|
|
12169
|
+
// services/receipt-collector.js
|
|
12170
|
+
var ReceiptCollector = class {
|
|
12171
|
+
constructor() {
|
|
12172
|
+
this.receipts = [];
|
|
12173
|
+
this.activeReceipt = null;
|
|
12174
|
+
}
|
|
12175
|
+
beginOperation(operationIndex, operationId = null, authorUsed = null) {
|
|
12176
|
+
this.activeReceipt = {
|
|
12177
|
+
operationIndex: typeof operationIndex === "number" ? operationIndex : 1,
|
|
12178
|
+
...operationId ? { operationId: String(operationId) } : {},
|
|
12179
|
+
attemptedDisposition: "applied",
|
|
12180
|
+
finalDisposition: "applied",
|
|
12181
|
+
committed: true,
|
|
12182
|
+
...authorUsed ? { authorUsed: String(authorUsed) } : {},
|
|
12183
|
+
revisionItems: [],
|
|
12184
|
+
commentIds: [],
|
|
12185
|
+
numberingIds: [],
|
|
12186
|
+
relationshipIds: [],
|
|
12187
|
+
affectedTargets: [],
|
|
12188
|
+
warnings: []
|
|
12189
|
+
};
|
|
12190
|
+
}
|
|
12191
|
+
recordRevision(id, kind = "structural", partName = "word/document.xml") {
|
|
12192
|
+
if (!this.activeReceipt || id == null) return;
|
|
12193
|
+
const strId = String(id);
|
|
12194
|
+
if (!this.activeReceipt.revisionItems.some((item) => item.id === strId && item.kind === kind && item.partName === partName)) {
|
|
12195
|
+
this.activeReceipt.revisionItems.push({
|
|
12196
|
+
id: strId,
|
|
12197
|
+
kind,
|
|
12198
|
+
partName
|
|
12199
|
+
});
|
|
12200
|
+
}
|
|
12201
|
+
}
|
|
12202
|
+
recordComment(id, _partName = "word/comments.xml") {
|
|
12203
|
+
if (!this.activeReceipt || id == null) return;
|
|
12204
|
+
const strId = String(id);
|
|
12205
|
+
if (!this.activeReceipt.commentIds.includes(strId)) {
|
|
12206
|
+
this.activeReceipt.commentIds.push(strId);
|
|
12207
|
+
}
|
|
12208
|
+
}
|
|
12209
|
+
recordNumbering(id, _partName = "word/numbering.xml") {
|
|
12210
|
+
if (!this.activeReceipt || id == null) return;
|
|
12211
|
+
const strId = String(id);
|
|
12212
|
+
if (!this.activeReceipt.numberingIds.includes(strId)) {
|
|
12213
|
+
this.activeReceipt.numberingIds.push(strId);
|
|
12214
|
+
}
|
|
12215
|
+
}
|
|
12216
|
+
recordRelationship(id, _partName = "word/_rels/document.xml.rels") {
|
|
12217
|
+
if (!this.activeReceipt || id == null) return;
|
|
12218
|
+
const strId = String(id);
|
|
12219
|
+
if (!this.activeReceipt.relationshipIds.includes(strId)) {
|
|
12220
|
+
this.activeReceipt.relationshipIds.push(strId);
|
|
12221
|
+
}
|
|
12222
|
+
}
|
|
12223
|
+
recordAffectedTarget(target) {
|
|
12224
|
+
if (!this.activeReceipt || !target) return;
|
|
12225
|
+
this.activeReceipt.affectedTargets.push(JSON.parse(JSON.stringify(target)));
|
|
12226
|
+
}
|
|
12227
|
+
recordWarning(warning) {
|
|
12228
|
+
if (!this.activeReceipt || !warning) return;
|
|
12229
|
+
this.activeReceipt.warnings.push(String(warning));
|
|
12230
|
+
}
|
|
12231
|
+
commitOperation(disposition = "applied") {
|
|
12232
|
+
if (!this.activeReceipt) return null;
|
|
12233
|
+
this.activeReceipt.attemptedDisposition = disposition;
|
|
12234
|
+
this.activeReceipt.finalDisposition = disposition;
|
|
12235
|
+
this.activeReceipt.committed = disposition === "applied";
|
|
12236
|
+
const committed = JSON.parse(JSON.stringify(this.activeReceipt));
|
|
12237
|
+
this.receipts.push(committed);
|
|
12238
|
+
this.activeReceipt = null;
|
|
12239
|
+
return committed;
|
|
12240
|
+
}
|
|
12241
|
+
abortOperation(disposition = "refused") {
|
|
12242
|
+
if (!this.activeReceipt) return null;
|
|
12243
|
+
this.activeReceipt.attemptedDisposition = disposition;
|
|
12244
|
+
this.activeReceipt.finalDisposition = disposition;
|
|
12245
|
+
this.activeReceipt.committed = false;
|
|
12246
|
+
const aborted = JSON.parse(JSON.stringify(this.activeReceipt));
|
|
12247
|
+
this.activeReceipt = null;
|
|
12248
|
+
return aborted;
|
|
12249
|
+
}
|
|
12250
|
+
createSavepoint() {
|
|
12251
|
+
return {
|
|
12252
|
+
receipts: JSON.parse(JSON.stringify(this.receipts)),
|
|
12253
|
+
activeReceipt: this.activeReceipt ? JSON.parse(JSON.stringify(this.activeReceipt)) : null
|
|
12254
|
+
};
|
|
12255
|
+
}
|
|
12256
|
+
restoreSavepoint(savepoint) {
|
|
12257
|
+
if (!savepoint) return;
|
|
12258
|
+
this.receipts = Array.isArray(savepoint.receipts) ? JSON.parse(JSON.stringify(savepoint.receipts)) : [];
|
|
12259
|
+
this.activeReceipt = savepoint.activeReceipt ? JSON.parse(JSON.stringify(savepoint.activeReceipt)) : null;
|
|
12260
|
+
}
|
|
12261
|
+
clear() {
|
|
12262
|
+
this.receipts = [];
|
|
12263
|
+
this.activeReceipt = null;
|
|
12264
|
+
}
|
|
12265
|
+
markRolledBack() {
|
|
12266
|
+
for (const receipt of this.receipts) {
|
|
12267
|
+
if (receipt.attemptedDisposition === "applied") {
|
|
12268
|
+
receipt.finalDisposition = "rolled_back";
|
|
12269
|
+
receipt.committed = false;
|
|
12270
|
+
}
|
|
12271
|
+
}
|
|
12272
|
+
this.activeReceipt = null;
|
|
12273
|
+
}
|
|
12274
|
+
getReceipts() {
|
|
12275
|
+
return JSON.parse(JSON.stringify(this.receipts));
|
|
12276
|
+
}
|
|
12277
|
+
getCurrentReceipt() {
|
|
12278
|
+
return this.activeReceipt ? JSON.parse(JSON.stringify(this.activeReceipt)) : null;
|
|
12279
|
+
}
|
|
12280
|
+
};
|
|
12281
|
+
function createEmptyReceipt(operationIndex, operationId = null, authorUsed = null, disposition = "not_attempted") {
|
|
12282
|
+
return {
|
|
12283
|
+
operationIndex: typeof operationIndex === "number" ? operationIndex : 1,
|
|
12284
|
+
...operationId ? { operationId: String(operationId) } : {},
|
|
12285
|
+
attemptedDisposition: disposition,
|
|
12286
|
+
finalDisposition: disposition,
|
|
12287
|
+
committed: false,
|
|
12288
|
+
...authorUsed ? { authorUsed: String(authorUsed) } : {},
|
|
12289
|
+
revisionItems: [],
|
|
12290
|
+
commentIds: [],
|
|
12291
|
+
numberingIds: [],
|
|
12292
|
+
relationshipIds: [],
|
|
12293
|
+
affectedTargets: [],
|
|
12294
|
+
warnings: []
|
|
12295
|
+
};
|
|
12296
|
+
}
|
|
12297
|
+
function reconcileReceiptsAgainstOutput(parts, receipts) {
|
|
12298
|
+
if (!Array.isArray(receipts) || receipts.length === 0) {
|
|
12299
|
+
return { valid: true };
|
|
12300
|
+
}
|
|
12301
|
+
const committedReceipts = receipts.filter((r) => r && r.committed === true && r.finalDisposition === "applied");
|
|
12302
|
+
if (committedReceipts.length === 0) {
|
|
12303
|
+
return { valid: true };
|
|
12304
|
+
}
|
|
12305
|
+
const revisionIdSet = /* @__PURE__ */ new Set();
|
|
12306
|
+
if (parts?.documentXml && typeof parts.documentXml === "string") {
|
|
12307
|
+
const revRegex = /<(?:w:)?(?:ins|del|rPrChange|pPrChange|moveFrom|moveTo)\b[^>]*?\b(?:w:)?id="([^"]+)"/g;
|
|
12308
|
+
let m;
|
|
12309
|
+
while ((m = revRegex.exec(parts.documentXml)) !== null) {
|
|
12310
|
+
revisionIdSet.add(m[1]);
|
|
12311
|
+
}
|
|
12312
|
+
}
|
|
12313
|
+
const commentIdSet = /* @__PURE__ */ new Set();
|
|
12314
|
+
if (parts?.commentsXml && typeof parts.commentsXml === "string") {
|
|
12315
|
+
const comRegex = /<(?:w:)?comment\b[^>]*?\b(?:w:)?id="([^"]+)"/g;
|
|
12316
|
+
let m;
|
|
12317
|
+
while ((m = comRegex.exec(parts.commentsXml)) !== null) {
|
|
12318
|
+
commentIdSet.add(m[1]);
|
|
12319
|
+
}
|
|
12320
|
+
}
|
|
12321
|
+
const numberingIdSet = /* @__PURE__ */ new Set();
|
|
12322
|
+
const combinedNumbering = [parts?.numberingXml, ...parts?.numberingXmlParts || []].filter(Boolean).join("\n");
|
|
12323
|
+
if (combinedNumbering) {
|
|
12324
|
+
const numRegex = /<(?:w:)?num\b[^>]*?\b(?:w:)?numId="([^"]+)"/g;
|
|
12325
|
+
let m;
|
|
12326
|
+
while ((m = numRegex.exec(combinedNumbering)) !== null) {
|
|
12327
|
+
numberingIdSet.add(m[1]);
|
|
12328
|
+
}
|
|
12329
|
+
}
|
|
12330
|
+
if (parts?.documentXml && typeof parts.documentXml === "string") {
|
|
12331
|
+
const docNumRegex = /<(?:w:)?numId\b[^>]*?\b(?:w:)?val="([^"]+)"/g;
|
|
12332
|
+
let m;
|
|
12333
|
+
while ((m = docNumRegex.exec(parts.documentXml)) !== null) {
|
|
12334
|
+
numberingIdSet.add(m[1]);
|
|
12335
|
+
}
|
|
12336
|
+
}
|
|
12337
|
+
const relIdSet = /* @__PURE__ */ new Set();
|
|
12338
|
+
if (parts?.relationshipsXml && typeof parts.relationshipsXml === "string") {
|
|
12339
|
+
const relRegex = /<Relationship\b[^>]*?\bId="([^"]+)"/g;
|
|
12340
|
+
let m;
|
|
12341
|
+
while ((m = relRegex.exec(parts.relationshipsXml)) !== null) {
|
|
12342
|
+
relIdSet.add(m[1]);
|
|
12343
|
+
}
|
|
12344
|
+
}
|
|
12345
|
+
for (const receipt of committedReceipts) {
|
|
12346
|
+
if (Array.isArray(receipt.revisionItems)) {
|
|
12347
|
+
for (const item of receipt.revisionItems) {
|
|
12348
|
+
if (item.partName === "word/document.xml" && !revisionIdSet.has(String(item.id))) {
|
|
12349
|
+
return {
|
|
12350
|
+
valid: false,
|
|
12351
|
+
error: {
|
|
12352
|
+
code: "RECEIPT_RECONCILIATION_FAILED",
|
|
12353
|
+
message: `Committed revision id '${item.id}' (kind: ${item.kind}) was not found in word/document.xml.`
|
|
12354
|
+
}
|
|
12355
|
+
};
|
|
12356
|
+
}
|
|
12357
|
+
}
|
|
12358
|
+
}
|
|
12359
|
+
if (Array.isArray(receipt.commentIds)) {
|
|
12360
|
+
for (const id of receipt.commentIds) {
|
|
12361
|
+
if (!commentIdSet.has(String(id))) {
|
|
12362
|
+
return {
|
|
12363
|
+
valid: false,
|
|
12364
|
+
error: {
|
|
12365
|
+
code: "RECEIPT_RECONCILIATION_FAILED",
|
|
12366
|
+
message: `Committed comment id '${id}' was not found in word/comments.xml.`
|
|
12367
|
+
}
|
|
12368
|
+
};
|
|
12369
|
+
}
|
|
12370
|
+
}
|
|
12371
|
+
}
|
|
12372
|
+
if (Array.isArray(receipt.numberingIds)) {
|
|
12373
|
+
for (const id of receipt.numberingIds) {
|
|
12374
|
+
if (!numberingIdSet.has(String(id))) {
|
|
12375
|
+
return {
|
|
12376
|
+
valid: false,
|
|
12377
|
+
error: {
|
|
12378
|
+
code: "RECEIPT_RECONCILIATION_FAILED",
|
|
12379
|
+
message: `Committed numbering id '${id}' was not found in numbering parts or document.`
|
|
12380
|
+
}
|
|
12381
|
+
};
|
|
12382
|
+
}
|
|
12383
|
+
}
|
|
12384
|
+
}
|
|
12385
|
+
if (parts?.relationshipsXml && Array.isArray(receipt.relationshipIds)) {
|
|
12386
|
+
for (const id of receipt.relationshipIds) {
|
|
12387
|
+
if (!relIdSet.has(String(id))) {
|
|
12388
|
+
return {
|
|
12389
|
+
valid: false,
|
|
12390
|
+
error: {
|
|
12391
|
+
code: "RECEIPT_RECONCILIATION_FAILED",
|
|
12392
|
+
message: `Committed relationship id '${id}' was not found in document.xml.rels.`
|
|
12393
|
+
}
|
|
12394
|
+
};
|
|
12395
|
+
}
|
|
12396
|
+
}
|
|
12397
|
+
}
|
|
12398
|
+
}
|
|
12399
|
+
return { valid: true };
|
|
12400
|
+
}
|
|
12401
|
+
|
|
10043
12402
|
// index.js
|
|
10044
12403
|
async function applyRedlineToOxml2(oxml, originalText, modifiedText, options = {}) {
|
|
10045
12404
|
const result = await applyRedlineToOxml(oxml, originalText, modifiedText, options);
|
|
@@ -10185,49 +12544,66 @@ export {
|
|
|
10185
12544
|
DiffOp,
|
|
10186
12545
|
NS_W,
|
|
10187
12546
|
NumberingService,
|
|
12547
|
+
ReceiptCollector,
|
|
10188
12548
|
ReconciliationPipeline,
|
|
10189
12549
|
RoutePlanKind,
|
|
10190
12550
|
RunKind,
|
|
10191
12551
|
WORD_MAIN_NS,
|
|
10192
12552
|
acceptTrackedChangesInOoxml,
|
|
12553
|
+
analyzeStructuredContent,
|
|
12554
|
+
applyCommentReplyToParts,
|
|
10193
12555
|
applyFormattingRemovalToOoxml,
|
|
10194
12556
|
applyHighlightToOoxml,
|
|
10195
12557
|
applyRedlineToOxml2 as applyRedlineToOxml,
|
|
10196
12558
|
applyRedlineToOxmlWithListFallback,
|
|
12559
|
+
areRevisionTokensEqual,
|
|
10197
12560
|
buildCommentElement,
|
|
10198
12561
|
buildCommentsPartXml,
|
|
10199
12562
|
buildExplicitDecimalMultilevelNumberingXml,
|
|
10200
12563
|
buildListMarkdown,
|
|
10201
12564
|
buildReconciliationPlan,
|
|
12565
|
+
buildRevisionTokenFraming,
|
|
10202
12566
|
buildSingleLineListStructuralFallbackPlan,
|
|
10203
12567
|
buildTargetReferenceSnapshot,
|
|
10204
12568
|
clearSingleLineListFallbackExplicitSequence,
|
|
10205
12569
|
collectContiguousListParagraphBlock,
|
|
12570
|
+
computeDocumentPartsRevisionToken,
|
|
12571
|
+
computeRevisionToken,
|
|
12572
|
+
computeRevisionTokenSync,
|
|
10206
12573
|
configureLogger,
|
|
10207
12574
|
configureXmlProvider,
|
|
10208
12575
|
containsTrackedChanges,
|
|
10209
12576
|
createDynamicNumberingIdState,
|
|
12577
|
+
createEmptyReceipt,
|
|
12578
|
+
createParagraphFingerprint,
|
|
10210
12579
|
deleteCommentsByAuthorInOoxml,
|
|
10211
12580
|
enforceListBindingOnParagraphNodes,
|
|
10212
12581
|
ensureCommentsArtifactsInZip,
|
|
12582
|
+
ensureCommentsExtendedArtifactsInZip,
|
|
10213
12583
|
ensureNumberingArtifactsInZip,
|
|
10214
12584
|
escapeXml,
|
|
10215
12585
|
executeSingleLineListStructuralFallback,
|
|
12586
|
+
extractCanonicalParagraphText,
|
|
12587
|
+
extractDocumentPartsEntries,
|
|
10216
12588
|
extractFirstParagraphNumId,
|
|
10217
12589
|
extractParagraphIdFromOoxml,
|
|
12590
|
+
extractParagraphRevisionSegments,
|
|
10218
12591
|
extractReplacementNodesFromOoxml,
|
|
10219
12592
|
findContainingWordElement,
|
|
10220
12593
|
findParagraphByBestTextMatch,
|
|
10221
12594
|
findParagraphByReference,
|
|
10222
12595
|
findParagraphByStrictText,
|
|
12596
|
+
findStrictTargetCandidates,
|
|
10223
12597
|
generateTableOoxml,
|
|
10224
12598
|
getBodyElementFromDocument,
|
|
10225
12599
|
getDefaultAuthor,
|
|
10226
12600
|
getDocumentParagraphNodes,
|
|
10227
12601
|
getPackagePartName,
|
|
12602
|
+
getParagraphId2 as getParagraphId,
|
|
10228
12603
|
getParagraphListInfo,
|
|
10229
12604
|
getParagraphText,
|
|
10230
12605
|
getPlatform,
|
|
12606
|
+
getTrackedChangeAuthors,
|
|
10231
12607
|
hasListItems,
|
|
10232
12608
|
inferNumberingStyleFromMarker,
|
|
10233
12609
|
inferTableReplacementParagraphBlock,
|
|
@@ -10239,12 +12615,15 @@ export {
|
|
|
10239
12615
|
injectCommentsIntoOoxml,
|
|
10240
12616
|
injectCommentsIntoPackage2 as injectCommentsIntoPackage,
|
|
10241
12617
|
insertBodyElementBeforeSectPr,
|
|
12618
|
+
inspectDocumentParts,
|
|
10242
12619
|
isLikelyStructuredTableSourceParagraph,
|
|
10243
12620
|
isMarkdownTableText,
|
|
12621
|
+
isNodeVisibleInRevisionView,
|
|
10244
12622
|
mergeNumberingXmlBySchemaOrder,
|
|
10245
12623
|
normalizeBodySectionOrderStandalone,
|
|
10246
12624
|
normalizeContentEscapesForRouting,
|
|
10247
12625
|
normalizeListItemsWithLevels,
|
|
12626
|
+
normalizeOpcEntryName,
|
|
10248
12627
|
normalizeWhitespaceForTargeting,
|
|
10249
12628
|
overwriteParagraphNumIds,
|
|
10250
12629
|
parseMarkdownListContent,
|
|
@@ -10252,8 +12631,11 @@ export {
|
|
|
10252
12631
|
parseOoxmlSafe,
|
|
10253
12632
|
parseParagraphReference,
|
|
10254
12633
|
planListInsertionOnlyEdit,
|
|
12634
|
+
planStructuredReplacement,
|
|
10255
12635
|
preprocessMarkdown,
|
|
12636
|
+
readCanonicalRunText,
|
|
10256
12637
|
reconcileMarkdownTableOoxml,
|
|
12638
|
+
reconcileReceiptsAgainstOutput,
|
|
10257
12639
|
recordSingleLineListFallbackExplicitSequence,
|
|
10258
12640
|
rejectTrackedChangesInOoxml,
|
|
10259
12641
|
remapNumberingPayloadForDocument,
|
|
@@ -10278,6 +12660,7 @@ export {
|
|
|
10278
12660
|
synthesizeTableMarkdownFromMultilineCellEdit,
|
|
10279
12661
|
validateDocxPackage,
|
|
10280
12662
|
validateRedlineOoxml,
|
|
12663
|
+
validateRevisionToken,
|
|
10281
12664
|
wrapInDocumentFragment
|
|
10282
12665
|
};
|
|
10283
12666
|
//# sourceMappingURL=docx-redline-js.esm.js.map
|