@ansonlai/docx-redline-js 0.1.6 → 0.2.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 +19 -3
- package/README.md +15 -0
- package/core/redline-validation.js +156 -0
- package/dist/docx-redline-js.esm.js +125 -1
- package/dist/docx-redline-js.esm.js.map +4 -4
- package/dist/docx-redline-js.esm.min.js +61 -61
- package/dist/docx-redline-js.esm.min.js.map +4 -4
- package/docs/VALIDATION.md +79 -23
- package/docs/plans/2026-05-31-architectural changes.md +591 -0
- package/index.d.ts +24 -0
- package/index.js +35 -34
- package/package.json +3 -2
- package/scripts/build.mjs +10 -5
- package/scripts/check-types.mjs +2 -1
- package/scripts/export-validation-fixtures.mjs +73 -16
- package/scripts/lib/minimal-zip.mjs +155 -0
- package/scripts/validate-fixtures-xsd.sh +37 -0
- package/scripts/word-com-differential.ps1 +133 -0
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
|
-
|
|
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
|
+
// @ansonlai/docx-redline-js v0.2.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;
|
|
@@ -7715,6 +7715,129 @@ async function executeSingleLineListStructuralFallback(plan, options = {}) {
|
|
|
7715
7715
|
};
|
|
7716
7716
|
}
|
|
7717
7717
|
|
|
7718
|
+
// core/redline-validation.js
|
|
7719
|
+
var REVISION_ID_ELEMENTS = /* @__PURE__ */ new Set(["ins", "del", "rPrChange", "pPrChange"]);
|
|
7720
|
+
var REVISION_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}T/;
|
|
7721
|
+
function localNameOf(node) {
|
|
7722
|
+
return String(node?.localName || node?.nodeName || "").replace(/^.*:/, "");
|
|
7723
|
+
}
|
|
7724
|
+
function elementsByLocalName(root, name) {
|
|
7725
|
+
return Array.from(root.getElementsByTagName("*")).filter((el) => localNameOf(el) === name);
|
|
7726
|
+
}
|
|
7727
|
+
function wordAttribute(node, name) {
|
|
7728
|
+
return node.getAttribute(`w:${name}`) || node.getAttribute(name) || "";
|
|
7729
|
+
}
|
|
7730
|
+
function xmlSpaceAttribute(node) {
|
|
7731
|
+
return node.getAttribute("xml:space") || node.getAttribute("space") || node.getAttributeNS?.("http://www.w3.org/XML/1998/namespace", "space") || "";
|
|
7732
|
+
}
|
|
7733
|
+
function isParagraphMarkRevision(node) {
|
|
7734
|
+
return localNameOf(node.parentNode) === "rPr";
|
|
7735
|
+
}
|
|
7736
|
+
function parseOoxmlForValidation(oxml) {
|
|
7737
|
+
const attempt = (xml) => {
|
|
7738
|
+
const doc = parseXml(xml);
|
|
7739
|
+
const parseError = doc.getElementsByTagName("parsererror")[0];
|
|
7740
|
+
if (parseError) {
|
|
7741
|
+
throw new Error(parseError.textContent || "XML parse error");
|
|
7742
|
+
}
|
|
7743
|
+
return doc;
|
|
7744
|
+
};
|
|
7745
|
+
try {
|
|
7746
|
+
return { doc: attempt(oxml) };
|
|
7747
|
+
} catch {
|
|
7748
|
+
try {
|
|
7749
|
+
return { doc: attempt(`<w:root xmlns:w="${NS_W}">${oxml}</w:root>`) };
|
|
7750
|
+
} catch (error2) {
|
|
7751
|
+
return { error: error2?.message || "XML parse error" };
|
|
7752
|
+
}
|
|
7753
|
+
}
|
|
7754
|
+
}
|
|
7755
|
+
function validateRedlineOoxml(oxml) {
|
|
7756
|
+
const issues = [];
|
|
7757
|
+
const addIssue = (code, severity, message) => issues.push({ code, severity, message });
|
|
7758
|
+
if (typeof oxml !== "string" || oxml.trim() === "") {
|
|
7759
|
+
addIssue("PARSE_ERROR", "error", "Input is not a non-empty OOXML string.");
|
|
7760
|
+
return { valid: false, issues };
|
|
7761
|
+
}
|
|
7762
|
+
const { doc, error: error2 } = parseOoxmlForValidation(oxml);
|
|
7763
|
+
if (!doc) {
|
|
7764
|
+
addIssue("PARSE_ERROR", "error", `OOXML does not parse as XML: ${error2}`);
|
|
7765
|
+
return { valid: false, issues };
|
|
7766
|
+
}
|
|
7767
|
+
const insElements = elementsByLocalName(doc, "ins");
|
|
7768
|
+
const delElements = elementsByLocalName(doc, "del");
|
|
7769
|
+
const revisions = insElements.concat(delElements);
|
|
7770
|
+
for (const revision of revisions) {
|
|
7771
|
+
const nested = Array.from(revision.getElementsByTagName("*")).filter((el) => el !== revision && ["ins", "del"].includes(localNameOf(el)));
|
|
7772
|
+
if (nested.length > 0) {
|
|
7773
|
+
addIssue(
|
|
7774
|
+
"NESTED_REVISION",
|
|
7775
|
+
"error",
|
|
7776
|
+
`<${revision.nodeName}> (w:id="${wordAttribute(revision, "id")}") contains nested <${nested[0].nodeName}>.`
|
|
7777
|
+
);
|
|
7778
|
+
}
|
|
7779
|
+
}
|
|
7780
|
+
for (const del of delElements) {
|
|
7781
|
+
const plainTextNodes = elementsByLocalName(del, "t");
|
|
7782
|
+
if (plainTextNodes.length > 0) {
|
|
7783
|
+
addIssue(
|
|
7784
|
+
"DEL_CONTAINS_T",
|
|
7785
|
+
"error",
|
|
7786
|
+
`<w:del> (w:id="${wordAttribute(del, "id")}") contains <w:t>; deleted text must use <w:delText>.`
|
|
7787
|
+
);
|
|
7788
|
+
}
|
|
7789
|
+
}
|
|
7790
|
+
for (const revision of revisions) {
|
|
7791
|
+
const missing = [];
|
|
7792
|
+
if (!wordAttribute(revision, "id")) missing.push("w:id");
|
|
7793
|
+
if (!wordAttribute(revision, "author")) missing.push("w:author");
|
|
7794
|
+
if (!REVISION_DATE_PATTERN.test(wordAttribute(revision, "date"))) missing.push("w:date");
|
|
7795
|
+
if (missing.length > 0) {
|
|
7796
|
+
addIssue(
|
|
7797
|
+
"MISSING_REVISION_METADATA",
|
|
7798
|
+
"error",
|
|
7799
|
+
`<${revision.nodeName}> is missing or has malformed ${missing.join(", ")}.`
|
|
7800
|
+
);
|
|
7801
|
+
}
|
|
7802
|
+
}
|
|
7803
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
7804
|
+
for (const node of Array.from(doc.getElementsByTagName("*"))) {
|
|
7805
|
+
if (!REVISION_ID_ELEMENTS.has(localNameOf(node))) continue;
|
|
7806
|
+
const id = wordAttribute(node, "id");
|
|
7807
|
+
if (!id) continue;
|
|
7808
|
+
if (seenIds.has(id)) {
|
|
7809
|
+
addIssue("DUPLICATE_REVISION_ID", "error", `Revision id ${id} appears more than once.`);
|
|
7810
|
+
}
|
|
7811
|
+
seenIds.add(id);
|
|
7812
|
+
}
|
|
7813
|
+
const textNodes = elementsByLocalName(doc, "t").concat(elementsByLocalName(doc, "delText"));
|
|
7814
|
+
for (const node of textNodes) {
|
|
7815
|
+
const text = node.textContent || "";
|
|
7816
|
+
if (/^\s|\s$/.test(text) && xmlSpaceAttribute(node) !== "preserve") {
|
|
7817
|
+
addIssue(
|
|
7818
|
+
"MISSING_SPACE_PRESERVE",
|
|
7819
|
+
"error",
|
|
7820
|
+
`<${node.nodeName}> has boundary whitespace without xml:space="preserve".`
|
|
7821
|
+
);
|
|
7822
|
+
}
|
|
7823
|
+
if (text === "") {
|
|
7824
|
+
addIssue("EMPTY_TEXT_ELEMENT", "warning", `<${node.nodeName}> is empty.`);
|
|
7825
|
+
}
|
|
7826
|
+
}
|
|
7827
|
+
for (const revision of revisions) {
|
|
7828
|
+
if (isParagraphMarkRevision(revision)) continue;
|
|
7829
|
+
const hasElementChild = Array.from(revision.childNodes || []).some((child) => child.nodeType === 1);
|
|
7830
|
+
if (!hasElementChild) {
|
|
7831
|
+
addIssue(
|
|
7832
|
+
"EMPTY_REVISION_WRAPPER",
|
|
7833
|
+
"warning",
|
|
7834
|
+
`<${revision.nodeName}> (w:id="${wordAttribute(revision, "id")}") wraps no content.`
|
|
7835
|
+
);
|
|
7836
|
+
}
|
|
7837
|
+
}
|
|
7838
|
+
return { valid: !issues.some((issue) => issue.severity === "error"), issues };
|
|
7839
|
+
}
|
|
7840
|
+
|
|
7718
7841
|
// core/table-targeting.js
|
|
7719
7842
|
function getDirectWordChildren(element, localName) {
|
|
7720
7843
|
if (!element) return [];
|
|
@@ -9578,6 +9701,7 @@ export {
|
|
|
9578
9701
|
synthesizeExpandedListScopeEdit,
|
|
9579
9702
|
synthesizeTableMarkdownFromMultilineCellEdit,
|
|
9580
9703
|
validateDocxPackage,
|
|
9704
|
+
validateRedlineOoxml,
|
|
9581
9705
|
wrapInDocumentFragment
|
|
9582
9706
|
};
|
|
9583
9707
|
//# sourceMappingURL=docx-redline-js.esm.js.map
|