@ansonlai/docx-redline-js 0.9.0 → 0.11.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/ARCHITECTURE.md +691 -669
- package/CHANGELOG.md +67 -1
- package/README.md +39 -8
- package/core/word-xml.js +160 -118
- package/dist/docx-redline-js.esm.js +528 -312
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +94 -94
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/dist/docx-redline.bundle.cjs +528 -312
- package/dist/docx-redline.bundle.cjs.map +4 -4
- package/dist/docx-redline.bundle.js +528 -312
- package/dist/docx-redline.bundle.js.map +4 -4
- package/docs/AGENT_KNOWLEDGE_BASE.md +64 -5
- package/docs/SKILL_AUTHORING.md +5 -1
- package/docs/TESTING.md +10 -1
- package/docs/schemas/document-operations.schema.json +1 -1
- package/engine/oxml-engine.js +748 -736
- package/engine/table-mode.js +7 -4
- package/index.d.ts +29 -2
- package/index.js +3 -0
- package/node/cli.js +993 -992
- package/package.json +1 -1
- package/pipeline/content-analysis.js +2 -1
- package/pipeline/markdown-processor.js +5 -2
- package/pipeline/structured-content.js +2 -1
- package/services/comment-parts-merge.js +124 -0
- package/services/comment-replies.js +1 -1
- package/services/document-operation-applier.js +1 -0
- package/services/document-operation-contract.js +1 -0
- package/services/document-operation-mutations.js +3362 -3346
- package/services/inspection-comparison.js +82 -0
- package/services/operation-heuristics.js +1 -1
- package/services/operation-preflight.js +42 -33
- package/services/revision-fidelity-verifier.js +5 -19
- package/services/standalone-docx-plumbing.js +38 -22
- package/services/table-reconciliation.js +11 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ansonlai/docx-redline-js v0.
|
|
1
|
+
// @ansonlai/docx-redline-js v0.11.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;
|
|
@@ -1663,7 +1663,10 @@ var MARKDOWN_PATTERNS = [
|
|
|
1663
1663
|
{ regex: /\*\*\+\+(.+?)\+\+\*\*/, format: { bold: true, underline: true } },
|
|
1664
1664
|
// Bold: **text** or __text__
|
|
1665
1665
|
{ regex: /\*\*(.+?)\*\*/, format: { bold: true } },
|
|
1666
|
-
|
|
1666
|
+
// Underscore delimiters follow CommonMark flanking rules: not intraword, not part of a
|
|
1667
|
+
// longer underscore run, and never around whitespace. So signature lines ("_____")
|
|
1668
|
+
// and snake_case identifiers stay literal text.
|
|
1669
|
+
{ regex: /(?<![A-Za-z0-9_])__(?=[^_\s])(.+?)(?<=[^_\s])__(?![A-Za-z0-9_])/, format: { bold: true } },
|
|
1667
1670
|
// Underline: ++text++
|
|
1668
1671
|
{ regex: /\+\+(.+?)\+\+/, format: { underline: true } },
|
|
1669
1672
|
// Strikethrough: ~~text~~ or ~text~
|
|
@@ -1671,7 +1674,7 @@ var MARKDOWN_PATTERNS = [
|
|
|
1671
1674
|
{ regex: /~(.+?)~/, format: { strikethrough: true } },
|
|
1672
1675
|
// Italic: *text* or _text_ (using lookahead only for compatibility)
|
|
1673
1676
|
{ regex: /\*(?!\*)(.+?)\*(?!\*)/, format: { italic: true } },
|
|
1674
|
-
{ regex: /_(
|
|
1677
|
+
{ regex: /(?<![A-Za-z0-9_])_(?=[^_\s])(.+?)(?<=[^_\s])_(?![A-Za-z0-9_])/, format: { italic: true } }
|
|
1675
1678
|
];
|
|
1676
1679
|
function preprocessMarkdown(text2) {
|
|
1677
1680
|
if (!text2) {
|
|
@@ -3842,6 +3845,23 @@ function withOoxmlSourceType(result) {
|
|
|
3842
3845
|
}
|
|
3843
3846
|
return { ...result, sourceType: classifyOoxmlSourceType(result.oxml) };
|
|
3844
3847
|
}
|
|
3848
|
+
function hasUnattributedTrackedChange(node) {
|
|
3849
|
+
if (!node) return false;
|
|
3850
|
+
for (const localName4 of TRACKED_CHANGE_NAMES) {
|
|
3851
|
+
for (const change of wordElementsByLocalName(node, localName4)) {
|
|
3852
|
+
const author = change.getAttribute?.("w:author") || change.getAttribute?.("author") || (typeof change.getAttributeNS === "function" ? change.getAttributeNS(NS_W, "author") : null);
|
|
3853
|
+
if (!author || !String(author).trim()) return true;
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
return false;
|
|
3857
|
+
}
|
|
3858
|
+
function resolveExistingRevisionsPolicy(policy, node, author) {
|
|
3859
|
+
if (policy !== "auto") return policy;
|
|
3860
|
+
const nodes = (Array.isArray(node) ? node : [node]).filter(Boolean);
|
|
3861
|
+
const own = String(author || "").trim().toLowerCase();
|
|
3862
|
+
const foreign = nodes.some((item) => containsTrackedChanges(item) && (hasUnattributedTrackedChange(item) || getTrackedChangeAuthors(item).some((name) => name.trim().toLowerCase() !== own)));
|
|
3863
|
+
return foreign ? "slice-cross-author" : "merge-same-author";
|
|
3864
|
+
}
|
|
3845
3865
|
|
|
3846
3866
|
// engine/paragraph-mark-revisions.js
|
|
3847
3867
|
function localNameOf(node) {
|
|
@@ -3948,6 +3968,7 @@ function paragraphMarkPPrXml(kind, ctx = {}) {
|
|
|
3948
3968
|
}
|
|
3949
3969
|
|
|
3950
3970
|
// services/table-reconciliation.js
|
|
3971
|
+
var cellMarkdown = (text2, options) => options?.plainTextContent === true ? { cleanText: String(text2 ?? ""), formatHints: [] } : preprocessMarkdown(text2);
|
|
3951
3972
|
function generateTableOoxml(tableData, options = {}) {
|
|
3952
3973
|
const {
|
|
3953
3974
|
generateRedlines = false,
|
|
@@ -3981,7 +4002,7 @@ function generateTableOoxml(tableData, options = {}) {
|
|
|
3981
4002
|
let cellsXml = "";
|
|
3982
4003
|
for (let c = 0; c < numCols; c++) {
|
|
3983
4004
|
const cellText = row[c] || "";
|
|
3984
|
-
const { cleanText, formatHints } =
|
|
4005
|
+
const { cleanText, formatHints } = cellMarkdown(cellText, options);
|
|
3985
4006
|
const runModel = [{
|
|
3986
4007
|
kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
|
|
3987
4008
|
text: cleanText,
|
|
@@ -4096,7 +4117,7 @@ function serializeVirtualGridToOoxml(grid, operations, options) {
|
|
|
4096
4117
|
let cellsXml = "";
|
|
4097
4118
|
const rowInsertMeta = generateRedlines ? createRevisionMetadata(author, revisionIdAllocator) : null;
|
|
4098
4119
|
for (const cellText of op.cells) {
|
|
4099
|
-
const { cleanText, formatHints } =
|
|
4120
|
+
const { cleanText, formatHints } = cellMarkdown(cellText, options);
|
|
4100
4121
|
const runModel = [{
|
|
4101
4122
|
kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
|
|
4102
4123
|
text: cleanText,
|
|
@@ -4170,7 +4191,7 @@ function reconcileMultiBlockCellContent(cell, newText, options) {
|
|
|
4170
4191
|
}
|
|
4171
4192
|
return;
|
|
4172
4193
|
}
|
|
4173
|
-
const { cleanText, formatHints } =
|
|
4194
|
+
const { cleanText, formatHints } = cellMarkdown(line, options);
|
|
4174
4195
|
const diffOps = computeWordLevelDiffOps(block.acceptedText, cleanText);
|
|
4175
4196
|
const model = applyPatches(splitRunsAtDiffBoundaries(block.runModel, diffOps), diffOps, {
|
|
4176
4197
|
generateRedlines,
|
|
@@ -4180,7 +4201,7 @@ function reconcileMultiBlockCellContent(cell, newText, options) {
|
|
|
4180
4201
|
out += serializeToOoxml(model, block.pPr, formatHints, { author, generateRedlines, revisionIdAllocator });
|
|
4181
4202
|
});
|
|
4182
4203
|
for (let i2 = cell.blocks.length; i2 < lines.length; i2++) {
|
|
4183
|
-
const { cleanText, formatHints } =
|
|
4204
|
+
const { cleanText, formatHints } = cellMarkdown(lines[i2], options);
|
|
4184
4205
|
const runModel = [{
|
|
4185
4206
|
kind: generateRedlines ? RunKind.INSERTION : RunKind.TEXT,
|
|
4186
4207
|
text: cleanText,
|
|
@@ -4199,7 +4220,7 @@ function reconcileCellContent(cell, newText, options) {
|
|
|
4199
4220
|
return reconcileMultiBlockCellContent(cell, newText, options);
|
|
4200
4221
|
}
|
|
4201
4222
|
const { generateRedlines, author, revisionIdAllocator = null } = options;
|
|
4202
|
-
const { cleanText, formatHints } =
|
|
4223
|
+
const { cleanText, formatHints } = cellMarkdown(newText, options);
|
|
4203
4224
|
const oldText = cell.getText();
|
|
4204
4225
|
const diffOps = computeWordLevelDiffOps(oldText, cleanText);
|
|
4205
4226
|
const baseBlock = cell.blocks[0] || { runModel: [], pPr: null };
|
|
@@ -4245,7 +4266,7 @@ function parseTable(text2) {
|
|
|
4245
4266
|
}
|
|
4246
4267
|
const isSeparatorLine = (line) => {
|
|
4247
4268
|
const normalized = line.replace(/\s+/g, "");
|
|
4248
|
-
return /^\|:?-{3,}:?(\|:?-{3,}:?)
|
|
4269
|
+
return /^\|:?-{3,}:?(?:(\|:?-{3,}:?)+\|?|\|)$/.test(normalized);
|
|
4249
4270
|
};
|
|
4250
4271
|
const hasHeader = lines.some(isSeparatorLine);
|
|
4251
4272
|
const dataLines = lines.filter((line) => !isSeparatorLine(line));
|
|
@@ -4829,7 +4850,7 @@ var ReconciliationPipeline = class {
|
|
|
4829
4850
|
};
|
|
4830
4851
|
|
|
4831
4852
|
// pipeline/structured-content.js
|
|
4832
|
-
var TABLE_SEPARATOR = /^\s
|
|
4853
|
+
var TABLE_SEPARATOR = /^\s*(?:\|\s*:?-{3,}:?\s*\||\|?\s*:?-{3,}:?\s*(?:\|\s*:?-{3,}:?\s*)+\|?)\s*$/;
|
|
4833
4854
|
function isBlank(line) {
|
|
4834
4855
|
return !line || line.trim().length === 0;
|
|
4835
4856
|
}
|
|
@@ -8486,7 +8507,7 @@ function keepUnchangedTextOutsideWrappers(diffs, mapping) {
|
|
|
8486
8507
|
function noChanges(serializer, xmlDoc) {
|
|
8487
8508
|
return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: false });
|
|
8488
8509
|
}
|
|
8489
|
-
function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines = true) {
|
|
8510
|
+
function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines = true, extra = {}) {
|
|
8490
8511
|
const tableNodes = getElementsByTagNSOrTag(xmlDoc, NS_W, "tbl");
|
|
8491
8512
|
const newTableData = parseTable(modifiedText);
|
|
8492
8513
|
const hasNewContent = newTableData.rows.length > 0 || newTableData.headers.length > 0;
|
|
@@ -8502,7 +8523,8 @@ function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, auth
|
|
|
8502
8523
|
const options = {
|
|
8503
8524
|
generateRedlines,
|
|
8504
8525
|
author,
|
|
8505
|
-
revisionIdAllocator: getRevisionIdAllocatorForDocument(xmlDoc)
|
|
8526
|
+
revisionIdAllocator: getRevisionIdAllocatorForDocument(xmlDoc),
|
|
8527
|
+
plainTextContent: extra.plainTextContent === true
|
|
8506
8528
|
};
|
|
8507
8529
|
const reconciledOxml = serializeVirtualGridToOoxml(oldGrid, operations, options);
|
|
8508
8530
|
const wrappedOxml = `<root xmlns:w="${NS_W}">${reconciledOxml}</root>`;
|
|
@@ -8522,14 +8544,14 @@ function applyTableReconciliation(xmlDoc, modifiedText, serializer, parser, auth
|
|
|
8522
8544
|
targetTable.parentNode.replaceChild(importedTable, targetTable);
|
|
8523
8545
|
return withOoxmlSourceType({ oxml: serializer.serializeToString(xmlDoc), hasChanges: true });
|
|
8524
8546
|
}
|
|
8525
|
-
function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines) {
|
|
8547
|
+
function applyTextToTableTransformation(xmlDoc, modifiedText, serializer, parser, author, generateRedlines, extra = {}) {
|
|
8526
8548
|
const revisionIdAllocator = getRevisionIdAllocatorForDocument(xmlDoc);
|
|
8527
8549
|
const tableData = parseTable(modifiedText);
|
|
8528
8550
|
if (!tableData || tableData.rows.length === 0 && tableData.headers.length === 0) {
|
|
8529
8551
|
log("[OxmlEngine] Failed to parse table data from Markdown");
|
|
8530
8552
|
return noChanges(serializer, xmlDoc);
|
|
8531
8553
|
}
|
|
8532
|
-
const tableOoxml = generateTableOoxml(tableData, { generateRedlines, author, revisionIdAllocator, trackAsBlock: true });
|
|
8554
|
+
const tableOoxml = generateTableOoxml(tableData, { generateRedlines, author, revisionIdAllocator, trackAsBlock: true, plainTextContent: extra.plainTextContent === true });
|
|
8533
8555
|
const tableDoc = parseOoxmlSafe(`<root xmlns:w="${NS_W}">${tableOoxml}</root>`, "application/xml").doc;
|
|
8534
8556
|
if (!tableDoc) return noChanges(serializer, xmlDoc);
|
|
8535
8557
|
const tableParseError = getXmlParseError(tableDoc);
|
|
@@ -9352,7 +9374,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
9352
9374
|
const operationWarnings = [];
|
|
9353
9375
|
let normalizedExistingRevisions = false;
|
|
9354
9376
|
let preservedInsertedParagraphMark = null;
|
|
9355
|
-
|
|
9377
|
+
let existingRevisionsPolicy = options.existingRevisions || "merge-same-author";
|
|
9356
9378
|
const keepNormalizedNoOp = existingRevisionsPolicy === "accept-all-first-keep-normalized";
|
|
9357
9379
|
const finalize = (result) => {
|
|
9358
9380
|
const withStatus = { ...result };
|
|
@@ -9400,6 +9422,10 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
9400
9422
|
const parsed = parseOoxmlSafe(inputOoxml, "text/xml");
|
|
9401
9423
|
parseWarnings = parsed.warnings;
|
|
9402
9424
|
let xmlDoc = parsed.doc;
|
|
9425
|
+
if (existingRevisionsPolicy === "auto") {
|
|
9426
|
+
existingRevisionsPolicy = resolveExistingRevisionsPolicy("auto", xmlDoc, author);
|
|
9427
|
+
options = { ...options, existingRevisions: existingRevisionsPolicy };
|
|
9428
|
+
}
|
|
9403
9429
|
const parseError = xmlDoc ? getXmlParseError(xmlDoc) : null;
|
|
9404
9430
|
if (parsed.error || parseError) {
|
|
9405
9431
|
const message = parsed.error?.message || parseError?.textContent || "Could not parse OOXML input.";
|
|
@@ -9434,7 +9460,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
9434
9460
|
if (existingRevisionsPolicy === "merge-same-author" || existingRevisionsPolicy === "slice-cross-author") {
|
|
9435
9461
|
const authors = getTrackedChangeAuthors(xmlDoc);
|
|
9436
9462
|
const currentAuthor = String(author || "").trim().toLowerCase();
|
|
9437
|
-
const isSameAuthor = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === currentAuthor);
|
|
9463
|
+
const isSameAuthor = authors.length > 0 && !hasUnattributedTrackedChange(xmlDoc) && authors.every((a) => a.trim().toLowerCase() === currentAuthor);
|
|
9438
9464
|
if (isSameAuthor) {
|
|
9439
9465
|
const commentIds = getCommentIdsInOoxml(xmlDoc);
|
|
9440
9466
|
if (commentIds.length > 0) {
|
|
@@ -9591,7 +9617,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
9591
9617
|
sanitizedText = structuredAnalysis.normalizedMarkdown;
|
|
9592
9618
|
}
|
|
9593
9619
|
}
|
|
9594
|
-
const { cleanText: cleanModifiedText, formatHints } = preprocessMarkdown(sanitizedText);
|
|
9620
|
+
const { cleanText: cleanModifiedText, formatHints } = options.plainTextContent === true ? { cleanText: sanitizedText, formatHints: [] } : preprocessMarkdown(sanitizedText);
|
|
9595
9621
|
const { existingFormatHints, textSpans, paragraphs } = extractFormattingFromOoxml(xmlDoc);
|
|
9596
9622
|
const hasLiteralTextChanges = existingRevisionsPolicy === "slice-cross-author" ? cleanModifiedText !== originalText : cleanModifiedText.trim() !== originalText.trim();
|
|
9597
9623
|
const hasListStructureChange = !hasLiteralTextChanges && options.explicitStructuredContent === true && isListTargetLoose(cleanModifiedText) && !sourceMatchesListShape(paragraphs, cleanModifiedText);
|
|
@@ -9724,11 +9750,11 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
9724
9750
|
if (isMarkdownTable && !hasTables) {
|
|
9725
9751
|
recordRouteSelection(options, "table", { transformation: "text-to-table" });
|
|
9726
9752
|
log("[OxmlEngine] Text-to-table transformation: generating new table from Markdown");
|
|
9727
|
-
return finalize(applyTextToTableTransformation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines));
|
|
9753
|
+
return finalize(applyTextToTableTransformation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines, { plainTextContent: options.plainTextContent === true }));
|
|
9728
9754
|
}
|
|
9729
9755
|
if (hasTables && isMarkdownTable) {
|
|
9730
9756
|
recordRouteSelection(options, "table", { transformation: "table-reconciliation" });
|
|
9731
|
-
return finalize(applyTableReconciliation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines));
|
|
9757
|
+
return finalize(applyTableReconciliation(xmlDoc, cleanModifiedText, serializer, null, author, generateRedlines, { plainTextContent: options.plainTextContent === true }));
|
|
9732
9758
|
}
|
|
9733
9759
|
if (usesSurgicalTextMode) {
|
|
9734
9760
|
recordRouteSelection(options, "surgical", { tableScoped: hasTables });
|
|
@@ -13423,6 +13449,70 @@ function inspectDocumentParts(parts, options = {}) {
|
|
|
13423
13449
|
};
|
|
13424
13450
|
}
|
|
13425
13451
|
|
|
13452
|
+
// services/inspection-comparison.js
|
|
13453
|
+
var commentIdentity = (comment) => [comment.author ?? null, comment.text ?? "", comment.anchoredText || ""];
|
|
13454
|
+
function reviewSegments(segments = []) {
|
|
13455
|
+
return segments.filter((segment) => segment.kind !== "baseline").reduce((result, segment) => {
|
|
13456
|
+
const next = {
|
|
13457
|
+
kind: segment.kind,
|
|
13458
|
+
author: segment.author || null,
|
|
13459
|
+
text: segment.text,
|
|
13460
|
+
acceptedStart: segment.acceptedStart ?? null,
|
|
13461
|
+
rejectedStart: segment.rejectedStart ?? null
|
|
13462
|
+
};
|
|
13463
|
+
const previous = result.at(-1);
|
|
13464
|
+
const adjacent = (key) => previous[key] == null && next[key] == null || previous[key] != null && previous[key] + previous.text.length === next[key];
|
|
13465
|
+
if (previous && previous.kind === next.kind && previous.author === next.author && adjacent("acceptedStart") && adjacent("rejectedStart")) previous.text += next.text;
|
|
13466
|
+
else result.push(next);
|
|
13467
|
+
return result;
|
|
13468
|
+
}, []);
|
|
13469
|
+
}
|
|
13470
|
+
function inspectionReviewShape(inspection) {
|
|
13471
|
+
const comments = inspection?.comments || [];
|
|
13472
|
+
return {
|
|
13473
|
+
paragraphs: (inspection?.paragraphs || []).map((paragraph) => ({
|
|
13474
|
+
text: paragraph.exactText,
|
|
13475
|
+
inTable: paragraph.inTable === true,
|
|
13476
|
+
table: paragraph.table ? [paragraph.table.rowIndex, paragraph.table.cellIndex] : null,
|
|
13477
|
+
list: paragraph.list ? [paragraph.list.format, paragraph.list.level, paragraph.list.label] : null,
|
|
13478
|
+
review: {
|
|
13479
|
+
present: paragraph.hasRevisions === true,
|
|
13480
|
+
authors: [...paragraph.revisionAuthors || []].sort(),
|
|
13481
|
+
segments: reviewSegments(paragraph.segments)
|
|
13482
|
+
}
|
|
13483
|
+
})),
|
|
13484
|
+
comments: comments.map((comment) => {
|
|
13485
|
+
const parent = comment.parentCommentId != null ? comments.find((item) => String(item.id) === String(comment.parentCommentId)) : null;
|
|
13486
|
+
return [
|
|
13487
|
+
...commentIdentity(comment),
|
|
13488
|
+
comment.done === true,
|
|
13489
|
+
parent ? commentIdentity(parent) : comment.parentCommentId != null || comment.parentParaId ? "unresolved parent" : null
|
|
13490
|
+
];
|
|
13491
|
+
}).map((item) => JSON.stringify(item)).sort()
|
|
13492
|
+
};
|
|
13493
|
+
}
|
|
13494
|
+
function compareDocumentInspections(expected, actual) {
|
|
13495
|
+
const left = inspectionReviewShape(expected);
|
|
13496
|
+
const right = inspectionReviewShape(actual);
|
|
13497
|
+
const differences = [];
|
|
13498
|
+
const note = (path, a, b) => {
|
|
13499
|
+
if (differences.length < 10 && JSON.stringify(a) !== JSON.stringify(b)) differences.push({ path, expected: a, actual: b });
|
|
13500
|
+
};
|
|
13501
|
+
note("paragraphs.length", left.paragraphs.length, right.paragraphs.length);
|
|
13502
|
+
const count = Math.max(left.paragraphs.length, right.paragraphs.length);
|
|
13503
|
+
for (let index = 0; index < count; index++) {
|
|
13504
|
+
const a = left.paragraphs[index] ?? null;
|
|
13505
|
+
const b = right.paragraphs[index] ?? null;
|
|
13506
|
+
if (!a || !b) {
|
|
13507
|
+
note(`paragraphs[${index}]`, a, b);
|
|
13508
|
+
continue;
|
|
13509
|
+
}
|
|
13510
|
+
for (const key of ["text", "inTable", "table", "list", "review"]) note(`paragraphs[${index}].${key}`, a[key], b[key]);
|
|
13511
|
+
}
|
|
13512
|
+
note("comments", left.comments, right.comments);
|
|
13513
|
+
return { matches: differences.length === 0, differences };
|
|
13514
|
+
}
|
|
13515
|
+
|
|
13426
13516
|
// services/comment-locator.js
|
|
13427
13517
|
function createParagraphTextIndex(paragraph, options = {}) {
|
|
13428
13518
|
const revisionView = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
|
|
@@ -13897,7 +13987,7 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, doc
|
|
|
13897
13987
|
extendedDoc = parsed.doc;
|
|
13898
13988
|
}
|
|
13899
13989
|
const occupied = usedParaIds(commentsDoc, extendedDoc);
|
|
13900
|
-
if (documentXml) for (const match of String(documentXml).matchAll(
|
|
13990
|
+
if (documentXml) for (const match of String(documentXml).matchAll(/\bw14:paraId="([0-9A-Fa-f]{8})"/g)) occupied.add(match[1].toUpperCase());
|
|
13901
13991
|
const parentParagraph = commentThreadParagraph(parent);
|
|
13902
13992
|
if (!parentParagraph) return { status: "error", error: { code: "PARENT_COMMENT_INVALID", message: `Parent comment '${parentCommentId}' has no paragraph.` } };
|
|
13903
13993
|
let parentParaId = commentParaId(parent);
|
|
@@ -13960,232 +14050,6 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, doc
|
|
|
13960
14050
|
};
|
|
13961
14051
|
}
|
|
13962
14052
|
|
|
13963
|
-
// engine/formatting-removal.js
|
|
13964
|
-
function removeNode2(node) {
|
|
13965
|
-
if (node?.parentNode) {
|
|
13966
|
-
node.parentNode.removeChild(node);
|
|
13967
|
-
}
|
|
13968
|
-
}
|
|
13969
|
-
function removeFormattingFromRPr(rPr, formatTypes = ["all"]) {
|
|
13970
|
-
if (!rPr) return null;
|
|
13971
|
-
const rPrClone = rPr.cloneNode(true);
|
|
13972
|
-
if (formatTypes.includes("all")) {
|
|
13973
|
-
const toRemove = [
|
|
13974
|
-
"w:b",
|
|
13975
|
-
"w:i",
|
|
13976
|
-
"w:u",
|
|
13977
|
-
"w:strike",
|
|
13978
|
-
"w:dstrike",
|
|
13979
|
-
"w:color",
|
|
13980
|
-
"w:sz",
|
|
13981
|
-
"w:szCs",
|
|
13982
|
-
"w:rFonts",
|
|
13983
|
-
"w:highlight",
|
|
13984
|
-
"w:vertAlign",
|
|
13985
|
-
"w:spacing",
|
|
13986
|
-
"w:w",
|
|
13987
|
-
"w:kern",
|
|
13988
|
-
"w:position"
|
|
13989
|
-
];
|
|
13990
|
-
toRemove.forEach((tag) => {
|
|
13991
|
-
const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace("w:", "")}`);
|
|
13992
|
-
elements.forEach(removeNode2);
|
|
13993
|
-
});
|
|
13994
|
-
} else {
|
|
13995
|
-
const tagMap = {
|
|
13996
|
-
"bold": "w:b",
|
|
13997
|
-
"italic": "w:i",
|
|
13998
|
-
"underline": "w:u",
|
|
13999
|
-
"strikethrough": "w:strike",
|
|
14000
|
-
"doubleStrike": "w:dstrike",
|
|
14001
|
-
"color": "w:color",
|
|
14002
|
-
"highlight": "w:highlight",
|
|
14003
|
-
"fontSize": "w:sz",
|
|
14004
|
-
"fontSizeCs": "w:szCs",
|
|
14005
|
-
// Complex script font size
|
|
14006
|
-
"fontFamily": "w:rFonts",
|
|
14007
|
-
"superscript": "w:vertAlign",
|
|
14008
|
-
"subscript": "w:vertAlign"
|
|
14009
|
-
};
|
|
14010
|
-
formatTypes.forEach((type) => {
|
|
14011
|
-
const tag = tagMap[type];
|
|
14012
|
-
if (tag) {
|
|
14013
|
-
const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace("w:", "")}`);
|
|
14014
|
-
elements.forEach(removeNode2);
|
|
14015
|
-
}
|
|
14016
|
-
});
|
|
14017
|
-
}
|
|
14018
|
-
return rPrClone.children.length > 0 ? rPrClone : null;
|
|
14019
|
-
}
|
|
14020
|
-
function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
|
|
14021
|
-
if (!targetText || !ooxmlString) return ooxmlString;
|
|
14022
|
-
const doc = parseOoxml(ooxmlString);
|
|
14023
|
-
if (!doc) return ooxmlString;
|
|
14024
|
-
const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
14025
|
-
const runs = doc.getElementsByTagNameNS(NS_W11, "r");
|
|
14026
|
-
const insertions = doc.getElementsByTagNameNS(NS_W11, "ins");
|
|
14027
|
-
const allRuns = [...Array.from(runs)];
|
|
14028
|
-
for (const ins of insertions) {
|
|
14029
|
-
const insideRuns = ins.getElementsByTagNameNS(NS_W11, "r");
|
|
14030
|
-
allRuns.push(...Array.from(insideRuns));
|
|
14031
|
-
}
|
|
14032
|
-
for (const run of allRuns) {
|
|
14033
|
-
const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
|
|
14034
|
-
const runText = Array.from(textNodes).map((t) => t.textContent).join("");
|
|
14035
|
-
if (runText.includes(targetText) || runText === targetText) {
|
|
14036
|
-
const rPrElements = run.getElementsByTagNameNS(NS_W11, "rPr");
|
|
14037
|
-
if (rPrElements.length > 0) {
|
|
14038
|
-
const rPr = rPrElements[0];
|
|
14039
|
-
const newRPr = removeFormattingFromRPr(rPr, formatTypes);
|
|
14040
|
-
if (newRPr) {
|
|
14041
|
-
if (rPr.parentNode) {
|
|
14042
|
-
rPr.parentNode.replaceChild(newRPr, rPr);
|
|
14043
|
-
}
|
|
14044
|
-
} else {
|
|
14045
|
-
removeNode2(rPr);
|
|
14046
|
-
}
|
|
14047
|
-
}
|
|
14048
|
-
}
|
|
14049
|
-
}
|
|
14050
|
-
return serializeOoxml(doc);
|
|
14051
|
-
}
|
|
14052
|
-
var HIGHLIGHT_COLOR_MAP = {
|
|
14053
|
-
"yellow": "yellow",
|
|
14054
|
-
"green": "green",
|
|
14055
|
-
"cyan": "cyan",
|
|
14056
|
-
"magenta": "magenta",
|
|
14057
|
-
"blue": "blue",
|
|
14058
|
-
"red": "red",
|
|
14059
|
-
"darkblue": "darkBlue",
|
|
14060
|
-
"darkcyan": "darkCyan",
|
|
14061
|
-
"darkgreen": "darkGreen",
|
|
14062
|
-
"darkmagenta": "darkMagenta",
|
|
14063
|
-
"darkred": "darkRed",
|
|
14064
|
-
"darkyellow": "darkYellow",
|
|
14065
|
-
"gray25": "lightGray",
|
|
14066
|
-
"gray50": "darkGray",
|
|
14067
|
-
"black": "black",
|
|
14068
|
-
"white": "white"
|
|
14069
|
-
};
|
|
14070
|
-
function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
|
|
14071
|
-
const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
14072
|
-
const ooxmlColor = HIGHLIGHT_COLOR_MAP[color.toLowerCase()] || "yellow";
|
|
14073
|
-
const generateRedlines = options?.generateRedlines ?? false;
|
|
14074
|
-
const author = options?.author || getDefaultAuthor();
|
|
14075
|
-
let rPrElement = rPr;
|
|
14076
|
-
if (!rPrElement) {
|
|
14077
|
-
rPrElement = createWordElement(doc, "w:rPr");
|
|
14078
|
-
} else {
|
|
14079
|
-
rPrElement = rPr.cloneNode(true);
|
|
14080
|
-
}
|
|
14081
|
-
let previousRPrState = null;
|
|
14082
|
-
if (generateRedlines) {
|
|
14083
|
-
previousRPrState = createWordElement(doc, "w:rPr");
|
|
14084
|
-
Array.from(rPrElement.childNodes).forEach((child) => {
|
|
14085
|
-
if (child.nodeName !== "w:rPrChange") {
|
|
14086
|
-
previousRPrState.appendChild(child.cloneNode(true));
|
|
14087
|
-
}
|
|
14088
|
-
});
|
|
14089
|
-
}
|
|
14090
|
-
const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W11, "highlight");
|
|
14091
|
-
Array.from(existingHighlight).forEach(removeNode2);
|
|
14092
|
-
const highlightEl = createWordElement(doc, "w:highlight");
|
|
14093
|
-
highlightEl.setAttributeNS(NS_W11, "w:val", ooxmlColor);
|
|
14094
|
-
rPrElement.appendChild(highlightEl);
|
|
14095
|
-
if (generateRedlines && previousRPrState) {
|
|
14096
|
-
const rPrChange = createWordElement(doc, "w:rPrChange");
|
|
14097
|
-
const metadata = createRevisionMetadata(author, doc);
|
|
14098
|
-
rPrChange.setAttribute("w:id", String(metadata.id));
|
|
14099
|
-
rPrChange.setAttribute("w:author", metadata.author);
|
|
14100
|
-
rPrChange.setAttribute("w:date", metadata.date);
|
|
14101
|
-
rPrChange.appendChild(previousRPrState);
|
|
14102
|
-
const existingChange = rPrElement.getElementsByTagNameNS(NS_W11, "rPrChange");
|
|
14103
|
-
Array.from(existingChange).forEach(removeNode2);
|
|
14104
|
-
rPrElement.appendChild(rPrChange);
|
|
14105
|
-
}
|
|
14106
|
-
return rPrElement;
|
|
14107
|
-
}
|
|
14108
|
-
function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", options = {}) {
|
|
14109
|
-
if (!targetText || !ooxmlString) return ooxmlString;
|
|
14110
|
-
const doc = parseOoxml(ooxmlString);
|
|
14111
|
-
if (!doc) return ooxmlString;
|
|
14112
|
-
let revisionIdAllocator;
|
|
14113
|
-
if (options?._revisionIdAllocator instanceof RevisionIdAllocator) {
|
|
14114
|
-
revisionIdAllocator = options._revisionIdAllocator;
|
|
14115
|
-
seedRevisionIdsFromDocument(doc, revisionIdAllocator);
|
|
14116
|
-
} else {
|
|
14117
|
-
revisionIdAllocator = createRevisionIdAllocator(doc);
|
|
14118
|
-
}
|
|
14119
|
-
const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
14120
|
-
const getRunText = (run) => {
|
|
14121
|
-
const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
|
|
14122
|
-
return Array.from(textNodes).map((t) => t.textContent).join("");
|
|
14123
|
-
};
|
|
14124
|
-
const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W11, "r"));
|
|
14125
|
-
const sourceRunsWithClaimedRevisionIds = /* @__PURE__ */ new WeakSet();
|
|
14126
|
-
const cloneRunWithText2 = (sourceRun, text2, shouldHighlight) => {
|
|
14127
|
-
const clonedRun = sourceRun.cloneNode(true);
|
|
14128
|
-
if (sourceRunsWithClaimedRevisionIds.has(sourceRun)) {
|
|
14129
|
-
refreshRunPropertyChangeIds(clonedRun, revisionIdAllocator);
|
|
14130
|
-
} else {
|
|
14131
|
-
sourceRunsWithClaimedRevisionIds.add(sourceRun);
|
|
14132
|
-
}
|
|
14133
|
-
const textNodes = clonedRun.getElementsByTagNameNS(NS_W11, "t");
|
|
14134
|
-
Array.from(textNodes).forEach(removeNode2);
|
|
14135
|
-
const newText = createWordElement(doc, "w:t");
|
|
14136
|
-
newText.setAttribute("xml:space", "preserve");
|
|
14137
|
-
newText.textContent = text2;
|
|
14138
|
-
clonedRun.appendChild(newText);
|
|
14139
|
-
if (shouldHighlight) {
|
|
14140
|
-
const rPrElements = clonedRun.getElementsByTagNameNS(NS_W11, "rPr");
|
|
14141
|
-
const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
|
|
14142
|
-
const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
|
|
14143
|
-
if (existingRPr) {
|
|
14144
|
-
clonedRun.replaceChild(newRPr, existingRPr);
|
|
14145
|
-
} else {
|
|
14146
|
-
clonedRun.insertBefore(newRPr, clonedRun.firstChild);
|
|
14147
|
-
}
|
|
14148
|
-
}
|
|
14149
|
-
return clonedRun;
|
|
14150
|
-
};
|
|
14151
|
-
for (const run of allRuns) {
|
|
14152
|
-
const runText = getRunText(run);
|
|
14153
|
-
if (!runText) continue;
|
|
14154
|
-
const matchIndexes = [];
|
|
14155
|
-
let searchOffset = 0;
|
|
14156
|
-
while (searchOffset <= runText.length - targetText.length) {
|
|
14157
|
-
const matchIndex = runText.indexOf(targetText, searchOffset);
|
|
14158
|
-
if (matchIndex === -1) break;
|
|
14159
|
-
matchIndexes.push(matchIndex);
|
|
14160
|
-
searchOffset = matchIndex + targetText.length;
|
|
14161
|
-
}
|
|
14162
|
-
if (matchIndexes.length === 0) continue;
|
|
14163
|
-
const parent = run.parentNode;
|
|
14164
|
-
if (!parent) {
|
|
14165
|
-
console.warn("[Highlight] Run parent is null; skipping. Likely already processed.");
|
|
14166
|
-
continue;
|
|
14167
|
-
}
|
|
14168
|
-
const fragment = doc.createDocumentFragment();
|
|
14169
|
-
let cursor = 0;
|
|
14170
|
-
for (const matchIndex of matchIndexes) {
|
|
14171
|
-
if (matchIndex > cursor) {
|
|
14172
|
-
fragment.appendChild(cloneRunWithText2(run, runText.slice(cursor, matchIndex), false));
|
|
14173
|
-
}
|
|
14174
|
-
fragment.appendChild(cloneRunWithText2(
|
|
14175
|
-
run,
|
|
14176
|
-
runText.slice(matchIndex, matchIndex + targetText.length),
|
|
14177
|
-
true
|
|
14178
|
-
));
|
|
14179
|
-
cursor = matchIndex + targetText.length;
|
|
14180
|
-
}
|
|
14181
|
-
if (cursor < runText.length) {
|
|
14182
|
-
fragment.appendChild(cloneRunWithText2(run, runText.slice(cursor), false));
|
|
14183
|
-
}
|
|
14184
|
-
parent.replaceChild(fragment, run);
|
|
14185
|
-
}
|
|
14186
|
-
return serializeOoxml(doc);
|
|
14187
|
-
}
|
|
14188
|
-
|
|
14189
14053
|
// services/package-parts.js
|
|
14190
14054
|
var WORD_ML = "application/vnd.openxmlformats-officedocument.wordprocessingml";
|
|
14191
14055
|
var REL_OFFICE_DOC = "http://schemas.openxmlformats.org/officeDocument/2006/relationships";
|
|
@@ -14473,31 +14337,34 @@ async function ensureNumberingArtifactsInZip(zip, numberingXmlList, options = {}
|
|
|
14473
14337
|
}
|
|
14474
14338
|
}
|
|
14475
14339
|
}
|
|
14340
|
+
function mergeCommentsPartXml(existingXml, commentsXml, options = {}) {
|
|
14341
|
+
if (!existingXml || options.replaceExisting === true) return commentsXml;
|
|
14342
|
+
const existingDoc = parseXmlStrictStandalone(existingXml, "word/comments.xml (existing)");
|
|
14343
|
+
const incomingDoc = parseXmlStrictStandalone(commentsXml, "word/comments.xml (incoming)");
|
|
14344
|
+
const incomingComments = Array.from(incomingDoc.documentElement.getElementsByTagNameNS(NS_W7, "comment"));
|
|
14345
|
+
if (incomingComments.length === 0) return existingXml;
|
|
14346
|
+
const existingRoot = existingDoc.documentElement;
|
|
14347
|
+
const usedIds = new Set(
|
|
14348
|
+
Array.from(existingRoot.getElementsByTagNameNS(NS_W7, "comment")).map((comment) => comment.getAttribute("w:id") || comment.getAttribute("id")).filter(Boolean)
|
|
14349
|
+
);
|
|
14350
|
+
for (const incomingComment of incomingComments) {
|
|
14351
|
+
const id = incomingComment.getAttribute("w:id") || incomingComment.getAttribute("id");
|
|
14352
|
+
if (id && usedIds.has(id)) {
|
|
14353
|
+
throw Object.assign(new Error(`Duplicate comment id: ${id}`), { code: "DUPLICATE_COMMENT_ID", commentId: id });
|
|
14354
|
+
}
|
|
14355
|
+
if (id) usedIds.add(id);
|
|
14356
|
+
existingRoot.appendChild(existingDoc.importNode(incomingComment, true));
|
|
14357
|
+
}
|
|
14358
|
+
return createSerializer().serializeToString(existingDoc);
|
|
14359
|
+
}
|
|
14476
14360
|
async function ensureCommentsArtifactsInZip(zip, commentsXml, options = {}) {
|
|
14477
14361
|
const onInfo = typeof options?.onInfo === "function" ? options.onInfo : () => {
|
|
14478
14362
|
};
|
|
14479
14363
|
if (!commentsXml) return;
|
|
14480
14364
|
const serializer = createSerializer();
|
|
14481
14365
|
const existingText = await readZipText(zip, COMMENTS_PATH);
|
|
14482
|
-
if (!existingText || options.replaceExisting === true)
|
|
14483
|
-
|
|
14484
|
-
zip.file(COMMENTS_PATH, commentsXml);
|
|
14485
|
-
} else {
|
|
14486
|
-
const existingDoc = parseXmlStrictStandalone(existingText, "word/comments.xml (existing)");
|
|
14487
|
-
const incomingDoc = parseXmlStrictStandalone(commentsXml, "word/comments.xml (incoming)");
|
|
14488
|
-
const existingRoot = existingDoc.documentElement;
|
|
14489
|
-
const existingIds = new Set(
|
|
14490
|
-
Array.from(existingRoot.getElementsByTagNameNS(NS_W7, "comment")).map((comment) => comment.getAttribute("w:id") || comment.getAttribute("id")).filter(Boolean)
|
|
14491
|
-
);
|
|
14492
|
-
for (const incomingComment of Array.from(incomingDoc.documentElement.getElementsByTagNameNS(NS_W7, "comment"))) {
|
|
14493
|
-
const id = incomingComment.getAttribute("w:id") || incomingComment.getAttribute("id");
|
|
14494
|
-
if (id && existingIds.has(id)) {
|
|
14495
|
-
throw new Error(`Duplicate comment id: ${id}`);
|
|
14496
|
-
}
|
|
14497
|
-
existingRoot.appendChild(existingDoc.importNode(incomingComment, true));
|
|
14498
|
-
}
|
|
14499
|
-
zip.file(COMMENTS_PATH, serializer.serializeToString(existingDoc));
|
|
14500
|
-
}
|
|
14366
|
+
if (!existingText || options.replaceExisting === true) onInfo("[Demo] Adding comments.xml");
|
|
14367
|
+
zip.file(COMMENTS_PATH, mergeCommentsPartXml(existingText, commentsXml, options));
|
|
14501
14368
|
const ctText = await readZipText(zip, CONTENT_TYPES_PATH);
|
|
14502
14369
|
if (ctText) {
|
|
14503
14370
|
const ctDoc = parseXmlStrictStandalone(ctText, CONTENT_TYPES_PATH);
|
|
@@ -14733,6 +14600,318 @@ async function validateDocxPackage(zip) {
|
|
|
14733
14600
|
}
|
|
14734
14601
|
}
|
|
14735
14602
|
|
|
14603
|
+
// services/comment-parts-merge.js
|
|
14604
|
+
var PART_KEYS = Object.freeze({
|
|
14605
|
+
comments: "commentsXml",
|
|
14606
|
+
commentsExtended: "commentsExtendedXml",
|
|
14607
|
+
commentsIds: "commentsIdsXml",
|
|
14608
|
+
commentsExtensible: "commentsExtensibleXml"
|
|
14609
|
+
});
|
|
14610
|
+
var partsOf = (parts) => Object.fromEntries(Object.values(PART_KEYS).map((key) => [key, parts?.[key] || null]));
|
|
14611
|
+
function mergeCommentParts(parts = {}, output = {}) {
|
|
14612
|
+
const before = partsOf(parts);
|
|
14613
|
+
const after = { ...before };
|
|
14614
|
+
const fail = (error2) => ({ status: "error", hasChanges: false, parts: before, changedParts: [], addedParts: [], error: error2 });
|
|
14615
|
+
const commentsMode = output?.commentsXmlMode === "replace" ? "replace" : "merge";
|
|
14616
|
+
const extendedMode = output?.commentsExtendedXmlMode === "replace" ? "replace" : "merge";
|
|
14617
|
+
for (const [label, value] of [
|
|
14618
|
+
...Object.values(PART_KEYS).map((key) => [`parts.${key}`, before[key]]),
|
|
14619
|
+
["output.commentsXml", output?.commentsXml],
|
|
14620
|
+
["output.commentsExtendedXml", output?.commentsExtendedXml]
|
|
14621
|
+
]) {
|
|
14622
|
+
if (value != null && typeof value !== "string") {
|
|
14623
|
+
return fail({ code: "INVALID_COMMENT_PARTS", message: `${label} must be an XML string, null or omitted (got ${typeof value}).` });
|
|
14624
|
+
}
|
|
14625
|
+
}
|
|
14626
|
+
const requirePart = (xml, part, rootName, label) => {
|
|
14627
|
+
let doc;
|
|
14628
|
+
try {
|
|
14629
|
+
doc = parseXmlStrictStandalone(xml, label);
|
|
14630
|
+
} catch (error2) {
|
|
14631
|
+
throw Object.assign(error2, { part });
|
|
14632
|
+
}
|
|
14633
|
+
if (doc.documentElement?.localName !== rootName) {
|
|
14634
|
+
throw Object.assign(new Error(`${label}: expected a <${rootName}> root element, found <${doc.documentElement?.localName || "none"}>.`), { code: "INVALID_COMMENT_PARTS", part });
|
|
14635
|
+
}
|
|
14636
|
+
return doc;
|
|
14637
|
+
};
|
|
14638
|
+
try {
|
|
14639
|
+
if (output?.commentsXml) {
|
|
14640
|
+
const incomingDoc = requirePart(output.commentsXml, "comments", "comments", "word/comments.xml (incoming)");
|
|
14641
|
+
const seen = /* @__PURE__ */ new Set();
|
|
14642
|
+
for (const node of Array.from(incomingDoc.getElementsByTagNameNS("*", "comment"))) {
|
|
14643
|
+
const id = node.getAttribute("w:id") || node.getAttribute("id");
|
|
14644
|
+
if (!id) continue;
|
|
14645
|
+
if (seen.has(id)) {
|
|
14646
|
+
throw Object.assign(new Error(`Duplicate comment id: ${id}`), { code: "DUPLICATE_COMMENT_ID", commentId: id, part: "comments" });
|
|
14647
|
+
}
|
|
14648
|
+
seen.add(id);
|
|
14649
|
+
}
|
|
14650
|
+
if (before.commentsXml) requirePart(before.commentsXml, "comments", "comments", "word/comments.xml (existing)");
|
|
14651
|
+
if (before.commentsIdsXml) requirePart(before.commentsIdsXml, "commentsIds", "commentsIds", "word/commentsIds.xml (existing)");
|
|
14652
|
+
if (before.commentsExtensibleXml) requirePart(before.commentsExtensibleXml, "commentsExtensible", "commentsExtensible", "word/commentsExtensible.xml (existing)");
|
|
14653
|
+
after.commentsXml = mergeCommentsPartXml(before.commentsXml, output.commentsXml, { replaceExisting: commentsMode === "replace" });
|
|
14654
|
+
Object.assign(after, reconcileCommentSiblingParts({
|
|
14655
|
+
commentsXml: after.commentsXml,
|
|
14656
|
+
commentsIdsXml: after.commentsIdsXml,
|
|
14657
|
+
commentsExtensibleXml: after.commentsExtensibleXml
|
|
14658
|
+
}));
|
|
14659
|
+
}
|
|
14660
|
+
if (output?.commentsExtendedXml) {
|
|
14661
|
+
if (extendedMode !== "replace" && before.commentsExtendedXml) {
|
|
14662
|
+
return fail({
|
|
14663
|
+
code: "UNSUPPORTED_EXISTING_COMMENT_THREAD_METADATA",
|
|
14664
|
+
part: "commentsExtended",
|
|
14665
|
+
message: 'The output carries a commentsExtended delta (mode "merge") for parts that already have thread metadata; a delta cannot be merged into an existing commentsExtended.xml. Nothing was merged.'
|
|
14666
|
+
});
|
|
14667
|
+
}
|
|
14668
|
+
requirePart(output.commentsExtendedXml, "commentsExtended", "commentsEx", "word/commentsExtended.xml (incoming)");
|
|
14669
|
+
after.commentsExtendedXml = output.commentsExtendedXml;
|
|
14670
|
+
}
|
|
14671
|
+
} catch (error2) {
|
|
14672
|
+
return fail({
|
|
14673
|
+
code: error2?.code || "COMMENT_PARTS_MERGE_FAILED",
|
|
14674
|
+
message: error2?.message || String(error2),
|
|
14675
|
+
...error2?.part || error2?.code === "DUPLICATE_COMMENT_ID" ? { part: error2.part || "comments" } : {},
|
|
14676
|
+
...error2?.commentId ? { commentId: error2.commentId } : {}
|
|
14677
|
+
});
|
|
14678
|
+
}
|
|
14679
|
+
const changedParts = [];
|
|
14680
|
+
const addedParts = [];
|
|
14681
|
+
for (const [name, key] of Object.entries(PART_KEYS)) {
|
|
14682
|
+
if (after[key] === before[key]) continue;
|
|
14683
|
+
changedParts.push(name);
|
|
14684
|
+
if (!before[key]) addedParts.push(name);
|
|
14685
|
+
}
|
|
14686
|
+
return { status: "ok", hasChanges: changedParts.length > 0, parts: after, changedParts, addedParts };
|
|
14687
|
+
}
|
|
14688
|
+
|
|
14689
|
+
// engine/formatting-removal.js
|
|
14690
|
+
function removeNode2(node) {
|
|
14691
|
+
if (node?.parentNode) {
|
|
14692
|
+
node.parentNode.removeChild(node);
|
|
14693
|
+
}
|
|
14694
|
+
}
|
|
14695
|
+
function removeFormattingFromRPr(rPr, formatTypes = ["all"]) {
|
|
14696
|
+
if (!rPr) return null;
|
|
14697
|
+
const rPrClone = rPr.cloneNode(true);
|
|
14698
|
+
if (formatTypes.includes("all")) {
|
|
14699
|
+
const toRemove = [
|
|
14700
|
+
"w:b",
|
|
14701
|
+
"w:i",
|
|
14702
|
+
"w:u",
|
|
14703
|
+
"w:strike",
|
|
14704
|
+
"w:dstrike",
|
|
14705
|
+
"w:color",
|
|
14706
|
+
"w:sz",
|
|
14707
|
+
"w:szCs",
|
|
14708
|
+
"w:rFonts",
|
|
14709
|
+
"w:highlight",
|
|
14710
|
+
"w:vertAlign",
|
|
14711
|
+
"w:spacing",
|
|
14712
|
+
"w:w",
|
|
14713
|
+
"w:kern",
|
|
14714
|
+
"w:position"
|
|
14715
|
+
];
|
|
14716
|
+
toRemove.forEach((tag) => {
|
|
14717
|
+
const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace("w:", "")}`);
|
|
14718
|
+
elements.forEach(removeNode2);
|
|
14719
|
+
});
|
|
14720
|
+
} else {
|
|
14721
|
+
const tagMap = {
|
|
14722
|
+
"bold": "w:b",
|
|
14723
|
+
"italic": "w:i",
|
|
14724
|
+
"underline": "w:u",
|
|
14725
|
+
"strikethrough": "w:strike",
|
|
14726
|
+
"doubleStrike": "w:dstrike",
|
|
14727
|
+
"color": "w:color",
|
|
14728
|
+
"highlight": "w:highlight",
|
|
14729
|
+
"fontSize": "w:sz",
|
|
14730
|
+
"fontSizeCs": "w:szCs",
|
|
14731
|
+
// Complex script font size
|
|
14732
|
+
"fontFamily": "w:rFonts",
|
|
14733
|
+
"superscript": "w:vertAlign",
|
|
14734
|
+
"subscript": "w:vertAlign"
|
|
14735
|
+
};
|
|
14736
|
+
formatTypes.forEach((type) => {
|
|
14737
|
+
const tag = tagMap[type];
|
|
14738
|
+
if (tag) {
|
|
14739
|
+
const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace("w:", "")}`);
|
|
14740
|
+
elements.forEach(removeNode2);
|
|
14741
|
+
}
|
|
14742
|
+
});
|
|
14743
|
+
}
|
|
14744
|
+
return rPrClone.children.length > 0 ? rPrClone : null;
|
|
14745
|
+
}
|
|
14746
|
+
function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
|
|
14747
|
+
if (!targetText || !ooxmlString) return ooxmlString;
|
|
14748
|
+
const doc = parseOoxml(ooxmlString);
|
|
14749
|
+
if (!doc) return ooxmlString;
|
|
14750
|
+
const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
14751
|
+
const runs = doc.getElementsByTagNameNS(NS_W11, "r");
|
|
14752
|
+
const insertions = doc.getElementsByTagNameNS(NS_W11, "ins");
|
|
14753
|
+
const allRuns = [...Array.from(runs)];
|
|
14754
|
+
for (const ins of insertions) {
|
|
14755
|
+
const insideRuns = ins.getElementsByTagNameNS(NS_W11, "r");
|
|
14756
|
+
allRuns.push(...Array.from(insideRuns));
|
|
14757
|
+
}
|
|
14758
|
+
for (const run of allRuns) {
|
|
14759
|
+
const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
|
|
14760
|
+
const runText = Array.from(textNodes).map((t) => t.textContent).join("");
|
|
14761
|
+
if (runText.includes(targetText) || runText === targetText) {
|
|
14762
|
+
const rPrElements = run.getElementsByTagNameNS(NS_W11, "rPr");
|
|
14763
|
+
if (rPrElements.length > 0) {
|
|
14764
|
+
const rPr = rPrElements[0];
|
|
14765
|
+
const newRPr = removeFormattingFromRPr(rPr, formatTypes);
|
|
14766
|
+
if (newRPr) {
|
|
14767
|
+
if (rPr.parentNode) {
|
|
14768
|
+
rPr.parentNode.replaceChild(newRPr, rPr);
|
|
14769
|
+
}
|
|
14770
|
+
} else {
|
|
14771
|
+
removeNode2(rPr);
|
|
14772
|
+
}
|
|
14773
|
+
}
|
|
14774
|
+
}
|
|
14775
|
+
}
|
|
14776
|
+
return serializeOoxml(doc);
|
|
14777
|
+
}
|
|
14778
|
+
var HIGHLIGHT_COLOR_MAP = {
|
|
14779
|
+
"yellow": "yellow",
|
|
14780
|
+
"green": "green",
|
|
14781
|
+
"cyan": "cyan",
|
|
14782
|
+
"magenta": "magenta",
|
|
14783
|
+
"blue": "blue",
|
|
14784
|
+
"red": "red",
|
|
14785
|
+
"darkblue": "darkBlue",
|
|
14786
|
+
"darkcyan": "darkCyan",
|
|
14787
|
+
"darkgreen": "darkGreen",
|
|
14788
|
+
"darkmagenta": "darkMagenta",
|
|
14789
|
+
"darkred": "darkRed",
|
|
14790
|
+
"darkyellow": "darkYellow",
|
|
14791
|
+
"gray25": "lightGray",
|
|
14792
|
+
"gray50": "darkGray",
|
|
14793
|
+
"black": "black",
|
|
14794
|
+
"white": "white"
|
|
14795
|
+
};
|
|
14796
|
+
function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
|
|
14797
|
+
const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
14798
|
+
const ooxmlColor = HIGHLIGHT_COLOR_MAP[color.toLowerCase()] || "yellow";
|
|
14799
|
+
const generateRedlines = options?.generateRedlines ?? false;
|
|
14800
|
+
const author = options?.author || getDefaultAuthor();
|
|
14801
|
+
let rPrElement = rPr;
|
|
14802
|
+
if (!rPrElement) {
|
|
14803
|
+
rPrElement = createWordElement(doc, "w:rPr");
|
|
14804
|
+
} else {
|
|
14805
|
+
rPrElement = rPr.cloneNode(true);
|
|
14806
|
+
}
|
|
14807
|
+
let previousRPrState = null;
|
|
14808
|
+
if (generateRedlines) {
|
|
14809
|
+
previousRPrState = createWordElement(doc, "w:rPr");
|
|
14810
|
+
Array.from(rPrElement.childNodes).forEach((child) => {
|
|
14811
|
+
if (child.nodeName !== "w:rPrChange") {
|
|
14812
|
+
previousRPrState.appendChild(child.cloneNode(true));
|
|
14813
|
+
}
|
|
14814
|
+
});
|
|
14815
|
+
}
|
|
14816
|
+
const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W11, "highlight");
|
|
14817
|
+
Array.from(existingHighlight).forEach(removeNode2);
|
|
14818
|
+
const highlightEl = createWordElement(doc, "w:highlight");
|
|
14819
|
+
highlightEl.setAttributeNS(NS_W11, "w:val", ooxmlColor);
|
|
14820
|
+
rPrElement.appendChild(highlightEl);
|
|
14821
|
+
if (generateRedlines && previousRPrState) {
|
|
14822
|
+
const rPrChange = createWordElement(doc, "w:rPrChange");
|
|
14823
|
+
const metadata = createRevisionMetadata(author, doc);
|
|
14824
|
+
rPrChange.setAttribute("w:id", String(metadata.id));
|
|
14825
|
+
rPrChange.setAttribute("w:author", metadata.author);
|
|
14826
|
+
rPrChange.setAttribute("w:date", metadata.date);
|
|
14827
|
+
rPrChange.appendChild(previousRPrState);
|
|
14828
|
+
const existingChange = rPrElement.getElementsByTagNameNS(NS_W11, "rPrChange");
|
|
14829
|
+
Array.from(existingChange).forEach(removeNode2);
|
|
14830
|
+
rPrElement.appendChild(rPrChange);
|
|
14831
|
+
}
|
|
14832
|
+
return rPrElement;
|
|
14833
|
+
}
|
|
14834
|
+
function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", options = {}) {
|
|
14835
|
+
if (!targetText || !ooxmlString) return ooxmlString;
|
|
14836
|
+
const doc = parseOoxml(ooxmlString);
|
|
14837
|
+
if (!doc) return ooxmlString;
|
|
14838
|
+
let revisionIdAllocator;
|
|
14839
|
+
if (options?._revisionIdAllocator instanceof RevisionIdAllocator) {
|
|
14840
|
+
revisionIdAllocator = options._revisionIdAllocator;
|
|
14841
|
+
seedRevisionIdsFromDocument(doc, revisionIdAllocator);
|
|
14842
|
+
} else {
|
|
14843
|
+
revisionIdAllocator = createRevisionIdAllocator(doc);
|
|
14844
|
+
}
|
|
14845
|
+
const NS_W11 = "http://schemas.openxmlformats.org/wordprocessingml/2006/main";
|
|
14846
|
+
const getRunText = (run) => {
|
|
14847
|
+
const textNodes = run.getElementsByTagNameNS(NS_W11, "t");
|
|
14848
|
+
return Array.from(textNodes).map((t) => t.textContent).join("");
|
|
14849
|
+
};
|
|
14850
|
+
const allRuns = Array.from(doc.getElementsByTagNameNS(NS_W11, "r"));
|
|
14851
|
+
const sourceRunsWithClaimedRevisionIds = /* @__PURE__ */ new WeakSet();
|
|
14852
|
+
const cloneRunWithText2 = (sourceRun, text2, shouldHighlight) => {
|
|
14853
|
+
const clonedRun = sourceRun.cloneNode(true);
|
|
14854
|
+
if (sourceRunsWithClaimedRevisionIds.has(sourceRun)) {
|
|
14855
|
+
refreshRunPropertyChangeIds(clonedRun, revisionIdAllocator);
|
|
14856
|
+
} else {
|
|
14857
|
+
sourceRunsWithClaimedRevisionIds.add(sourceRun);
|
|
14858
|
+
}
|
|
14859
|
+
const textNodes = clonedRun.getElementsByTagNameNS(NS_W11, "t");
|
|
14860
|
+
Array.from(textNodes).forEach(removeNode2);
|
|
14861
|
+
const newText = createWordElement(doc, "w:t");
|
|
14862
|
+
newText.setAttribute("xml:space", "preserve");
|
|
14863
|
+
newText.textContent = text2;
|
|
14864
|
+
clonedRun.appendChild(newText);
|
|
14865
|
+
if (shouldHighlight) {
|
|
14866
|
+
const rPrElements = clonedRun.getElementsByTagNameNS(NS_W11, "rPr");
|
|
14867
|
+
const existingRPr = rPrElements.length > 0 ? rPrElements[0] : null;
|
|
14868
|
+
const newRPr = injectHighlightIntoRPr(doc, existingRPr, color, options);
|
|
14869
|
+
if (existingRPr) {
|
|
14870
|
+
clonedRun.replaceChild(newRPr, existingRPr);
|
|
14871
|
+
} else {
|
|
14872
|
+
clonedRun.insertBefore(newRPr, clonedRun.firstChild);
|
|
14873
|
+
}
|
|
14874
|
+
}
|
|
14875
|
+
return clonedRun;
|
|
14876
|
+
};
|
|
14877
|
+
for (const run of allRuns) {
|
|
14878
|
+
const runText = getRunText(run);
|
|
14879
|
+
if (!runText) continue;
|
|
14880
|
+
const matchIndexes = [];
|
|
14881
|
+
let searchOffset = 0;
|
|
14882
|
+
while (searchOffset <= runText.length - targetText.length) {
|
|
14883
|
+
const matchIndex = runText.indexOf(targetText, searchOffset);
|
|
14884
|
+
if (matchIndex === -1) break;
|
|
14885
|
+
matchIndexes.push(matchIndex);
|
|
14886
|
+
searchOffset = matchIndex + targetText.length;
|
|
14887
|
+
}
|
|
14888
|
+
if (matchIndexes.length === 0) continue;
|
|
14889
|
+
const parent = run.parentNode;
|
|
14890
|
+
if (!parent) {
|
|
14891
|
+
console.warn("[Highlight] Run parent is null; skipping. Likely already processed.");
|
|
14892
|
+
continue;
|
|
14893
|
+
}
|
|
14894
|
+
const fragment = doc.createDocumentFragment();
|
|
14895
|
+
let cursor = 0;
|
|
14896
|
+
for (const matchIndex of matchIndexes) {
|
|
14897
|
+
if (matchIndex > cursor) {
|
|
14898
|
+
fragment.appendChild(cloneRunWithText2(run, runText.slice(cursor, matchIndex), false));
|
|
14899
|
+
}
|
|
14900
|
+
fragment.appendChild(cloneRunWithText2(
|
|
14901
|
+
run,
|
|
14902
|
+
runText.slice(matchIndex, matchIndex + targetText.length),
|
|
14903
|
+
true
|
|
14904
|
+
));
|
|
14905
|
+
cursor = matchIndex + targetText.length;
|
|
14906
|
+
}
|
|
14907
|
+
if (cursor < runText.length) {
|
|
14908
|
+
fragment.appendChild(cloneRunWithText2(run, runText.slice(cursor), false));
|
|
14909
|
+
}
|
|
14910
|
+
parent.replaceChild(fragment, run);
|
|
14911
|
+
}
|
|
14912
|
+
return serializeOoxml(doc);
|
|
14913
|
+
}
|
|
14914
|
+
|
|
14736
14915
|
// orchestration/route-plan.js
|
|
14737
14916
|
var RoutePlanKind = Object.freeze({
|
|
14738
14917
|
STRUCTURED_LIST_DIRECT: "structured_list_direct",
|
|
@@ -16199,6 +16378,7 @@ var SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
|
|
|
16199
16378
|
"highlight"
|
|
16200
16379
|
]);
|
|
16201
16380
|
var EXISTING_REVISIONS_POLICIES = Object.freeze([
|
|
16381
|
+
"auto",
|
|
16202
16382
|
"merge-same-author",
|
|
16203
16383
|
"slice-cross-author",
|
|
16204
16384
|
"reject-input",
|
|
@@ -18436,7 +18616,7 @@ function deriveParagraphThenTableAppend(modifiedText) {
|
|
|
18436
18616
|
const tableLines = lines.slice(1);
|
|
18437
18617
|
if (!tableLines.every((line) => line.trim().startsWith("|"))) return null;
|
|
18438
18618
|
const separator = tableLines[1].replace(/\s+/g, "");
|
|
18439
|
-
if (!/^\|:?-{3,}:?(\|:?-{3,}:?)
|
|
18619
|
+
if (!/^\|:?-{3,}:?(?:(\|:?-{3,}:?)+\|?|\|)$/.test(separator)) return null;
|
|
18440
18620
|
return { paragraphMarkdown: lines[0].trim(), tableText: tableLines.join("\n") };
|
|
18441
18621
|
}
|
|
18442
18622
|
|
|
@@ -19066,7 +19246,10 @@ async function buildInsertedPlainParagraph(xmlDoc, anchorParagraph, text2, revis
|
|
|
19066
19246
|
String(text2 || ""),
|
|
19067
19247
|
{
|
|
19068
19248
|
author,
|
|
19069
|
-
generateRedlines: false
|
|
19249
|
+
generateRedlines: false,
|
|
19250
|
+
// A caller's structuredContent: false keeps inserted paragraphs plain too: no headings,
|
|
19251
|
+
// and inline Markdown markers stay literal.
|
|
19252
|
+
...options.plainTextContent === true ? { structuredContent: false, plainTextContent: true } : {}
|
|
19070
19253
|
}
|
|
19071
19254
|
);
|
|
19072
19255
|
let sourceParagraph = null;
|
|
@@ -20253,13 +20436,17 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
20253
20436
|
}
|
|
20254
20437
|
};
|
|
20255
20438
|
}
|
|
20439
|
+
const autoExistingRevisions = options.existingRevisions === "auto";
|
|
20440
|
+
if (autoExistingRevisions) {
|
|
20441
|
+
options = { ...options, existingRevisions: resolveExistingRevisionsPolicy("auto", targetParagraph, author) };
|
|
20442
|
+
}
|
|
20256
20443
|
const hasRevisions = containsTrackedChanges(targetParagraph);
|
|
20257
20444
|
const existingPolicy = options.existingRevisions || "merge-same-author";
|
|
20258
20445
|
if (hasRevisions && existingPolicy !== "accept-all-first" && existingPolicy !== "accept-all-first-keep-normalized") {
|
|
20259
20446
|
if (existingPolicy === "merge-same-author" || existingPolicy === "slice-cross-author") {
|
|
20260
20447
|
const authors = getTrackedChangeAuthors(targetParagraph);
|
|
20261
20448
|
const opAuthor = String(author || "").trim().toLowerCase();
|
|
20262
|
-
const allSame = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === opAuthor);
|
|
20449
|
+
const allSame = authors.length > 0 && !hasUnattributedTrackedChange(targetParagraph) && authors.every((a) => a.trim().toLowerCase() === opAuthor);
|
|
20263
20450
|
if (!allSame && existingPolicy === "merge-same-author") {
|
|
20264
20451
|
return {
|
|
20265
20452
|
documentXml,
|
|
@@ -20268,7 +20455,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
20268
20455
|
status: "error",
|
|
20269
20456
|
error: {
|
|
20270
20457
|
code: "EXISTING_REVISIONS",
|
|
20271
|
-
message: `Target paragraph contains tracked changes from another author (${authors
|
|
20458
|
+
message: `Target paragraph contains tracked changes from another author (${[...authors, ...hasUnattributedTrackedChange(targetParagraph) ? ["unattributed"] : []].join(", ")}). Use existingRevisions: "slice-cross-author" for a surgical edit that preserves reviewer history; accepting or rejecting revisions requires separate authorization.`,
|
|
20272
20459
|
revisionAuthors: authors,
|
|
20273
20460
|
currentPolicy: existingPolicy
|
|
20274
20461
|
}
|
|
@@ -20410,6 +20597,9 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
20410
20597
|
onWarn
|
|
20411
20598
|
}) : typeof targetText === "string" && /\r?\n/.test(targetText) ? findReconstructionParagraphRange(xmlDoc, targetText) : null;
|
|
20412
20599
|
const hasExplicitRangeScope = Array.isArray(explicitRangeParagraphs) && explicitRangeParagraphs.length > 0;
|
|
20600
|
+
if (autoExistingRevisions && hasExplicitRangeScope) {
|
|
20601
|
+
options = { ...options, existingRevisions: resolveExistingRevisionsPolicy("auto", explicitRangeParagraphs, author) };
|
|
20602
|
+
}
|
|
20413
20603
|
if (!useTableScope && hasExplicitRangeScope) {
|
|
20414
20604
|
const insertionEntries = buildExplicitRangeInsertionEntries(explicitRangeParagraphs, effectiveModifiedText);
|
|
20415
20605
|
if (insertionEntries && insertionEntries.length > 0) {
|
|
@@ -20585,7 +20775,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
20585
20775
|
author,
|
|
20586
20776
|
// A wholly inserted paragraph must track its own paragraph mark; otherwise Word's
|
|
20587
20777
|
// Reject All removes the text but leaves an empty paragraph behind.
|
|
20588
|
-
{ generateRedlines, trackParagraphMark: true }
|
|
20778
|
+
{ generateRedlines, trackParagraphMark: true, plainTextContent: options.plainTextContent === true }
|
|
20589
20779
|
);
|
|
20590
20780
|
const imported = xmlDoc.importNode(plainParagraph, true);
|
|
20591
20781
|
ensureParagraphIdsOnImportedNode(imported, operationSession);
|
|
@@ -20724,6 +20914,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
20724
20914
|
existingRevisions: options.existingRevisions || "merge-same-author",
|
|
20725
20915
|
structuredContent: options.structuredContent !== false,
|
|
20726
20916
|
explicitStructuredContent: options.explicitStructuredContent === true,
|
|
20917
|
+
plainTextContent: options.plainTextContent === true,
|
|
20727
20918
|
_revisionIdAllocator: revisionIdAllocator,
|
|
20728
20919
|
_isolatedTableCell: useTableScope
|
|
20729
20920
|
}) : await applyRedlineToOxml2(scopedXml, originalTextForApply, effectiveModifiedText, {
|
|
@@ -20735,6 +20926,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
20735
20926
|
pairReplacements: options.pairReplacements === true,
|
|
20736
20927
|
insertionAffinity: options.insertionAffinity || null,
|
|
20737
20928
|
literalTextEdit: options.literalTextEdit === true,
|
|
20929
|
+
plainTextContent: options.plainTextContent === true,
|
|
20738
20930
|
_revisionIdAllocator: revisionIdAllocator,
|
|
20739
20931
|
_isolatedTableCell: useTableScope
|
|
20740
20932
|
});
|
|
@@ -21745,7 +21937,31 @@ function verifyUntrackedLocality({ before, after, targetSourceIndex, beforeViews
|
|
|
21745
21937
|
}
|
|
21746
21938
|
return violations.length ? { ok: false, violations } : { ok: true };
|
|
21747
21939
|
}
|
|
21748
|
-
|
|
21940
|
+
var numberingDefinitionCache = /* @__PURE__ */ new Map();
|
|
21941
|
+
var NUMBERING_DEFINITION_CACHE_SIZE = 8;
|
|
21942
|
+
function parseNumberingDefinitions(text2) {
|
|
21943
|
+
const cached = numberingDefinitionCache.get(text2);
|
|
21944
|
+
if (cached) return cached;
|
|
21945
|
+
const abstractLevels = /* @__PURE__ */ new Map();
|
|
21946
|
+
for (const match of text2.matchAll(/<(?:\w+:)?abstractNum\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?abstractNum>/g)) {
|
|
21947
|
+
const id = /abstractNumId="([^"]*)"/.exec(match[1])?.[1];
|
|
21948
|
+
if (id == null) continue;
|
|
21949
|
+
const levels = abstractLevels.get(id) || /* @__PURE__ */ new Set();
|
|
21950
|
+
for (const lvl of match[2].matchAll(/<(?:\w+:)?lvl\b[^>]*?ilvl="([^"]*)"/g)) levels.add(lvl[1]);
|
|
21951
|
+
abstractLevels.set(id, levels);
|
|
21952
|
+
}
|
|
21953
|
+
const numToAbstract = /* @__PURE__ */ new Map();
|
|
21954
|
+
for (const match of text2.matchAll(/<(?:\w+:)?num\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?num>/g)) {
|
|
21955
|
+
const id = /numId="([^"]*)"/.exec(match[1])?.[1];
|
|
21956
|
+
const abstractId = /abstractNumId[^>]*?val="([^"]*)"/.exec(match[2])?.[1];
|
|
21957
|
+
if (id != null && abstractId != null) numToAbstract.set(id, abstractId);
|
|
21958
|
+
}
|
|
21959
|
+
const definitions = { abstractLevels, numToAbstract };
|
|
21960
|
+
if (numberingDefinitionCache.size >= NUMBERING_DEFINITION_CACHE_SIZE) numberingDefinitionCache.delete(numberingDefinitionCache.keys().next().value);
|
|
21961
|
+
numberingDefinitionCache.set(text2, definitions);
|
|
21962
|
+
return definitions;
|
|
21963
|
+
}
|
|
21964
|
+
function verifyNumberingReferences({ before, after, numberingXmls }) {
|
|
21749
21965
|
const references = (documentNode) => {
|
|
21750
21966
|
const found = /* @__PURE__ */ new Map();
|
|
21751
21967
|
for (const numIdNode of getAllReferencedNumIdElements(documentNode)) {
|
|
@@ -21761,21 +21977,7 @@ function verifyNumberingReferences({ before, after, numberingXmls, beforeViews =
|
|
|
21761
21977
|
const used = references(after);
|
|
21762
21978
|
if (used.size === 0) return { ok: true };
|
|
21763
21979
|
const known = references(before);
|
|
21764
|
-
const
|
|
21765
|
-
const abstractLevels = /* @__PURE__ */ new Map();
|
|
21766
|
-
for (const match of text2.matchAll(/<(?:\w+:)?abstractNum\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?abstractNum>/g)) {
|
|
21767
|
-
const id = /abstractNumId="([^"]*)"/.exec(match[1])?.[1];
|
|
21768
|
-
if (id == null) continue;
|
|
21769
|
-
const levels = abstractLevels.get(id) || /* @__PURE__ */ new Set();
|
|
21770
|
-
for (const lvl of match[2].matchAll(/<(?:\w+:)?lvl\b[^>]*?ilvl="([^"]*)"/g)) levels.add(lvl[1]);
|
|
21771
|
-
abstractLevels.set(id, levels);
|
|
21772
|
-
}
|
|
21773
|
-
const numToAbstract = /* @__PURE__ */ new Map();
|
|
21774
|
-
for (const match of text2.matchAll(/<(?:\w+:)?num\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?num>/g)) {
|
|
21775
|
-
const id = /numId="([^"]*)"/.exec(match[1])?.[1];
|
|
21776
|
-
const abstractId = /abstractNumId[^>]*?val="([^"]*)"/.exec(match[2])?.[1];
|
|
21777
|
-
if (id != null && abstractId != null) numToAbstract.set(id, abstractId);
|
|
21778
|
-
}
|
|
21980
|
+
const { abstractLevels, numToAbstract } = parseNumberingDefinitions((numberingXmls || []).filter(Boolean).join("\n"));
|
|
21779
21981
|
const violations = [];
|
|
21780
21982
|
for (const [numId, levels] of used) {
|
|
21781
21983
|
if (known.has(numId)) continue;
|
|
@@ -22124,6 +22326,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
|
|
|
22124
22326
|
...operation.existingRevisions ? { existingRevisions: operation.existingRevisions } : {},
|
|
22125
22327
|
structuredContent: typeof operation.structuredContent === "boolean" ? operation.structuredContent : options.structuredContent !== false,
|
|
22126
22328
|
explicitStructuredContent: operation.structuredContent === true,
|
|
22329
|
+
plainTextContent: operation.structuredContent === false || operation.structuredContent == null && options.structuredContent === false,
|
|
22127
22330
|
...operation._localizedReplacementCompilation && operation.structuredContent !== true ? { literalTextEdit: true } : {},
|
|
22128
22331
|
pairReplacements: typeof operation.pairReplacements === "boolean" ? operation.pairReplacements : options.pairReplacements !== false,
|
|
22129
22332
|
...operation.insertionAffinity ? { insertionAffinity: operation.insertionAffinity } : {},
|
|
@@ -23378,35 +23581,44 @@ function preflightOperations(documentXml, operations, author, options = {}) {
|
|
|
23378
23581
|
const anchor = operation.operationKind === "comment" ? operation.textToComment || paragraphText : operation.operationKind === "highlight" ? operation.textToHighlight : operation.operationKind === "format" ? operation.textToFormat : null;
|
|
23379
23582
|
const anchorResolution = operation.operationKind === "comment" && anchor != null ? resolveTextInParagraphIndex(createParagraphTextIndex(paragraph, { revisionView: targetView }), anchor) : null;
|
|
23380
23583
|
const anchorFound = anchor == null || (anchorResolution ? anchorResolution.found : paragraphText.includes(anchor));
|
|
23584
|
+
const rangeParagraphs = (opType) => {
|
|
23585
|
+
if (operation.targetEndRef) {
|
|
23586
|
+
return resolveParagraphRangeByRefs(xmlDoc, operation.targetRef, operation.targetEndRef, {
|
|
23587
|
+
opType,
|
|
23588
|
+
targetRefSnapshot: options.targetRefSnapshot || null,
|
|
23589
|
+
onInfo: options.onInfo,
|
|
23590
|
+
onWarn: options.onWarn
|
|
23591
|
+
});
|
|
23592
|
+
}
|
|
23593
|
+
if (!operation.targetEndDescriptor) return [paragraph];
|
|
23594
|
+
const endResolved = resolveTargetParagraph(xmlDoc, {
|
|
23595
|
+
targetText: operation.targetEndDescriptor.text,
|
|
23596
|
+
targetRef: operation.targetEndDescriptor.index,
|
|
23597
|
+
targetDescriptor: operation.targetEndDescriptor,
|
|
23598
|
+
opType,
|
|
23599
|
+
strictAmbiguity: strictTargets,
|
|
23600
|
+
paragraphMetadataIndex: currentMetadataIndex,
|
|
23601
|
+
metadataIndices,
|
|
23602
|
+
onInfo: options.onInfo,
|
|
23603
|
+
onWarn: options.onWarn
|
|
23604
|
+
});
|
|
23605
|
+
const allParagraphs = getDocumentParagraphNodes(xmlDoc);
|
|
23606
|
+
const startIndex = allParagraphs.indexOf(paragraph);
|
|
23607
|
+
const endIndex = allParagraphs.indexOf(endResolved?.paragraph);
|
|
23608
|
+
return startIndex >= 0 && endIndex >= startIndex ? allParagraphs.slice(startIndex, endIndex + 1) : null;
|
|
23609
|
+
};
|
|
23381
23610
|
const hasRevisions = containsTrackedChanges(paragraph);
|
|
23382
|
-
const
|
|
23611
|
+
const requestedPolicy = operation.existingRevisions || options.existingRevisions || "merge-same-author";
|
|
23612
|
+
const existingPolicy = resolveExistingRevisionsPolicy(
|
|
23613
|
+
requestedPolicy,
|
|
23614
|
+
requestedPolicy === "auto" && operation.operationKind === "redline" && (operation.targetEndRef || operation.targetEndDescriptor) ? rangeParagraphs("redline") || paragraph : paragraph,
|
|
23615
|
+
authorUsed
|
|
23616
|
+
);
|
|
23383
23617
|
const deletingWholeParagraph = operation.operationKind === "redline" && operation.modified === "";
|
|
23384
23618
|
const commentIds = deletingWholeParagraph ? getCommentIdsInParagraph(paragraph) : [];
|
|
23385
23619
|
let error2 = null;
|
|
23386
23620
|
if (operation.operationKind === "restore") {
|
|
23387
|
-
|
|
23388
|
-
opType: "restore",
|
|
23389
|
-
targetRefSnapshot: options.targetRefSnapshot || null,
|
|
23390
|
-
onInfo: options.onInfo,
|
|
23391
|
-
onWarn: options.onWarn
|
|
23392
|
-
}) : [paragraph];
|
|
23393
|
-
if (!operation.targetEndRef && operation.targetEndDescriptor) {
|
|
23394
|
-
const endResolved = resolveTargetParagraph(xmlDoc, {
|
|
23395
|
-
targetText: operation.targetEndDescriptor.text,
|
|
23396
|
-
targetRef: operation.targetEndDescriptor.index,
|
|
23397
|
-
targetDescriptor: operation.targetEndDescriptor,
|
|
23398
|
-
opType: "restore",
|
|
23399
|
-
strictAmbiguity: strictTargets,
|
|
23400
|
-
paragraphMetadataIndex: currentMetadataIndex,
|
|
23401
|
-
metadataIndices,
|
|
23402
|
-
onInfo: options.onInfo,
|
|
23403
|
-
onWarn: options.onWarn
|
|
23404
|
-
});
|
|
23405
|
-
const allParagraphs = getDocumentParagraphNodes(xmlDoc);
|
|
23406
|
-
const startIndex = allParagraphs.indexOf(paragraph);
|
|
23407
|
-
const endIndex = allParagraphs.indexOf(endResolved?.paragraph);
|
|
23408
|
-
restorationParagraphs = startIndex >= 0 && endIndex >= startIndex ? allParagraphs.slice(startIndex, endIndex + 1) : null;
|
|
23409
|
-
}
|
|
23621
|
+
const restorationParagraphs = rangeParagraphs("restore");
|
|
23410
23622
|
const replacements = Array.isArray(operation.modified) ? operation.modified : [operation.modified];
|
|
23411
23623
|
if (!restorationParagraphs || restorationParagraphs[0] !== paragraph || replacements.length !== restorationParagraphs.length) {
|
|
23412
23624
|
error2 = {
|
|
@@ -23453,11 +23665,11 @@ function preflightOperations(documentXml, operations, author, options = {}) {
|
|
|
23453
23665
|
if (existingPolicy === "merge-same-author" || existingPolicy === "slice-cross-author") {
|
|
23454
23666
|
const authors = getTrackedChangeAuthors(paragraph);
|
|
23455
23667
|
const opAuthor = String(authorUsed || "").trim().toLowerCase();
|
|
23456
|
-
const allSame = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === opAuthor);
|
|
23668
|
+
const allSame = authors.length > 0 && !hasUnattributedTrackedChange(paragraph) && authors.every((a) => a.trim().toLowerCase() === opAuthor);
|
|
23457
23669
|
if (!allSame && existingPolicy === "merge-same-author") {
|
|
23458
23670
|
error2 = {
|
|
23459
23671
|
code: "EXISTING_REVISIONS",
|
|
23460
|
-
message: `Target paragraph contains tracked changes from another author (${authors
|
|
23672
|
+
message: `Target paragraph contains tracked changes from another author (${[...authors, ...hasUnattributedTrackedChange(paragraph) ? ["unattributed"] : []].join(", ")}). Use existingRevisions: "slice-cross-author" for a surgical edit that preserves reviewer history; accepting or rejecting revisions requires separate authorization.`,
|
|
23461
23673
|
revisionAuthors: authors,
|
|
23462
23674
|
currentPolicy: existingPolicy
|
|
23463
23675
|
};
|
|
@@ -25543,6 +25755,7 @@ export {
|
|
|
25543
25755
|
canonicalNumberingDefinition,
|
|
25544
25756
|
clearSingleLineListFallbackExplicitSequence,
|
|
25545
25757
|
collectContiguousListParagraphBlock,
|
|
25758
|
+
compareDocumentInspections,
|
|
25546
25759
|
computeDocumentPartsRevisionToken,
|
|
25547
25760
|
computePackageRevisionToken,
|
|
25548
25761
|
computeRevisionToken,
|
|
@@ -25594,9 +25807,11 @@ export {
|
|
|
25594
25807
|
injectCommentsIntoPackage2 as injectCommentsIntoPackage,
|
|
25595
25808
|
insertBodyElementBeforeSectPr,
|
|
25596
25809
|
inspectDocumentParts,
|
|
25810
|
+
inspectionReviewShape,
|
|
25597
25811
|
isLikelyStructuredTableSourceParagraph,
|
|
25598
25812
|
isMarkdownTableText,
|
|
25599
25813
|
isNodeVisibleInRevisionView,
|
|
25814
|
+
mergeCommentParts,
|
|
25600
25815
|
mergeNumberingParts,
|
|
25601
25816
|
mergeNumberingXmlBySchemaOrder,
|
|
25602
25817
|
normalizeBodySectionOrderStandalone,
|
|
@@ -25623,6 +25838,7 @@ export {
|
|
|
25623
25838
|
repairKnownContentTypes,
|
|
25624
25839
|
reserveNextNumberingId,
|
|
25625
25840
|
reserveNextNumberingIdPair,
|
|
25841
|
+
resolveExistingRevisionsPolicy,
|
|
25626
25842
|
resolveParagraphRangeByRefs,
|
|
25627
25843
|
resolveSingleLineListFallbackNumberingAction,
|
|
25628
25844
|
resolveTargetParagraph,
|