@ansonlai/docx-redline-js 0.9.0 → 0.10.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 +679 -669
- package/CHANGELOG.md +32 -1
- package/README.md +29 -6
- package/core/word-xml.js +160 -118
- package/dist/docx-redline-js.esm.js +171 -51
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +93 -93
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/dist/docx-redline.bundle.cjs +171 -51
- package/dist/docx-redline.bundle.cjs.map +4 -4
- package/dist/docx-redline.bundle.js +171 -51
- package/dist/docx-redline.bundle.js.map +4 -4
- package/docs/AGENT_KNOWLEDGE_BASE.md +30 -4
- package/docs/SKILL_AUTHORING.md +5 -1
- package/docs/TESTING.md +4 -0
- package/docs/schemas/document-operations.schema.json +1 -1
- package/engine/oxml-engine.js +748 -736
- package/index.d.ts +6 -1
- package/index.js +2 -0
- package/node/cli.js +993 -992
- package/package.json +1 -1
- package/pipeline/markdown-processor.js +5 -2
- 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 +3358 -3346
- package/services/inspection-comparison.js +82 -0
- package/services/operation-preflight.js +42 -33
- package/services/revision-fidelity-verifier.js +5 -19
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// @ansonlai/docx-redline-js standalone CommonJS bundle v0.
|
|
1
|
+
// @ansonlai/docx-redline-js standalone CommonJS bundle v0.10.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;
|
|
@@ -8009,6 +8009,7 @@ __export(index_exports, {
|
|
|
8009
8009
|
canonicalNumberingDefinition: () => canonicalNumberingDefinition,
|
|
8010
8010
|
clearSingleLineListFallbackExplicitSequence: () => clearSingleLineListFallbackExplicitSequence,
|
|
8011
8011
|
collectContiguousListParagraphBlock: () => collectContiguousListParagraphBlock,
|
|
8012
|
+
compareDocumentInspections: () => compareDocumentInspections,
|
|
8012
8013
|
computeDocumentPartsRevisionToken: () => computeDocumentPartsRevisionToken,
|
|
8013
8014
|
computePackageRevisionToken: () => computePackageRevisionToken,
|
|
8014
8015
|
computeRevisionToken: () => computeRevisionToken,
|
|
@@ -8060,6 +8061,7 @@ __export(index_exports, {
|
|
|
8060
8061
|
injectCommentsIntoPackage: () => injectCommentsIntoPackage2,
|
|
8061
8062
|
insertBodyElementBeforeSectPr: () => insertBodyElementBeforeSectPr,
|
|
8062
8063
|
inspectDocumentParts: () => inspectDocumentParts,
|
|
8064
|
+
inspectionReviewShape: () => inspectionReviewShape,
|
|
8063
8065
|
isLikelyStructuredTableSourceParagraph: () => isLikelyStructuredTableSourceParagraph,
|
|
8064
8066
|
isMarkdownTableText: () => isMarkdownTableText,
|
|
8065
8067
|
isNodeVisibleInRevisionView: () => isNodeVisibleInRevisionView,
|
|
@@ -8089,6 +8091,7 @@ __export(index_exports, {
|
|
|
8089
8091
|
repairKnownContentTypes: () => repairKnownContentTypes,
|
|
8090
8092
|
reserveNextNumberingId: () => reserveNextNumberingId,
|
|
8091
8093
|
reserveNextNumberingIdPair: () => reserveNextNumberingIdPair,
|
|
8094
|
+
resolveExistingRevisionsPolicy: () => resolveExistingRevisionsPolicy,
|
|
8092
8095
|
resolveParagraphRangeByRefs: () => resolveParagraphRangeByRefs,
|
|
8093
8096
|
resolveSingleLineListFallbackNumberingAction: () => resolveSingleLineListFallbackNumberingAction,
|
|
8094
8097
|
resolveTargetParagraph: () => resolveTargetParagraph,
|
|
@@ -8287,7 +8290,10 @@ var MARKDOWN_PATTERNS = [
|
|
|
8287
8290
|
{ regex: /\*\*\+\+(.+?)\+\+\*\*/, format: { bold: true, underline: true } },
|
|
8288
8291
|
// Bold: **text** or __text__
|
|
8289
8292
|
{ regex: /\*\*(.+?)\*\*/, format: { bold: true } },
|
|
8290
|
-
|
|
8293
|
+
// Underscore delimiters follow CommonMark flanking rules: not intraword, not part of a
|
|
8294
|
+
// longer underscore run, and never around whitespace. So signature lines ("_____")
|
|
8295
|
+
// and snake_case identifiers stay literal text.
|
|
8296
|
+
{ regex: /(?<![A-Za-z0-9_])__(?=[^_\s])(.+?)(?<=[^_\s])__(?![A-Za-z0-9_])/, format: { bold: true } },
|
|
8291
8297
|
// Underline: ++text++
|
|
8292
8298
|
{ regex: /\+\+(.+?)\+\+/, format: { underline: true } },
|
|
8293
8299
|
// Strikethrough: ~~text~~ or ~text~
|
|
@@ -8295,7 +8301,7 @@ var MARKDOWN_PATTERNS = [
|
|
|
8295
8301
|
{ regex: /~(.+?)~/, format: { strikethrough: true } },
|
|
8296
8302
|
// Italic: *text* or _text_ (using lookahead only for compatibility)
|
|
8297
8303
|
{ regex: /\*(?!\*)(.+?)\*(?!\*)/, format: { italic: true } },
|
|
8298
|
-
{ regex: /_(
|
|
8304
|
+
{ regex: /(?<![A-Za-z0-9_])_(?=[^_\s])(.+?)(?<=[^_\s])_(?![A-Za-z0-9_])/, format: { italic: true } }
|
|
8299
8305
|
];
|
|
8300
8306
|
function preprocessMarkdown(text2) {
|
|
8301
8307
|
if (!text2) {
|
|
@@ -10466,6 +10472,23 @@ function withOoxmlSourceType(result) {
|
|
|
10466
10472
|
}
|
|
10467
10473
|
return { ...result, sourceType: classifyOoxmlSourceType(result.oxml) };
|
|
10468
10474
|
}
|
|
10475
|
+
function hasUnattributedTrackedChange(node) {
|
|
10476
|
+
if (!node) return false;
|
|
10477
|
+
for (const localName4 of TRACKED_CHANGE_NAMES) {
|
|
10478
|
+
for (const change of wordElementsByLocalName(node, localName4)) {
|
|
10479
|
+
const author = change.getAttribute?.("w:author") || change.getAttribute?.("author") || (typeof change.getAttributeNS === "function" ? change.getAttributeNS(NS_W, "author") : null);
|
|
10480
|
+
if (!author || !String(author).trim()) return true;
|
|
10481
|
+
}
|
|
10482
|
+
}
|
|
10483
|
+
return false;
|
|
10484
|
+
}
|
|
10485
|
+
function resolveExistingRevisionsPolicy(policy, node, author) {
|
|
10486
|
+
if (policy !== "auto") return policy;
|
|
10487
|
+
const nodes = (Array.isArray(node) ? node : [node]).filter(Boolean);
|
|
10488
|
+
const own = String(author || "").trim().toLowerCase();
|
|
10489
|
+
const foreign = nodes.some((item) => containsTrackedChanges(item) && (hasUnattributedTrackedChange(item) || getTrackedChangeAuthors(item).some((name) => name.trim().toLowerCase() !== own)));
|
|
10490
|
+
return foreign ? "slice-cross-author" : "merge-same-author";
|
|
10491
|
+
}
|
|
10469
10492
|
|
|
10470
10493
|
// engine/paragraph-mark-revisions.js
|
|
10471
10494
|
function localNameOf(node) {
|
|
@@ -15976,7 +15999,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
15976
15999
|
const operationWarnings = [];
|
|
15977
16000
|
let normalizedExistingRevisions = false;
|
|
15978
16001
|
let preservedInsertedParagraphMark = null;
|
|
15979
|
-
|
|
16002
|
+
let existingRevisionsPolicy = options.existingRevisions || "merge-same-author";
|
|
15980
16003
|
const keepNormalizedNoOp = existingRevisionsPolicy === "accept-all-first-keep-normalized";
|
|
15981
16004
|
const finalize = (result) => {
|
|
15982
16005
|
const withStatus = { ...result };
|
|
@@ -16024,6 +16047,10 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
16024
16047
|
const parsed = parseOoxmlSafe(inputOoxml, "text/xml");
|
|
16025
16048
|
parseWarnings = parsed.warnings;
|
|
16026
16049
|
let xmlDoc = parsed.doc;
|
|
16050
|
+
if (existingRevisionsPolicy === "auto") {
|
|
16051
|
+
existingRevisionsPolicy = resolveExistingRevisionsPolicy("auto", xmlDoc, author);
|
|
16052
|
+
options = { ...options, existingRevisions: existingRevisionsPolicy };
|
|
16053
|
+
}
|
|
16027
16054
|
const parseError = xmlDoc ? getXmlParseError(xmlDoc) : null;
|
|
16028
16055
|
if (parsed.error || parseError) {
|
|
16029
16056
|
const message = parsed.error?.message || parseError?.textContent || "Could not parse OOXML input.";
|
|
@@ -16058,7 +16085,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
16058
16085
|
if (existingRevisionsPolicy === "merge-same-author" || existingRevisionsPolicy === "slice-cross-author") {
|
|
16059
16086
|
const authors = getTrackedChangeAuthors(xmlDoc);
|
|
16060
16087
|
const currentAuthor = String(author || "").trim().toLowerCase();
|
|
16061
|
-
const isSameAuthor = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === currentAuthor);
|
|
16088
|
+
const isSameAuthor = authors.length > 0 && !hasUnattributedTrackedChange(xmlDoc) && authors.every((a) => a.trim().toLowerCase() === currentAuthor);
|
|
16062
16089
|
if (isSameAuthor) {
|
|
16063
16090
|
const commentIds = getCommentIdsInOoxml(xmlDoc);
|
|
16064
16091
|
if (commentIds.length > 0) {
|
|
@@ -16215,7 +16242,7 @@ async function applyRedlineToOxml(oxml, originalText, modifiedText, options = {}
|
|
|
16215
16242
|
sanitizedText = structuredAnalysis.normalizedMarkdown;
|
|
16216
16243
|
}
|
|
16217
16244
|
}
|
|
16218
|
-
const { cleanText: cleanModifiedText, formatHints } = preprocessMarkdown(sanitizedText);
|
|
16245
|
+
const { cleanText: cleanModifiedText, formatHints } = options.plainTextContent === true ? { cleanText: sanitizedText, formatHints: [] } : preprocessMarkdown(sanitizedText);
|
|
16219
16246
|
const { existingFormatHints, textSpans, paragraphs } = extractFormattingFromOoxml(xmlDoc);
|
|
16220
16247
|
const hasLiteralTextChanges = existingRevisionsPolicy === "slice-cross-author" ? cleanModifiedText !== originalText : cleanModifiedText.trim() !== originalText.trim();
|
|
16221
16248
|
const hasListStructureChange = !hasLiteralTextChanges && options.explicitStructuredContent === true && isListTargetLoose(cleanModifiedText) && !sourceMatchesListShape(paragraphs, cleanModifiedText);
|
|
@@ -20047,6 +20074,70 @@ function inspectDocumentParts(parts, options = {}) {
|
|
|
20047
20074
|
};
|
|
20048
20075
|
}
|
|
20049
20076
|
|
|
20077
|
+
// services/inspection-comparison.js
|
|
20078
|
+
var commentIdentity = (comment) => [comment.author ?? null, comment.text ?? "", comment.anchoredText || ""];
|
|
20079
|
+
function reviewSegments(segments = []) {
|
|
20080
|
+
return segments.filter((segment) => segment.kind !== "baseline").reduce((result, segment) => {
|
|
20081
|
+
const next = {
|
|
20082
|
+
kind: segment.kind,
|
|
20083
|
+
author: segment.author || null,
|
|
20084
|
+
text: segment.text,
|
|
20085
|
+
acceptedStart: segment.acceptedStart ?? null,
|
|
20086
|
+
rejectedStart: segment.rejectedStart ?? null
|
|
20087
|
+
};
|
|
20088
|
+
const previous = result.at(-1);
|
|
20089
|
+
const adjacent = (key) => previous[key] == null && next[key] == null || previous[key] != null && previous[key] + previous.text.length === next[key];
|
|
20090
|
+
if (previous && previous.kind === next.kind && previous.author === next.author && adjacent("acceptedStart") && adjacent("rejectedStart")) previous.text += next.text;
|
|
20091
|
+
else result.push(next);
|
|
20092
|
+
return result;
|
|
20093
|
+
}, []);
|
|
20094
|
+
}
|
|
20095
|
+
function inspectionReviewShape(inspection) {
|
|
20096
|
+
const comments = inspection?.comments || [];
|
|
20097
|
+
return {
|
|
20098
|
+
paragraphs: (inspection?.paragraphs || []).map((paragraph) => ({
|
|
20099
|
+
text: paragraph.exactText,
|
|
20100
|
+
inTable: paragraph.inTable === true,
|
|
20101
|
+
table: paragraph.table ? [paragraph.table.rowIndex, paragraph.table.cellIndex] : null,
|
|
20102
|
+
list: paragraph.list ? [paragraph.list.format, paragraph.list.level, paragraph.list.label] : null,
|
|
20103
|
+
review: {
|
|
20104
|
+
present: paragraph.hasRevisions === true,
|
|
20105
|
+
authors: [...paragraph.revisionAuthors || []].sort(),
|
|
20106
|
+
segments: reviewSegments(paragraph.segments)
|
|
20107
|
+
}
|
|
20108
|
+
})),
|
|
20109
|
+
comments: comments.map((comment) => {
|
|
20110
|
+
const parent = comment.parentCommentId != null ? comments.find((item) => String(item.id) === String(comment.parentCommentId)) : null;
|
|
20111
|
+
return [
|
|
20112
|
+
...commentIdentity(comment),
|
|
20113
|
+
comment.done === true,
|
|
20114
|
+
parent ? commentIdentity(parent) : comment.parentCommentId != null || comment.parentParaId ? "unresolved parent" : null
|
|
20115
|
+
];
|
|
20116
|
+
}).map((item) => JSON.stringify(item)).sort()
|
|
20117
|
+
};
|
|
20118
|
+
}
|
|
20119
|
+
function compareDocumentInspections(expected, actual) {
|
|
20120
|
+
const left = inspectionReviewShape(expected);
|
|
20121
|
+
const right = inspectionReviewShape(actual);
|
|
20122
|
+
const differences = [];
|
|
20123
|
+
const note = (path, a, b) => {
|
|
20124
|
+
if (differences.length < 10 && JSON.stringify(a) !== JSON.stringify(b)) differences.push({ path, expected: a, actual: b });
|
|
20125
|
+
};
|
|
20126
|
+
note("paragraphs.length", left.paragraphs.length, right.paragraphs.length);
|
|
20127
|
+
const count = Math.max(left.paragraphs.length, right.paragraphs.length);
|
|
20128
|
+
for (let index = 0; index < count; index++) {
|
|
20129
|
+
const a = left.paragraphs[index] ?? null;
|
|
20130
|
+
const b = right.paragraphs[index] ?? null;
|
|
20131
|
+
if (!a || !b) {
|
|
20132
|
+
note(`paragraphs[${index}]`, a, b);
|
|
20133
|
+
continue;
|
|
20134
|
+
}
|
|
20135
|
+
for (const key of ["text", "inTable", "table", "list", "review"]) note(`paragraphs[${index}].${key}`, a[key], b[key]);
|
|
20136
|
+
}
|
|
20137
|
+
note("comments", left.comments, right.comments);
|
|
20138
|
+
return { matches: differences.length === 0, differences };
|
|
20139
|
+
}
|
|
20140
|
+
|
|
20050
20141
|
// services/comment-locator.js
|
|
20051
20142
|
function createParagraphTextIndex(paragraph, options = {}) {
|
|
20052
20143
|
const revisionView = options.revisionView === "current" ? "accepted" : options.revisionView || "accepted";
|
|
@@ -20521,7 +20612,7 @@ function applyCommentReplyToParts({ commentsXml, commentsExtendedXml = null, doc
|
|
|
20521
20612
|
extendedDoc = parsed.doc;
|
|
20522
20613
|
}
|
|
20523
20614
|
const occupied = usedParaIds(commentsDoc, extendedDoc);
|
|
20524
|
-
if (documentXml) for (const match of String(documentXml).matchAll(
|
|
20615
|
+
if (documentXml) for (const match of String(documentXml).matchAll(/\bw14:paraId="([0-9A-Fa-f]{8})"/g)) occupied.add(match[1].toUpperCase());
|
|
20525
20616
|
const parentParagraph = commentThreadParagraph(parent);
|
|
20526
20617
|
if (!parentParagraph) return { status: "error", error: { code: "PARENT_COMMENT_INVALID", message: `Parent comment '${parentCommentId}' has no paragraph.` } };
|
|
20527
20618
|
let parentParaId = commentParaId(parent);
|
|
@@ -22823,6 +22914,7 @@ var SUPPORTED_OPERATION_TYPES = /* @__PURE__ */ new Set([
|
|
|
22823
22914
|
"highlight"
|
|
22824
22915
|
]);
|
|
22825
22916
|
var EXISTING_REVISIONS_POLICIES = Object.freeze([
|
|
22917
|
+
"auto",
|
|
22826
22918
|
"merge-same-author",
|
|
22827
22919
|
"slice-cross-author",
|
|
22828
22920
|
"reject-input",
|
|
@@ -26877,13 +26969,17 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
26877
26969
|
}
|
|
26878
26970
|
};
|
|
26879
26971
|
}
|
|
26972
|
+
const autoExistingRevisions = options.existingRevisions === "auto";
|
|
26973
|
+
if (autoExistingRevisions) {
|
|
26974
|
+
options = { ...options, existingRevisions: resolveExistingRevisionsPolicy("auto", targetParagraph, author) };
|
|
26975
|
+
}
|
|
26880
26976
|
const hasRevisions = containsTrackedChanges(targetParagraph);
|
|
26881
26977
|
const existingPolicy = options.existingRevisions || "merge-same-author";
|
|
26882
26978
|
if (hasRevisions && existingPolicy !== "accept-all-first" && existingPolicy !== "accept-all-first-keep-normalized") {
|
|
26883
26979
|
if (existingPolicy === "merge-same-author" || existingPolicy === "slice-cross-author") {
|
|
26884
26980
|
const authors = getTrackedChangeAuthors(targetParagraph);
|
|
26885
26981
|
const opAuthor = String(author || "").trim().toLowerCase();
|
|
26886
|
-
const allSame = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === opAuthor);
|
|
26982
|
+
const allSame = authors.length > 0 && !hasUnattributedTrackedChange(targetParagraph) && authors.every((a) => a.trim().toLowerCase() === opAuthor);
|
|
26887
26983
|
if (!allSame && existingPolicy === "merge-same-author") {
|
|
26888
26984
|
return {
|
|
26889
26985
|
documentXml,
|
|
@@ -26892,7 +26988,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
26892
26988
|
status: "error",
|
|
26893
26989
|
error: {
|
|
26894
26990
|
code: "EXISTING_REVISIONS",
|
|
26895
|
-
message: `Target paragraph contains tracked changes from another author (${authors
|
|
26991
|
+
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.`,
|
|
26896
26992
|
revisionAuthors: authors,
|
|
26897
26993
|
currentPolicy: existingPolicy
|
|
26898
26994
|
}
|
|
@@ -27034,6 +27130,9 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
27034
27130
|
onWarn
|
|
27035
27131
|
}) : typeof targetText === "string" && /\r?\n/.test(targetText) ? findReconstructionParagraphRange(xmlDoc, targetText) : null;
|
|
27036
27132
|
const hasExplicitRangeScope = Array.isArray(explicitRangeParagraphs) && explicitRangeParagraphs.length > 0;
|
|
27133
|
+
if (autoExistingRevisions && hasExplicitRangeScope) {
|
|
27134
|
+
options = { ...options, existingRevisions: resolveExistingRevisionsPolicy("auto", explicitRangeParagraphs, author) };
|
|
27135
|
+
}
|
|
27037
27136
|
if (!useTableScope && hasExplicitRangeScope) {
|
|
27038
27137
|
const insertionEntries = buildExplicitRangeInsertionEntries(explicitRangeParagraphs, effectiveModifiedText);
|
|
27039
27138
|
if (insertionEntries && insertionEntries.length > 0) {
|
|
@@ -27359,6 +27458,7 @@ async function applyToParagraphByExactText(documentXml, targetText, modifiedText
|
|
|
27359
27458
|
pairReplacements: options.pairReplacements === true,
|
|
27360
27459
|
insertionAffinity: options.insertionAffinity || null,
|
|
27361
27460
|
literalTextEdit: options.literalTextEdit === true,
|
|
27461
|
+
plainTextContent: options.plainTextContent === true,
|
|
27362
27462
|
_revisionIdAllocator: revisionIdAllocator,
|
|
27363
27463
|
_isolatedTableCell: useTableScope
|
|
27364
27464
|
});
|
|
@@ -28369,7 +28469,31 @@ function verifyUntrackedLocality({ before, after, targetSourceIndex, beforeViews
|
|
|
28369
28469
|
}
|
|
28370
28470
|
return violations.length ? { ok: false, violations } : { ok: true };
|
|
28371
28471
|
}
|
|
28372
|
-
|
|
28472
|
+
var numberingDefinitionCache = /* @__PURE__ */ new Map();
|
|
28473
|
+
var NUMBERING_DEFINITION_CACHE_SIZE = 8;
|
|
28474
|
+
function parseNumberingDefinitions(text2) {
|
|
28475
|
+
const cached = numberingDefinitionCache.get(text2);
|
|
28476
|
+
if (cached) return cached;
|
|
28477
|
+
const abstractLevels = /* @__PURE__ */ new Map();
|
|
28478
|
+
for (const match of text2.matchAll(/<(?:\w+:)?abstractNum\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?abstractNum>/g)) {
|
|
28479
|
+
const id = /abstractNumId="([^"]*)"/.exec(match[1])?.[1];
|
|
28480
|
+
if (id == null) continue;
|
|
28481
|
+
const levels = abstractLevels.get(id) || /* @__PURE__ */ new Set();
|
|
28482
|
+
for (const lvl of match[2].matchAll(/<(?:\w+:)?lvl\b[^>]*?ilvl="([^"]*)"/g)) levels.add(lvl[1]);
|
|
28483
|
+
abstractLevels.set(id, levels);
|
|
28484
|
+
}
|
|
28485
|
+
const numToAbstract = /* @__PURE__ */ new Map();
|
|
28486
|
+
for (const match of text2.matchAll(/<(?:\w+:)?num\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?num>/g)) {
|
|
28487
|
+
const id = /numId="([^"]*)"/.exec(match[1])?.[1];
|
|
28488
|
+
const abstractId = /abstractNumId[^>]*?val="([^"]*)"/.exec(match[2])?.[1];
|
|
28489
|
+
if (id != null && abstractId != null) numToAbstract.set(id, abstractId);
|
|
28490
|
+
}
|
|
28491
|
+
const definitions = { abstractLevels, numToAbstract };
|
|
28492
|
+
if (numberingDefinitionCache.size >= NUMBERING_DEFINITION_CACHE_SIZE) numberingDefinitionCache.delete(numberingDefinitionCache.keys().next().value);
|
|
28493
|
+
numberingDefinitionCache.set(text2, definitions);
|
|
28494
|
+
return definitions;
|
|
28495
|
+
}
|
|
28496
|
+
function verifyNumberingReferences({ before, after, numberingXmls }) {
|
|
28373
28497
|
const references = (documentNode) => {
|
|
28374
28498
|
const found = /* @__PURE__ */ new Map();
|
|
28375
28499
|
for (const numIdNode of getAllReferencedNumIdElements(documentNode)) {
|
|
@@ -28385,21 +28509,7 @@ function verifyNumberingReferences({ before, after, numberingXmls, beforeViews =
|
|
|
28385
28509
|
const used = references(after);
|
|
28386
28510
|
if (used.size === 0) return { ok: true };
|
|
28387
28511
|
const known = references(before);
|
|
28388
|
-
const
|
|
28389
|
-
const abstractLevels = /* @__PURE__ */ new Map();
|
|
28390
|
-
for (const match of text2.matchAll(/<(?:\w+:)?abstractNum\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?abstractNum>/g)) {
|
|
28391
|
-
const id = /abstractNumId="([^"]*)"/.exec(match[1])?.[1];
|
|
28392
|
-
if (id == null) continue;
|
|
28393
|
-
const levels = abstractLevels.get(id) || /* @__PURE__ */ new Set();
|
|
28394
|
-
for (const lvl of match[2].matchAll(/<(?:\w+:)?lvl\b[^>]*?ilvl="([^"]*)"/g)) levels.add(lvl[1]);
|
|
28395
|
-
abstractLevels.set(id, levels);
|
|
28396
|
-
}
|
|
28397
|
-
const numToAbstract = /* @__PURE__ */ new Map();
|
|
28398
|
-
for (const match of text2.matchAll(/<(?:\w+:)?num\b([^>]*)>([\s\S]*?)<\/(?:\w+:)?num>/g)) {
|
|
28399
|
-
const id = /numId="([^"]*)"/.exec(match[1])?.[1];
|
|
28400
|
-
const abstractId = /abstractNumId[^>]*?val="([^"]*)"/.exec(match[2])?.[1];
|
|
28401
|
-
if (id != null && abstractId != null) numToAbstract.set(id, abstractId);
|
|
28402
|
-
}
|
|
28512
|
+
const { abstractLevels, numToAbstract } = parseNumberingDefinitions((numberingXmls || []).filter(Boolean).join("\n"));
|
|
28403
28513
|
const violations = [];
|
|
28404
28514
|
for (const [numId, levels] of used) {
|
|
28405
28515
|
if (known.has(numId)) continue;
|
|
@@ -28748,6 +28858,7 @@ async function applyOperationToDocumentXml(documentXml, op, author, runtimeConte
|
|
|
28748
28858
|
...operation.existingRevisions ? { existingRevisions: operation.existingRevisions } : {},
|
|
28749
28859
|
structuredContent: typeof operation.structuredContent === "boolean" ? operation.structuredContent : options.structuredContent !== false,
|
|
28750
28860
|
explicitStructuredContent: operation.structuredContent === true,
|
|
28861
|
+
plainTextContent: operation.structuredContent === false || operation.structuredContent == null && options.structuredContent === false,
|
|
28751
28862
|
...operation._localizedReplacementCompilation && operation.structuredContent !== true ? { literalTextEdit: true } : {},
|
|
28752
28863
|
pairReplacements: typeof operation.pairReplacements === "boolean" ? operation.pairReplacements : options.pairReplacements !== false,
|
|
28753
28864
|
...operation.insertionAffinity ? { insertionAffinity: operation.insertionAffinity } : {},
|
|
@@ -30002,35 +30113,44 @@ function preflightOperations(documentXml, operations, author, options = {}) {
|
|
|
30002
30113
|
const anchor = operation.operationKind === "comment" ? operation.textToComment || paragraphText : operation.operationKind === "highlight" ? operation.textToHighlight : operation.operationKind === "format" ? operation.textToFormat : null;
|
|
30003
30114
|
const anchorResolution = operation.operationKind === "comment" && anchor != null ? resolveTextInParagraphIndex(createParagraphTextIndex(paragraph, { revisionView: targetView }), anchor) : null;
|
|
30004
30115
|
const anchorFound = anchor == null || (anchorResolution ? anchorResolution.found : paragraphText.includes(anchor));
|
|
30116
|
+
const rangeParagraphs = (opType) => {
|
|
30117
|
+
if (operation.targetEndRef) {
|
|
30118
|
+
return resolveParagraphRangeByRefs(xmlDoc, operation.targetRef, operation.targetEndRef, {
|
|
30119
|
+
opType,
|
|
30120
|
+
targetRefSnapshot: options.targetRefSnapshot || null,
|
|
30121
|
+
onInfo: options.onInfo,
|
|
30122
|
+
onWarn: options.onWarn
|
|
30123
|
+
});
|
|
30124
|
+
}
|
|
30125
|
+
if (!operation.targetEndDescriptor) return [paragraph];
|
|
30126
|
+
const endResolved = resolveTargetParagraph(xmlDoc, {
|
|
30127
|
+
targetText: operation.targetEndDescriptor.text,
|
|
30128
|
+
targetRef: operation.targetEndDescriptor.index,
|
|
30129
|
+
targetDescriptor: operation.targetEndDescriptor,
|
|
30130
|
+
opType,
|
|
30131
|
+
strictAmbiguity: strictTargets,
|
|
30132
|
+
paragraphMetadataIndex: currentMetadataIndex,
|
|
30133
|
+
metadataIndices,
|
|
30134
|
+
onInfo: options.onInfo,
|
|
30135
|
+
onWarn: options.onWarn
|
|
30136
|
+
});
|
|
30137
|
+
const allParagraphs = getDocumentParagraphNodes(xmlDoc);
|
|
30138
|
+
const startIndex = allParagraphs.indexOf(paragraph);
|
|
30139
|
+
const endIndex = allParagraphs.indexOf(endResolved?.paragraph);
|
|
30140
|
+
return startIndex >= 0 && endIndex >= startIndex ? allParagraphs.slice(startIndex, endIndex + 1) : null;
|
|
30141
|
+
};
|
|
30005
30142
|
const hasRevisions = containsTrackedChanges(paragraph);
|
|
30006
|
-
const
|
|
30143
|
+
const requestedPolicy = operation.existingRevisions || options.existingRevisions || "merge-same-author";
|
|
30144
|
+
const existingPolicy = resolveExistingRevisionsPolicy(
|
|
30145
|
+
requestedPolicy,
|
|
30146
|
+
requestedPolicy === "auto" && operation.operationKind === "redline" && (operation.targetEndRef || operation.targetEndDescriptor) ? rangeParagraphs("redline") || paragraph : paragraph,
|
|
30147
|
+
authorUsed
|
|
30148
|
+
);
|
|
30007
30149
|
const deletingWholeParagraph = operation.operationKind === "redline" && operation.modified === "";
|
|
30008
30150
|
const commentIds = deletingWholeParagraph ? getCommentIdsInParagraph(paragraph) : [];
|
|
30009
30151
|
let error2 = null;
|
|
30010
30152
|
if (operation.operationKind === "restore") {
|
|
30011
|
-
|
|
30012
|
-
opType: "restore",
|
|
30013
|
-
targetRefSnapshot: options.targetRefSnapshot || null,
|
|
30014
|
-
onInfo: options.onInfo,
|
|
30015
|
-
onWarn: options.onWarn
|
|
30016
|
-
}) : [paragraph];
|
|
30017
|
-
if (!operation.targetEndRef && operation.targetEndDescriptor) {
|
|
30018
|
-
const endResolved = resolveTargetParagraph(xmlDoc, {
|
|
30019
|
-
targetText: operation.targetEndDescriptor.text,
|
|
30020
|
-
targetRef: operation.targetEndDescriptor.index,
|
|
30021
|
-
targetDescriptor: operation.targetEndDescriptor,
|
|
30022
|
-
opType: "restore",
|
|
30023
|
-
strictAmbiguity: strictTargets,
|
|
30024
|
-
paragraphMetadataIndex: currentMetadataIndex,
|
|
30025
|
-
metadataIndices,
|
|
30026
|
-
onInfo: options.onInfo,
|
|
30027
|
-
onWarn: options.onWarn
|
|
30028
|
-
});
|
|
30029
|
-
const allParagraphs = getDocumentParagraphNodes(xmlDoc);
|
|
30030
|
-
const startIndex = allParagraphs.indexOf(paragraph);
|
|
30031
|
-
const endIndex = allParagraphs.indexOf(endResolved?.paragraph);
|
|
30032
|
-
restorationParagraphs = startIndex >= 0 && endIndex >= startIndex ? allParagraphs.slice(startIndex, endIndex + 1) : null;
|
|
30033
|
-
}
|
|
30153
|
+
const restorationParagraphs = rangeParagraphs("restore");
|
|
30034
30154
|
const replacements = Array.isArray(operation.modified) ? operation.modified : [operation.modified];
|
|
30035
30155
|
if (!restorationParagraphs || restorationParagraphs[0] !== paragraph || replacements.length !== restorationParagraphs.length) {
|
|
30036
30156
|
error2 = {
|
|
@@ -30077,11 +30197,11 @@ function preflightOperations(documentXml, operations, author, options = {}) {
|
|
|
30077
30197
|
if (existingPolicy === "merge-same-author" || existingPolicy === "slice-cross-author") {
|
|
30078
30198
|
const authors = getTrackedChangeAuthors(paragraph);
|
|
30079
30199
|
const opAuthor = String(authorUsed || "").trim().toLowerCase();
|
|
30080
|
-
const allSame = authors.length > 0 && authors.every((a) => a.trim().toLowerCase() === opAuthor);
|
|
30200
|
+
const allSame = authors.length > 0 && !hasUnattributedTrackedChange(paragraph) && authors.every((a) => a.trim().toLowerCase() === opAuthor);
|
|
30081
30201
|
if (!allSame && existingPolicy === "merge-same-author") {
|
|
30082
30202
|
error2 = {
|
|
30083
30203
|
code: "EXISTING_REVISIONS",
|
|
30084
|
-
message: `Target paragraph contains tracked changes from another author (${authors
|
|
30204
|
+
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.`,
|
|
30085
30205
|
revisionAuthors: authors,
|
|
30086
30206
|
currentPolicy: existingPolicy
|
|
30087
30207
|
};
|