@ansonlai/docx-redline-js 0.1.6 → 0.2.1

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 CHANGED
@@ -72,6 +72,8 @@ No Word add-in entrypoints or host-specific integration layers are part of this
72
72
  - Namespace-safe Word element creation, tracked-change detection, and OOXML payload source-shape helpers.
73
73
  - `core/types.js`
74
74
  - Shared model enums/types plus revision metadata generation and document-aware revision ID seeding.
75
+ - `core/redline-validation.js`
76
+ - Runtime structural validation (`validateRedlineOoxml`) mirroring the test-suite invariants: no nested revisions, `w:delText` inside `w:del`, complete revision metadata, unique revision ids, preserved boundary whitespace.
75
77
  - `engine/oxml-engine.js`
76
78
  - Main reconciliation router, mode selection, existing-revision policy gate, and status/error result handling.
77
79
  - `engine/run-builders.js`
@@ -128,6 +130,8 @@ still be re-exported from `index.js`.
128
130
  - Do not write unknown `result.oxml` payloads directly into `word/document.xml`;
129
131
  normalize with `extractReplacementNodesFromOoxml(...)` or use
130
132
  `applyOperationToDocumentXml(...).documentXml` for full-document replacement.
133
+ - Run `validateRedlineOoxml(oxml)` on generated output before packaging it;
134
+ it reports structural invariant violations as `{ valid, issues }`.
131
135
 
132
136
 
133
137
  ## Build Output
@@ -150,11 +154,23 @@ The bundle inlines `diff-match-patch` and keeps `@xmldom/xmldom` external.
150
154
  - `npm run check:types`
151
155
  - Smoke-checks `index.d.ts`.
152
156
  - `node scripts/export-validation-fixtures.mjs`
153
- - Writes release-time validation fixtures to `tmp/validation-docx/`.
157
+ - Writes release-time validation fixtures to `tmp/validation-docx/` as
158
+ `word/document.xml` parts, assembled `.docx` files, and expected-text sidecars.
159
+ - `tests/roundtrip_fuzz_tests.mjs` (part of `npm test`)
160
+ - Seeded fuzz sweep of the accept/reject round-trip invariant; tune with
161
+ `FUZZ_SEED` / `FUZZ_ITERATIONS`.
154
162
  - `npm run smoke:word -- path/to/file.docx`
155
163
  - Optional Windows/Word COM smoke test for a completed `.docx`.
156
-
157
- Use these checks before publishing or tagging.
164
+ - `npm run smoke:word:diff`
165
+ - Windows/Word COM differential test: Word itself accepts/rejects the
166
+ generated fixtures and the resulting text is compared to expectations.
167
+ - `bash scripts/validate-fixtures-xsd.sh`
168
+ - Validates exported fixtures against the ECMA-376 transitional `wml.xsd`.
169
+ - `.github/workflows/validation.yml`
170
+ - Nightly independent-oracle validation: XSD schema check, LibreOffice
171
+ conversion, and an extended 20k-case fuzz sweep with a fresh seed.
172
+
173
+ Use these checks before publishing or tagging. See `docs/VALIDATION.md`.
158
174
 
159
175
  ## Fast Orientation For Contributors
160
176
 
package/README.md CHANGED
@@ -139,6 +139,7 @@ Common result fields:
139
139
  | `ingestOoxml(oxml)` | Flatten OOXML into an internal run model with offsets. |
140
140
  | `preprocessMarkdown(text)` | Normalize markdown and extract format hints. |
141
141
  | `containsTrackedChanges(xmlDoc)` | Detect `w:ins`, `w:del`, move revisions, property changes, and paragraph-mark revision markup in a parsed OOXML document/fragment. |
142
+ | `validateRedlineOoxml(oxml)` | Validate generated redline OOXML against the package's structural invariants (no nested revisions, `w:delText` inside `w:del`, complete metadata, unique revision ids, preserved boundary whitespace). Returns `{ valid, issues }`; run it before writing output into a package. |
142
143
 
143
144
  ### Services
144
145
 
@@ -259,6 +260,20 @@ On Windows with desktop Word installed, you can smoke-test a completed `.docx`:
259
260
  npm run smoke:word -- path/to/file.docx
260
261
  ```
261
262
 
263
+ To validate against Word as an independent oracle (Word itself accepts and
264
+ rejects the generated revisions and the resulting text is compared to the
265
+ expected outcomes):
266
+
267
+ ```bash
268
+ node scripts/export-validation-fixtures.mjs
269
+ npm run smoke:word:diff
270
+ ```
271
+
272
+ A nightly GitHub Actions workflow additionally validates generated fixtures
273
+ against the ECMA-376 transitional schemas (`xmllint`), opens them with
274
+ LibreOffice, and runs an extended fuzz sweep of the accept/reject round-trip
275
+ invariant with a fresh seed. See [docs/VALIDATION.md](./docs/VALIDATION.md).
276
+
262
277
  ## Architecture
263
278
 
264
279
  See [ARCHITECTURE.md](./ARCHITECTURE.md) for module layout, data flow, and contributor guidance.
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Runtime structural validation for generated redline OOXML.
3
+ *
4
+ * Mirrors the invariants enforced by the test-suite round-trip harness so
5
+ * downstream consumers can verify output before writing it into a package:
6
+ * no nested revisions, deleted text uses w:delText, revision metadata is
7
+ * complete, revision ids are unique, and boundary whitespace is preserved.
8
+ */
9
+
10
+ import { parseXml } from '../adapters/xml-adapter.js';
11
+ import { NS_W } from './types.js';
12
+
13
+ const REVISION_ID_ELEMENTS = new Set(['ins', 'del', 'rPrChange', 'pPrChange']);
14
+ const REVISION_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}T/;
15
+
16
+ function localNameOf(node) {
17
+ return String(node?.localName || node?.nodeName || '').replace(/^.*:/, '');
18
+ }
19
+
20
+ function elementsByLocalName(root, name) {
21
+ return Array.from(root.getElementsByTagName('*')).filter(el => localNameOf(el) === name);
22
+ }
23
+
24
+ function wordAttribute(node, name) {
25
+ return node.getAttribute(`w:${name}`) || node.getAttribute(name) || '';
26
+ }
27
+
28
+ function xmlSpaceAttribute(node) {
29
+ return node.getAttribute('xml:space') ||
30
+ node.getAttribute('space') ||
31
+ node.getAttributeNS?.('http://www.w3.org/XML/1998/namespace', 'space') ||
32
+ '';
33
+ }
34
+
35
+ function isParagraphMarkRevision(node) {
36
+ return localNameOf(node.parentNode) === 'rPr';
37
+ }
38
+
39
+ function parseOoxmlForValidation(oxml) {
40
+ const attempt = xml => {
41
+ const doc = parseXml(xml);
42
+ const parseError = doc.getElementsByTagName('parsererror')[0];
43
+ if (parseError) {
44
+ throw new Error(parseError.textContent || 'XML parse error');
45
+ }
46
+ return doc;
47
+ };
48
+
49
+ try {
50
+ return { doc: attempt(oxml) };
51
+ } catch {
52
+ try {
53
+ return { doc: attempt(`<w:root xmlns:w="${NS_W}">${oxml}</w:root>`) };
54
+ } catch (error) {
55
+ return { error: error?.message || 'XML parse error' };
56
+ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Validates redline OOXML against the package's structural invariants.
62
+ *
63
+ * Issue severities: 'error' issues indicate output Word may repair or
64
+ * mis-resolve; 'warning' issues are suspicious but tolerated by Word.
65
+ *
66
+ * @param {string} oxml - OOXML string (fragment, document, or package scope)
67
+ * @returns {{ valid: boolean, issues: Array<{ code: string, severity: 'error'|'warning', message: string }> }}
68
+ */
69
+ export function validateRedlineOoxml(oxml) {
70
+ const issues = [];
71
+ const addIssue = (code, severity, message) => issues.push({ code, severity, message });
72
+
73
+ if (typeof oxml !== 'string' || oxml.trim() === '') {
74
+ addIssue('PARSE_ERROR', 'error', 'Input is not a non-empty OOXML string.');
75
+ return { valid: false, issues };
76
+ }
77
+
78
+ const { doc, error } = parseOoxmlForValidation(oxml);
79
+ if (!doc) {
80
+ addIssue('PARSE_ERROR', 'error', `OOXML does not parse as XML: ${error}`);
81
+ return { valid: false, issues };
82
+ }
83
+
84
+ const insElements = elementsByLocalName(doc, 'ins');
85
+ const delElements = elementsByLocalName(doc, 'del');
86
+ const revisions = insElements.concat(delElements);
87
+
88
+ // No w:ins/w:del nested inside another w:ins/w:del.
89
+ for (const revision of revisions) {
90
+ const nested = Array.from(revision.getElementsByTagName('*'))
91
+ .filter(el => el !== revision && ['ins', 'del'].includes(localNameOf(el)));
92
+ if (nested.length > 0) {
93
+ addIssue('NESTED_REVISION', 'error',
94
+ `<${revision.nodeName}> (w:id="${wordAttribute(revision, 'id')}") contains nested <${nested[0].nodeName}>.`);
95
+ }
96
+ }
97
+
98
+ // Deleted runs must carry w:delText, never w:t.
99
+ for (const del of delElements) {
100
+ const plainTextNodes = elementsByLocalName(del, 't');
101
+ if (plainTextNodes.length > 0) {
102
+ addIssue('DEL_CONTAINS_T', 'error',
103
+ `<w:del> (w:id="${wordAttribute(del, 'id')}") contains <w:t>; deleted text must use <w:delText>.`);
104
+ }
105
+ }
106
+
107
+ // Every revision needs complete metadata.
108
+ for (const revision of revisions) {
109
+ const missing = [];
110
+ if (!wordAttribute(revision, 'id')) missing.push('w:id');
111
+ if (!wordAttribute(revision, 'author')) missing.push('w:author');
112
+ if (!REVISION_DATE_PATTERN.test(wordAttribute(revision, 'date'))) missing.push('w:date');
113
+ if (missing.length > 0) {
114
+ addIssue('MISSING_REVISION_METADATA', 'error',
115
+ `<${revision.nodeName}> is missing or has malformed ${missing.join(', ')}.`);
116
+ }
117
+ }
118
+
119
+ // Revision ids must be unique among ins/del/rPrChange/pPrChange.
120
+ const seenIds = new Set();
121
+ for (const node of Array.from(doc.getElementsByTagName('*'))) {
122
+ if (!REVISION_ID_ELEMENTS.has(localNameOf(node))) continue;
123
+ const id = wordAttribute(node, 'id');
124
+ if (!id) continue;
125
+ if (seenIds.has(id)) {
126
+ addIssue('DUPLICATE_REVISION_ID', 'error', `Revision id ${id} appears more than once.`);
127
+ }
128
+ seenIds.add(id);
129
+ }
130
+
131
+ // Boundary whitespace requires xml:space="preserve".
132
+ const textNodes = elementsByLocalName(doc, 't').concat(elementsByLocalName(doc, 'delText'));
133
+ for (const node of textNodes) {
134
+ const text = node.textContent || '';
135
+ if (/^\s|\s$/.test(text) && xmlSpaceAttribute(node) !== 'preserve') {
136
+ addIssue('MISSING_SPACE_PRESERVE', 'error',
137
+ `<${node.nodeName}> has boundary whitespace without xml:space="preserve".`);
138
+ }
139
+ if (text === '') {
140
+ addIssue('EMPTY_TEXT_ELEMENT', 'warning', `<${node.nodeName}> is empty.`);
141
+ }
142
+ }
143
+
144
+ // Empty w:ins/w:del wrappers (paragraph-mark revisions inside w:rPr are
145
+ // legitimately empty and excluded).
146
+ for (const revision of revisions) {
147
+ if (isParagraphMarkRevision(revision)) continue;
148
+ const hasElementChild = Array.from(revision.childNodes || []).some(child => child.nodeType === 1);
149
+ if (!hasElementChild) {
150
+ addIssue('EMPTY_REVISION_WRAPPER', 'warning',
151
+ `<${revision.nodeName}> (w:id="${wordAttribute(revision, 'id')}") wraps no content.`);
152
+ }
153
+ }
154
+
155
+ return { valid: !issues.some(issue => issue.severity === 'error'), issues };
156
+ }
@@ -1,4 +1,4 @@
1
- // @ansonlai/docx-redline-js v0.1.6 — https://github.com/AnsonLai/docx-redline-js
1
+ // @ansonlai/docx-redline-js v0.2.1 — 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;
@@ -6,7 +6,11 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
8
  var __commonJS = (cb, mod) => function __require() {
9
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
9
+ try {
10
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ } catch (e) {
12
+ throw mod = 0, e;
13
+ }
10
14
  };
11
15
  var __copyProps = (to, from, except, desc) => {
12
16
  if (from && typeof from === "object" || typeof from === "function") {
@@ -4425,11 +4429,11 @@ function createFormattedRuns(xmlDoc, text, baseRPr, formatHints, baseOffset, aut
4425
4429
  const applicableHints = formatHints.filter(
4426
4430
  (h) => h.start <= segmentBaseOffset && h.end >= segmentEndOffset
4427
4431
  );
4428
- const combinedFormat = {};
4432
+ const combinedFormat = { ...extractFormatFromRPr(baseRPr) };
4429
4433
  applicableHints.forEach((h) => {
4430
4434
  if (h.format) Object.assign(combinedFormat, h.format);
4431
4435
  });
4432
- const formattedRPr = injectFormattingToRPr(xmlDoc, baseRPr, combinedFormat, author, generateRedlines);
4436
+ const formattedRPr = applicableHints.length > 0 ? injectFormattingToRPr(xmlDoc, baseRPr, combinedFormat, author, generateRedlines) : baseRPr?.cloneNode(true) || null;
4433
4437
  runs.push(createTextRunWithRPrElement(xmlDoc, segment, formattedRPr, false));
4434
4438
  }
4435
4439
  return runs;
@@ -4549,6 +4553,7 @@ function isWordElement3(node, localName) {
4549
4553
  return nodeName === `w:${localName}` || nodeName === localName;
4550
4554
  }
4551
4555
  function buildParagraphInfos(xmlDoc, paragraphs, textSpans) {
4556
+ void xmlDoc;
4552
4557
  const spansByParagraph = /* @__PURE__ */ new Map();
4553
4558
  for (const span of textSpans) {
4554
4559
  if (!span || !span.paragraph) continue;
@@ -4753,6 +4758,7 @@ function normalizePrecomputedFormatContext(precomputedContext) {
4753
4758
  };
4754
4759
  }
4755
4760
  function applyFormatRemovalAsSurgicalReplacement(xmlDoc, textSpans, existingFormatHints, serializer, author, generateRedlines = true) {
4761
+ void textSpans;
4756
4762
  let hasAnyChanges = false;
4757
4763
  const processedRuns = /* @__PURE__ */ new Set();
4758
4764
  log(`[OxmlEngine] Surgical format removal: ${existingFormatHints.length} hints to process (using w:rPrChange)`);
@@ -5208,26 +5214,11 @@ function cloneRunPiece(xmlDoc, sourceNode, text, asDeletedText) {
5208
5214
 
5209
5215
  // engine/surgical-diff-application.js
5210
5216
  function reconcileFormattingForTextSpan(xmlDoc, span, start, end, applicableHints, author, generateRedlines) {
5211
- const desiredFormat = {};
5212
- if (applicableHints.length > 0) {
5213
- applicableHints.forEach((h) => Object.assign(desiredFormat, h.format));
5214
- }
5217
+ if (applicableHints.length === 0) return false;
5215
5218
  const rPr = span.rPr;
5216
- const hasElement = (localName) => {
5217
- if (!rPr) return false;
5218
- for (let node = rPr.firstChild; node; node = node.nextSibling) {
5219
- if (isWordElement(node, localName)) {
5220
- return true;
5221
- }
5222
- }
5223
- return false;
5224
- };
5225
- const existingFormat = {
5226
- bold: hasElement("b"),
5227
- italic: hasElement("i"),
5228
- underline: hasElement("u"),
5229
- strikethrough: hasElement("strike")
5230
- };
5219
+ const existingFormat = extractFormatFromRPr(rPr);
5220
+ const desiredFormat = { ...existingFormat };
5221
+ applicableHints.forEach((h) => Object.assign(desiredFormat, h.format));
5231
5222
  const formatsToCheck = ["bold", "italic", "underline", "strikethrough"];
5232
5223
  const changesNeeded = formatsToCheck.some((f) => !!desiredFormat[f] !== existingFormat[f]);
5233
5224
  if (!changesNeeded) return false;
@@ -5361,6 +5352,7 @@ function insertTextRuns(xmlDoc, parent, referenceNode, text, baseRPr, author, fo
5361
5352
 
5362
5353
  // engine/surgical-mode.js
5363
5354
  function applySurgicalMode(xmlDoc, originalText, modifiedText, serializer, author, formatHints, generateRedlines = true, targetParagraph = null) {
5355
+ void originalText;
5364
5356
  const allParagraphs = targetParagraph ? [targetParagraph] : getDocumentParagraphs(xmlDoc);
5365
5357
  const { fullText, textSpans } = buildSurgicalTextSpans(allParagraphs);
5366
5358
  const diffs = computeWordDiffs(fullText, modifiedText);
@@ -7715,6 +7707,129 @@ async function executeSingleLineListStructuralFallback(plan, options = {}) {
7715
7707
  };
7716
7708
  }
7717
7709
 
7710
+ // core/redline-validation.js
7711
+ var REVISION_ID_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "rPrChange", "pPrChange"]);
7712
+ var REVISION_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}T/;
7713
+ function localNameOf(node) {
7714
+ return String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
7715
+ }
7716
+ function elementsByLocalName(root, name) {
7717
+ return Array.from(root.getElementsByTagName("*")).filter((el) => localNameOf(el) === name);
7718
+ }
7719
+ function wordAttribute(node, name) {
7720
+ return node.getAttribute(`w:${name}`) || node.getAttribute(name) || "";
7721
+ }
7722
+ function xmlSpaceAttribute(node) {
7723
+ return node.getAttribute("xml:space") || node.getAttribute("space") || node.getAttributeNS?.("http://www.w3.org/XML/1998/namespace", "space") || "";
7724
+ }
7725
+ function isParagraphMarkRevision(node) {
7726
+ return localNameOf(node.parentNode) === "rPr";
7727
+ }
7728
+ function parseOoxmlForValidation(oxml) {
7729
+ const attempt = (xml) => {
7730
+ const doc = parseXml(xml);
7731
+ const parseError = doc.getElementsByTagName("parsererror")[0];
7732
+ if (parseError) {
7733
+ throw new Error(parseError.textContent || "XML parse error");
7734
+ }
7735
+ return doc;
7736
+ };
7737
+ try {
7738
+ return { doc: attempt(oxml) };
7739
+ } catch {
7740
+ try {
7741
+ return { doc: attempt(`<w:root xmlns:w="${NS_W}">${oxml}</w:root>`) };
7742
+ } catch (error2) {
7743
+ return { error: error2?.message || "XML parse error" };
7744
+ }
7745
+ }
7746
+ }
7747
+ function validateRedlineOoxml(oxml) {
7748
+ const issues = [];
7749
+ const addIssue = (code, severity, message) => issues.push({ code, severity, message });
7750
+ if (typeof oxml !== "string" || oxml.trim() === "") {
7751
+ addIssue("PARSE_ERROR", "error", "Input is not a non-empty OOXML string.");
7752
+ return { valid: false, issues };
7753
+ }
7754
+ const { doc, error: error2 } = parseOoxmlForValidation(oxml);
7755
+ if (!doc) {
7756
+ addIssue("PARSE_ERROR", "error", `OOXML does not parse as XML: ${error2}`);
7757
+ return { valid: false, issues };
7758
+ }
7759
+ const insElements = elementsByLocalName(doc, "ins");
7760
+ const delElements = elementsByLocalName(doc, "del");
7761
+ const revisions = insElements.concat(delElements);
7762
+ for (const revision of revisions) {
7763
+ const nested = Array.from(revision.getElementsByTagName("*")).filter((el) => el !== revision && ["ins", "del"].includes(localNameOf(el)));
7764
+ if (nested.length > 0) {
7765
+ addIssue(
7766
+ "NESTED_REVISION",
7767
+ "error",
7768
+ `<${revision.nodeName}> (w:id="${wordAttribute(revision, "id")}") contains nested <${nested[0].nodeName}>.`
7769
+ );
7770
+ }
7771
+ }
7772
+ for (const del of delElements) {
7773
+ const plainTextNodes = elementsByLocalName(del, "t");
7774
+ if (plainTextNodes.length > 0) {
7775
+ addIssue(
7776
+ "DEL_CONTAINS_T",
7777
+ "error",
7778
+ `<w:del> (w:id="${wordAttribute(del, "id")}") contains <w:t>; deleted text must use <w:delText>.`
7779
+ );
7780
+ }
7781
+ }
7782
+ for (const revision of revisions) {
7783
+ const missing = [];
7784
+ if (!wordAttribute(revision, "id")) missing.push("w:id");
7785
+ if (!wordAttribute(revision, "author")) missing.push("w:author");
7786
+ if (!REVISION_DATE_PATTERN.test(wordAttribute(revision, "date"))) missing.push("w:date");
7787
+ if (missing.length > 0) {
7788
+ addIssue(
7789
+ "MISSING_REVISION_METADATA",
7790
+ "error",
7791
+ `<${revision.nodeName}> is missing or has malformed ${missing.join(", ")}.`
7792
+ );
7793
+ }
7794
+ }
7795
+ const seenIds = /* @__PURE__ */ new Set();
7796
+ for (const node of Array.from(doc.getElementsByTagName("*"))) {
7797
+ if (!REVISION_ID_ELEMENTS.has(localNameOf(node))) continue;
7798
+ const id = wordAttribute(node, "id");
7799
+ if (!id) continue;
7800
+ if (seenIds.has(id)) {
7801
+ addIssue("DUPLICATE_REVISION_ID", "error", `Revision id ${id} appears more than once.`);
7802
+ }
7803
+ seenIds.add(id);
7804
+ }
7805
+ const textNodes = elementsByLocalName(doc, "t").concat(elementsByLocalName(doc, "delText"));
7806
+ for (const node of textNodes) {
7807
+ const text = node.textContent || "";
7808
+ if (/^\s|\s$/.test(text) && xmlSpaceAttribute(node) !== "preserve") {
7809
+ addIssue(
7810
+ "MISSING_SPACE_PRESERVE",
7811
+ "error",
7812
+ `<${node.nodeName}> has boundary whitespace without xml:space="preserve".`
7813
+ );
7814
+ }
7815
+ if (text === "") {
7816
+ addIssue("EMPTY_TEXT_ELEMENT", "warning", `<${node.nodeName}> is empty.`);
7817
+ }
7818
+ }
7819
+ for (const revision of revisions) {
7820
+ if (isParagraphMarkRevision(revision)) continue;
7821
+ const hasElementChild = Array.from(revision.childNodes || []).some((child) => child.nodeType === 1);
7822
+ if (!hasElementChild) {
7823
+ addIssue(
7824
+ "EMPTY_REVISION_WRAPPER",
7825
+ "warning",
7826
+ `<${revision.nodeName}> (w:id="${wordAttribute(revision, "id")}") wraps no content.`
7827
+ );
7828
+ }
7829
+ }
7830
+ return { valid: !issues.some((issue) => issue.severity === "error"), issues };
7831
+ }
7832
+
7718
7833
  // core/table-targeting.js
7719
7834
  function getDirectWordChildren(element, localName) {
7720
7835
  if (!element) return [];
@@ -8679,6 +8794,11 @@ function injectCommentsIntoPackage2(packageOxml, commentsXml) {
8679
8794
  }
8680
8795
 
8681
8796
  // engine/formatting-removal.js
8797
+ function removeNode2(node) {
8798
+ if (node?.parentNode) {
8799
+ node.parentNode.removeChild(node);
8800
+ }
8801
+ }
8682
8802
  function removeFormattingFromRPr(rPr, formatTypes = ["all"]) {
8683
8803
  if (!rPr) return null;
8684
8804
  const rPrClone = rPr.cloneNode(true);
@@ -8702,7 +8822,7 @@ function removeFormattingFromRPr(rPr, formatTypes = ["all"]) {
8702
8822
  ];
8703
8823
  toRemove.forEach((tag) => {
8704
8824
  const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace("w:", "")}`);
8705
- elements.forEach((el) => el.remove());
8825
+ elements.forEach(removeNode2);
8706
8826
  });
8707
8827
  } else {
8708
8828
  const tagMap = {
@@ -8724,7 +8844,7 @@ function removeFormattingFromRPr(rPr, formatTypes = ["all"]) {
8724
8844
  const tag = tagMap[type];
8725
8845
  if (tag) {
8726
8846
  const elements = rPrClone.querySelectorAll(`${tag}, ${tag.replace("w:", "")}`);
8727
- elements.forEach((el) => el.remove());
8847
+ elements.forEach(removeNode2);
8728
8848
  }
8729
8849
  });
8730
8850
  }
@@ -8754,7 +8874,7 @@ function applyFormattingRemovalToOoxml(ooxmlString, targetText, formatTypes) {
8754
8874
  rPr.parentNode.replaceChild(newRPr, rPr);
8755
8875
  }
8756
8876
  } else {
8757
- rPr.remove();
8877
+ removeNode2(rPr);
8758
8878
  }
8759
8879
  }
8760
8880
  }
@@ -8800,7 +8920,7 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
8800
8920
  });
8801
8921
  }
8802
8922
  const existingHighlight = rPrElement.getElementsByTagNameNS(NS_W7, "highlight");
8803
- Array.from(existingHighlight).forEach((el) => el.remove());
8923
+ Array.from(existingHighlight).forEach(removeNode2);
8804
8924
  const highlightEl = createWordElement(doc, "w:highlight");
8805
8925
  highlightEl.setAttributeNS(NS_W7, "w:val", ooxmlColor);
8806
8926
  rPrElement.appendChild(highlightEl);
@@ -8812,7 +8932,7 @@ function injectHighlightIntoRPr(doc, rPr, color = "yellow", options = {}) {
8812
8932
  rPrChange.setAttribute("w:date", metadata.date);
8813
8933
  rPrChange.appendChild(previousRPrState);
8814
8934
  const existingChange = rPrElement.getElementsByTagNameNS(NS_W7, "rPrChange");
8815
- Array.from(existingChange).forEach((el) => el.remove());
8935
+ Array.from(existingChange).forEach(removeNode2);
8816
8936
  rPrElement.appendChild(rPrChange);
8817
8937
  }
8818
8938
  return rPrElement;
@@ -8844,7 +8964,7 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
8844
8964
  if (prefixText.length > 0) {
8845
8965
  const prefixRun = run.cloneNode(true);
8846
8966
  const tNodes = prefixRun.getElementsByTagNameNS(NS_W7, "t");
8847
- Array.from(tNodes).forEach((t) => t.remove());
8967
+ Array.from(tNodes).forEach(removeNode2);
8848
8968
  const newT = createWordElement(doc, "w:t");
8849
8969
  newT.setAttribute("xml:space", "preserve");
8850
8970
  newT.textContent = prefixText;
@@ -8854,7 +8974,7 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
8854
8974
  if (matchText.length > 0) {
8855
8975
  const matchRun = run.cloneNode(true);
8856
8976
  const tNodes = matchRun.getElementsByTagNameNS(NS_W7, "t");
8857
- Array.from(tNodes).forEach((t) => t.remove());
8977
+ Array.from(tNodes).forEach(removeNode2);
8858
8978
  const newT = createWordElement(doc, "w:t");
8859
8979
  newT.setAttribute("xml:space", "preserve");
8860
8980
  newT.textContent = matchText;
@@ -8872,7 +8992,7 @@ function applyHighlightToOoxml(ooxmlString, targetText, color = "yellow", option
8872
8992
  if (suffixText.length > 0) {
8873
8993
  const suffixRun = run.cloneNode(true);
8874
8994
  const tNodes = suffixRun.getElementsByTagNameNS(NS_W7, "t");
8875
- Array.from(tNodes).forEach((t) => t.remove());
8995
+ Array.from(tNodes).forEach(removeNode2);
8876
8996
  const newT = createWordElement(doc, "w:t");
8877
8997
  newT.setAttribute("xml:space", "preserve");
8878
8998
  newT.textContent = suffixText;
@@ -9578,6 +9698,7 @@ export {
9578
9698
  synthesizeExpandedListScopeEdit,
9579
9699
  synthesizeTableMarkdownFromMultilineCellEdit,
9580
9700
  validateDocxPackage,
9701
+ validateRedlineOoxml,
9581
9702
  wrapInDocumentFragment
9582
9703
  };
9583
9704
  //# sourceMappingURL=docx-redline-js.esm.js.map