@adeu/core 1.14.0 → 1.15.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/dist/index.cjs +2198 -1788
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -12
- package/dist/index.d.ts +7 -12
- package/dist/index.js +2198 -1788
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/comment_dedup.test.ts +269 -0
- package/src/domain.test.ts +280 -77
- package/src/domain.ts +146 -46
- package/src/engine.qa.test.ts +162 -0
- package/src/engine.ts +274 -89
- package/src/ingest.ts +283 -133
- package/src/mapper.ts +425 -196
- package/src/models.ts +2 -0
- package/src/repro_heading_bug.test.ts +174 -0
- package/src/search_write_engine.test.ts +112 -0
package/src/domain.test.ts
CHANGED
|
@@ -1,55 +1,69 @@
|
|
|
1
|
-
import { describe, it, expect } from
|
|
2
|
-
import { createTestDocument, addParagraph } from
|
|
3
|
-
import { DocumentObject } from
|
|
4
|
-
import { extractTextFromBuffer } from
|
|
5
|
-
import { RedlineEngine, BatchValidationError } from
|
|
6
|
-
import { ModifyText } from
|
|
7
|
-
import { split_structural_appendix } from
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { createTestDocument, addParagraph } from "./test-utils.js";
|
|
3
|
+
import { DocumentObject } from "./docx/bridge.js";
|
|
4
|
+
import { extractTextFromBuffer } from "./ingest.js";
|
|
5
|
+
import { RedlineEngine, BatchValidationError } from "./engine.js";
|
|
6
|
+
import { ModifyText } from "./models.js";
|
|
7
|
+
import { split_structural_appendix } from "./pagination.js";
|
|
8
|
+
import { parseXml } from "./docx/dom.js";
|
|
9
|
+
|
|
10
|
+
function addBookmark(
|
|
11
|
+
paragraph: Element,
|
|
12
|
+
name: string,
|
|
13
|
+
idVal: string = "0",
|
|
14
|
+
text: string = "",
|
|
15
|
+
) {
|
|
10
16
|
const doc = paragraph.ownerDocument!;
|
|
11
|
-
const start = doc.createElement(
|
|
12
|
-
start.setAttribute(
|
|
13
|
-
start.setAttribute(
|
|
17
|
+
const start = doc.createElement("w:bookmarkStart");
|
|
18
|
+
start.setAttribute("w:name", name);
|
|
19
|
+
start.setAttribute("w:id", idVal);
|
|
14
20
|
paragraph.appendChild(start);
|
|
15
21
|
|
|
16
22
|
if (text) {
|
|
17
|
-
const r = doc.createElement(
|
|
18
|
-
const t = doc.createElement(
|
|
23
|
+
const r = doc.createElement("w:r");
|
|
24
|
+
const t = doc.createElement("w:t");
|
|
19
25
|
t.textContent = text;
|
|
20
|
-
if (text.includes(
|
|
26
|
+
if (text.includes(" ")) t.setAttribute("xml:space", "preserve");
|
|
21
27
|
r.appendChild(t);
|
|
22
28
|
paragraph.appendChild(r);
|
|
23
29
|
}
|
|
24
30
|
|
|
25
|
-
const end = doc.createElement(
|
|
26
|
-
end.setAttribute(
|
|
31
|
+
const end = doc.createElement("w:bookmarkEnd");
|
|
32
|
+
end.setAttribute("w:id", idVal);
|
|
27
33
|
paragraph.appendChild(end);
|
|
28
34
|
}
|
|
29
35
|
|
|
30
36
|
function addCrossReference(paragraph: Element, refName: string, text: string) {
|
|
31
37
|
const doc = paragraph.ownerDocument!;
|
|
32
|
-
const fld = doc.createElement(
|
|
33
|
-
fld.setAttribute(
|
|
34
|
-
const r = doc.createElement(
|
|
35
|
-
const t = doc.createElement(
|
|
38
|
+
const fld = doc.createElement("w:fldSimple");
|
|
39
|
+
fld.setAttribute("w:instr", ` REF ${refName} \\h `);
|
|
40
|
+
const r = doc.createElement("w:r");
|
|
41
|
+
const t = doc.createElement("w:t");
|
|
36
42
|
t.textContent = text;
|
|
37
|
-
if (text.includes(
|
|
43
|
+
if (text.includes(" ")) t.setAttribute("xml:space", "preserve");
|
|
38
44
|
r.appendChild(t);
|
|
39
45
|
fld.appendChild(r);
|
|
40
46
|
paragraph.appendChild(fld);
|
|
41
47
|
}
|
|
42
48
|
|
|
43
|
-
function addHyperlink(
|
|
44
|
-
|
|
49
|
+
function addHyperlink(
|
|
50
|
+
docObj: DocumentObject,
|
|
51
|
+
paragraph: Element,
|
|
52
|
+
url: string,
|
|
53
|
+
text: string,
|
|
54
|
+
) {
|
|
55
|
+
const idStr = docObj.relateToExternal(
|
|
56
|
+
url,
|
|
57
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
|
|
58
|
+
);
|
|
45
59
|
|
|
46
60
|
const doc = paragraph.ownerDocument!;
|
|
47
|
-
const hyperlink = doc.createElement(
|
|
48
|
-
hyperlink.setAttribute(
|
|
49
|
-
const r = doc.createElement(
|
|
50
|
-
const t = doc.createElement(
|
|
61
|
+
const hyperlink = doc.createElement("w:hyperlink");
|
|
62
|
+
hyperlink.setAttribute("r:id", idStr);
|
|
63
|
+
const r = doc.createElement("w:r");
|
|
64
|
+
const t = doc.createElement("w:t");
|
|
51
65
|
t.textContent = text;
|
|
52
|
-
if (text.includes(
|
|
66
|
+
if (text.includes(" ")) t.setAttribute("xml:space", "preserve");
|
|
53
67
|
r.appendChild(t);
|
|
54
68
|
hyperlink.appendChild(r);
|
|
55
69
|
paragraph.appendChild(hyperlink);
|
|
@@ -65,10 +79,12 @@ function setupFootnotesPart(docObj: DocumentObject) {
|
|
|
65
79
|
<w:p><w:r><w:t>Footnote content.</w:t></w:r></w:p>
|
|
66
80
|
</w:footnote>
|
|
67
81
|
</w:footnotes>`;
|
|
68
|
-
|
|
69
|
-
const partname =
|
|
70
|
-
const ctype =
|
|
71
|
-
|
|
82
|
+
|
|
83
|
+
const partname = "/word/footnotes.xml";
|
|
84
|
+
const ctype =
|
|
85
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml";
|
|
86
|
+
const relType =
|
|
87
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes";
|
|
72
88
|
|
|
73
89
|
const part = docObj.pkg.addPart(partname, ctype, fnXml);
|
|
74
90
|
docObj.relateTo(part, relType);
|
|
@@ -76,34 +92,43 @@ function setupFootnotesPart(docObj: DocumentObject) {
|
|
|
76
92
|
|
|
77
93
|
async function createDomainSemanticsStream() {
|
|
78
94
|
const doc = await createTestDocument();
|
|
79
|
-
|
|
95
|
+
|
|
80
96
|
// 1. Appendix / Definitions
|
|
81
97
|
const p1 = addParagraph(doc, "1. Definitions");
|
|
82
|
-
p1.insertBefore(p1.ownerDocument!.createElement(
|
|
83
|
-
addParagraph(
|
|
84
|
-
|
|
98
|
+
p1.insertBefore(p1.ownerDocument!.createElement("w:pPr"), p1.firstChild);
|
|
99
|
+
addParagraph(
|
|
100
|
+
doc,
|
|
101
|
+
'"Affiliate" means any entity that controls, is controlled by, or is under common control.',
|
|
102
|
+
);
|
|
103
|
+
addParagraph(
|
|
104
|
+
doc,
|
|
105
|
+
"“Confidential Information” means all non-public information disclosed by one party to the other.",
|
|
106
|
+
);
|
|
85
107
|
addParagraph(doc, "This paragraph does not define anything.");
|
|
86
108
|
|
|
87
109
|
const p2 = addParagraph(doc, "2. Obligations");
|
|
88
|
-
p2.insertBefore(p2.ownerDocument!.createElement(
|
|
89
|
-
addParagraph(
|
|
110
|
+
p2.insertBefore(p2.ownerDocument!.createElement("w:pPr"), p2.firstChild);
|
|
111
|
+
addParagraph(
|
|
112
|
+
doc,
|
|
113
|
+
"The Affiliate shall protect the Confidential Information to the highest standard.",
|
|
114
|
+
);
|
|
90
115
|
|
|
91
116
|
// 3. Bookmarks and Cross-References
|
|
92
117
|
const p3 = addParagraph(doc, "Subject to ");
|
|
93
118
|
addBookmark(p3, "MyBookmark_1", "1", "Anchored Clause");
|
|
94
|
-
const r3 = p3.ownerDocument!.createElement(
|
|
95
|
-
const t3 = p3.ownerDocument!.createElement(
|
|
119
|
+
const r3 = p3.ownerDocument!.createElement("w:r");
|
|
120
|
+
const t3 = p3.ownerDocument!.createElement("w:t");
|
|
96
121
|
t3.textContent = ", the parties agree to...";
|
|
97
|
-
t3.setAttribute(
|
|
122
|
+
t3.setAttribute("xml:space", "preserve");
|
|
98
123
|
r3.appendChild(t3);
|
|
99
124
|
p3.appendChild(r3);
|
|
100
125
|
|
|
101
126
|
const p4 = addParagraph(doc, "As strictly stated in ");
|
|
102
127
|
addCrossReference(p4, "MyBookmark_1", "Anchored Clause");
|
|
103
|
-
const r4 = p4.ownerDocument!.createElement(
|
|
104
|
-
const t4 = p4.ownerDocument!.createElement(
|
|
128
|
+
const r4 = p4.ownerDocument!.createElement("w:r");
|
|
129
|
+
const t4 = p4.ownerDocument!.createElement("w:t");
|
|
105
130
|
t4.textContent = ", either party may terminate.";
|
|
106
|
-
t4.setAttribute(
|
|
131
|
+
t4.setAttribute("xml:space", "preserve");
|
|
107
132
|
r4.appendChild(t4);
|
|
108
133
|
p4.appendChild(r4);
|
|
109
134
|
|
|
@@ -117,9 +142,9 @@ async function createDomainSemanticsStream() {
|
|
|
117
142
|
|
|
118
143
|
// 5. Footnotes
|
|
119
144
|
const pFn = addParagraph(doc, "Sentence with footnote");
|
|
120
|
-
const rFn = pFn.ownerDocument!.createElement(
|
|
121
|
-
const ref = pFn.ownerDocument!.createElement(
|
|
122
|
-
ref.setAttribute(
|
|
145
|
+
const rFn = pFn.ownerDocument!.createElement("w:r");
|
|
146
|
+
const ref = pFn.ownerDocument!.createElement("w:footnoteReference");
|
|
147
|
+
ref.setAttribute("w:id", "1");
|
|
123
148
|
rFn.appendChild(ref);
|
|
124
149
|
pFn.appendChild(rFn);
|
|
125
150
|
setupFootnotesPart(doc);
|
|
@@ -134,8 +159,8 @@ async function createDomainSemanticsStream() {
|
|
|
134
159
|
return doc.save();
|
|
135
160
|
}
|
|
136
161
|
|
|
137
|
-
describe(
|
|
138
|
-
it(
|
|
162
|
+
describe("Domain Semantics Engine", () => {
|
|
163
|
+
it("extracts and projects structural appendix and diagnostics correctly", async () => {
|
|
139
164
|
const buf = await createDomainSemanticsStream();
|
|
140
165
|
const text = await extractTextFromBuffer(buf);
|
|
141
166
|
|
|
@@ -171,17 +196,74 @@ describe('Domain Semantics Engine', () => {
|
|
|
171
196
|
});
|
|
172
197
|
|
|
173
198
|
const edgeCases = [
|
|
174
|
-
{
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
{
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
199
|
+
{
|
|
200
|
+
target: "# Document Structure (Read-Only)",
|
|
201
|
+
newText: "# Modified Document Structure",
|
|
202
|
+
errChecker: (m: string) =>
|
|
203
|
+
m.includes("read-only boundary") || m.includes("appendix"),
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
target: "Sentence with footnote[^fn-1]",
|
|
207
|
+
newText: "Sentence with footnote",
|
|
208
|
+
errChecker: (m: string) =>
|
|
209
|
+
m.includes("footnote") &&
|
|
210
|
+
(m.includes("delete") || m.includes("remove")),
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
target: "Sentence with footnote",
|
|
214
|
+
newText: "Sentence with footnote[^fn-99]",
|
|
215
|
+
errChecker: (m: string) =>
|
|
216
|
+
m.includes("footnote") &&
|
|
217
|
+
(m.includes("insert") || m.includes("create")),
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
target: "Some text.",
|
|
221
|
+
newText: "Some text.{#_Ref99999}",
|
|
222
|
+
errChecker: (m: string) => m.includes("anchor"),
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
target: "Section 5. Indemnification{#_Ref12345}",
|
|
226
|
+
newText: "Section 5. Indemnification{#_Ref99999}",
|
|
227
|
+
errChecker: (m: string) => m.includes("anchor"),
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
target: "[~Section 5~](#_Ref12345)",
|
|
231
|
+
newText: "[~Section 6~](#_Ref12345)",
|
|
232
|
+
errChecker: (m: string) =>
|
|
233
|
+
m.includes("cross-reference") || m.includes("rejected"),
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
target: "[~Section 5~](#_Ref12345)",
|
|
237
|
+
newText: "[~Section 5~](#_Ref99999)",
|
|
238
|
+
errChecker: (m: string) =>
|
|
239
|
+
m.includes("dependency corruption") || m.includes("rejected"),
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
target: "As detailed in [~Section 5~](#_Ref12345)",
|
|
243
|
+
newText:
|
|
244
|
+
"As detailed in [~Section 5~](#_Ref12345) and [~Section 6~](#_Ref999)",
|
|
245
|
+
errChecker: (m: string) =>
|
|
246
|
+
m.includes("cross-reference") || m.includes("read-only"),
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
target: "As detailed in [~Section 5~](#_Ref12345)",
|
|
250
|
+
newText: "As detailed in nothing",
|
|
251
|
+
errChecker: (m: string) =>
|
|
252
|
+
m.includes("cross-reference") || m.includes("delete"),
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
target: "Please visit [Adeu HQ](https://adeu.com)",
|
|
256
|
+
newText:
|
|
257
|
+
"Please visit [Adeu HQ](https://adeu.com) and [Google](https://google.com)",
|
|
258
|
+
errChecker: (m: string) =>
|
|
259
|
+
m.includes("hyperlink") || m.includes("insert"),
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
target: "Please visit [Adeu HQ](https://adeu.com)",
|
|
263
|
+
newText: "Please visit nothing",
|
|
264
|
+
errChecker: (m: string) =>
|
|
265
|
+
m.includes("hyperlink") || m.includes("delete"),
|
|
266
|
+
},
|
|
185
267
|
];
|
|
186
268
|
|
|
187
269
|
for (const tc of edgeCases) {
|
|
@@ -189,7 +271,11 @@ describe('Domain Semantics Engine', () => {
|
|
|
189
271
|
const buf = await createDomainSemanticsStream();
|
|
190
272
|
const doc = await DocumentObject.load(buf);
|
|
191
273
|
const engine = new RedlineEngine(doc);
|
|
192
|
-
const edit: ModifyText = {
|
|
274
|
+
const edit: ModifyText = {
|
|
275
|
+
type: "modify",
|
|
276
|
+
target_text: tc.target,
|
|
277
|
+
new_text: tc.newText,
|
|
278
|
+
};
|
|
193
279
|
|
|
194
280
|
let errorThrown = false;
|
|
195
281
|
try {
|
|
@@ -197,7 +283,7 @@ describe('Domain Semantics Engine', () => {
|
|
|
197
283
|
} catch (e) {
|
|
198
284
|
errorThrown = true;
|
|
199
285
|
if (e instanceof BatchValidationError) {
|
|
200
|
-
const msg = e.errors.join(
|
|
286
|
+
const msg = e.errors.join("\n").toLowerCase();
|
|
201
287
|
expect(tc.errChecker(msg)).toBe(true);
|
|
202
288
|
} else {
|
|
203
289
|
throw e; // unexpected error
|
|
@@ -207,23 +293,27 @@ describe('Domain Semantics Engine', () => {
|
|
|
207
293
|
});
|
|
208
294
|
}
|
|
209
295
|
|
|
210
|
-
it(
|
|
296
|
+
it("safely edits footnotes and accepts changes", async () => {
|
|
211
297
|
const buf = await createDomainSemanticsStream();
|
|
212
298
|
const doc = await DocumentObject.load(buf);
|
|
213
299
|
const engine = new RedlineEngine(doc);
|
|
214
300
|
|
|
215
|
-
const edit: ModifyText = {
|
|
301
|
+
const edit: ModifyText = {
|
|
302
|
+
type: "modify",
|
|
303
|
+
target_text: "Footnote content.",
|
|
304
|
+
new_text: "This is an edited footnote.",
|
|
305
|
+
};
|
|
216
306
|
const stats = engine.process_batch([edit]);
|
|
217
307
|
expect(stats.edits_applied).toBe(1);
|
|
218
308
|
|
|
219
309
|
engine.accept_all_revisions();
|
|
220
310
|
const outBuf = await doc.save();
|
|
221
311
|
const cleanText = await extractTextFromBuffer(outBuf, true);
|
|
222
|
-
|
|
312
|
+
|
|
223
313
|
expect(cleanText).toContain("[^fn-1]: This is an edited footnote.");
|
|
224
314
|
});
|
|
225
315
|
|
|
226
|
-
it(
|
|
316
|
+
it("extracts defined terms and finds typos correctly", async () => {
|
|
227
317
|
const doc = await createTestDocument();
|
|
228
318
|
addParagraph(doc, '"Agreement" means this contract.');
|
|
229
319
|
addParagraph(doc, "“Party” shall mean either side.");
|
|
@@ -231,17 +321,23 @@ describe('Domain Semantics Engine', () => {
|
|
|
231
321
|
addParagraph(doc, 'This contract (hereinafter, the "Contract") is valid.');
|
|
232
322
|
addParagraph(doc, '"Confidential Information" on salainen asia.');
|
|
233
323
|
addParagraph(doc, '1.1 "Affiliate" tarkoittaa osakkuusyhtiötä.');
|
|
234
|
-
addParagraph(
|
|
324
|
+
addParagraph(
|
|
325
|
+
doc,
|
|
326
|
+
'We will act as the disclosing party (jäljempänä "Discloser").',
|
|
327
|
+
);
|
|
235
328
|
addParagraph(doc, 'This is a syntax example: ("Heading*") and ("<Term>")');
|
|
236
|
-
|
|
329
|
+
|
|
237
330
|
addParagraph(doc, "The Agreement is binding. The Contract is signed.");
|
|
238
331
|
addParagraph(doc, "There is an Agrement here.");
|
|
239
|
-
addParagraph(
|
|
332
|
+
addParagraph(
|
|
333
|
+
doc,
|
|
334
|
+
"We shared Confidential Information with the Affiliate. The Discloser is happy.",
|
|
335
|
+
);
|
|
240
336
|
|
|
241
337
|
const buf = await doc.save();
|
|
242
338
|
const full_text = await extractTextFromBuffer(buf, false);
|
|
243
339
|
const [, appendix] = split_structural_appendix(full_text);
|
|
244
|
-
|
|
340
|
+
|
|
245
341
|
expect(appendix).toContain('"Agreement" \u2014 used');
|
|
246
342
|
expect(appendix).toContain('"Contract" \u2014 used');
|
|
247
343
|
expect(appendix).toContain('"Confidential Information" \u2014 used');
|
|
@@ -252,11 +348,15 @@ describe('Domain Semantics Engine', () => {
|
|
|
252
348
|
expect(appendix).not.toContain('"Heading*"');
|
|
253
349
|
expect(appendix).not.toContain('"<Term>"');
|
|
254
350
|
|
|
255
|
-
expect(appendix).toContain(
|
|
256
|
-
|
|
351
|
+
expect(appendix).toContain(
|
|
352
|
+
"[Error] Duplicate Definition: 'Agreement' is defined multiple times.",
|
|
353
|
+
);
|
|
354
|
+
expect(appendix).toContain(
|
|
355
|
+
"[Info] Possible Typos for 'Agreement': Found 'Agrement'",
|
|
356
|
+
);
|
|
257
357
|
});
|
|
258
358
|
|
|
259
|
-
it(
|
|
359
|
+
it("reduces typo noise for short acronyms", async () => {
|
|
260
360
|
const doc = await createTestDocument();
|
|
261
361
|
addParagraph(doc, '"PSUs" means power supply units.');
|
|
262
362
|
addParagraph(doc, '"CPU" means central processing unit.');
|
|
@@ -270,11 +370,114 @@ describe('Domain Semantics Engine', () => {
|
|
|
270
370
|
const buf = await doc.save();
|
|
271
371
|
const full_text = await extractTextFromBuffer(buf, false);
|
|
272
372
|
const [, appendix] = split_structural_appendix(full_text);
|
|
273
|
-
|
|
274
|
-
expect(appendix).toContain(
|
|
373
|
+
|
|
374
|
+
expect(appendix).toContain(
|
|
375
|
+
"[Info] Possible Typos for 'Party': Found 'Pary'",
|
|
376
|
+
);
|
|
275
377
|
expect(appendix).not.toContain("'GPU'");
|
|
276
378
|
expect(appendix).not.toContain("'GPUs'");
|
|
277
379
|
expect(appendix).not.toContain("'ESAs'");
|
|
278
380
|
expect(appendix).not.toContain("'LSPs'");
|
|
279
381
|
});
|
|
280
|
-
|
|
382
|
+
|
|
383
|
+
describe("Privacy-flag warnings in Document Settings section", () => {
|
|
384
|
+
// Helper to attach a synthetic word/settings.xml with the given inner XML
|
|
385
|
+
// (children of <w:settings>). Replaces any existing settings part.
|
|
386
|
+
function installSettings(doc: any, innerXml: string) {
|
|
387
|
+
const fullXml = `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
388
|
+
<w:settings xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main">${innerXml}</w:settings>`;
|
|
389
|
+
doc.pkg.parts = doc.pkg.parts.filter(
|
|
390
|
+
(p: any) =>
|
|
391
|
+
p.partname !== "/word/settings.xml" &&
|
|
392
|
+
p.partname !== "word/settings.xml",
|
|
393
|
+
);
|
|
394
|
+
doc.pkg.addPart(
|
|
395
|
+
"/word/settings.xml",
|
|
396
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml",
|
|
397
|
+
fullXml,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
it("emits warnings when removePersonalInformation and removeDateAndTime are enabled", async () => {
|
|
402
|
+
const doc = await createTestDocument();
|
|
403
|
+
addParagraph(doc, "Some body text so the doc is non-empty.");
|
|
404
|
+
installSettings(
|
|
405
|
+
doc,
|
|
406
|
+
'<w:removePersonalInformation w:val="true"/><w:removeDateAndTime w:val="true"/>',
|
|
407
|
+
);
|
|
408
|
+
|
|
409
|
+
const buf = await doc.save();
|
|
410
|
+
const full_text = await extractTextFromBuffer(buf, false);
|
|
411
|
+
const [, appendix] = split_structural_appendix(full_text);
|
|
412
|
+
|
|
413
|
+
expect(appendix).toContain("## Document Settings");
|
|
414
|
+
expect(appendix).toContain("`removePersonalInformation` is enabled");
|
|
415
|
+
expect(appendix).toContain("`removeDateAndTime` is enabled");
|
|
416
|
+
expect(appendix).toContain("[Warning]");
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it("treats an element with no w:val as truthy (OOXML default)", async () => {
|
|
420
|
+
const doc = await createTestDocument();
|
|
421
|
+
addParagraph(doc, "Body.");
|
|
422
|
+
installSettings(doc, "<w:removePersonalInformation/>");
|
|
423
|
+
|
|
424
|
+
const buf = await doc.save();
|
|
425
|
+
const full_text = await extractTextFromBuffer(buf, false);
|
|
426
|
+
const [, appendix] = split_structural_appendix(full_text);
|
|
427
|
+
|
|
428
|
+
expect(appendix).toContain("`removePersonalInformation` is enabled");
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
it.each(["0", "false", "off", "FALSE"])(
|
|
432
|
+
'does NOT warn when the flag is explicitly disabled (w:val="%s")',
|
|
433
|
+
async (val) => {
|
|
434
|
+
const doc = await createTestDocument();
|
|
435
|
+
addParagraph(doc, "Body.");
|
|
436
|
+
installSettings(
|
|
437
|
+
doc,
|
|
438
|
+
`<w:removePersonalInformation w:val="${val}"/><w:removeDateAndTime w:val="${val}"/>`,
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
const buf = await doc.save();
|
|
442
|
+
const full_text = await extractTextFromBuffer(buf, false);
|
|
443
|
+
const [, appendix] = split_structural_appendix(full_text);
|
|
444
|
+
|
|
445
|
+
expect(appendix).not.toContain("## Document Settings");
|
|
446
|
+
expect(appendix).not.toContain(
|
|
447
|
+
"`removePersonalInformation` is enabled",
|
|
448
|
+
);
|
|
449
|
+
expect(appendix).not.toContain("`removeDateAndTime` is enabled");
|
|
450
|
+
},
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
it("does NOT emit the section when no privacy flags are present", async () => {
|
|
454
|
+
const doc = await createTestDocument();
|
|
455
|
+
addParagraph(doc, "Body with no privacy flags.");
|
|
456
|
+
installSettings(doc, '<w:zoom w:percent="100"/>');
|
|
457
|
+
|
|
458
|
+
const buf = await doc.save();
|
|
459
|
+
const full_text = await extractTextFromBuffer(buf, false);
|
|
460
|
+
const [, appendix] = split_structural_appendix(full_text);
|
|
461
|
+
|
|
462
|
+
expect(appendix).not.toContain("## Document Settings");
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it("places ## Document Settings before ## Defined Terms in the appendix", async () => {
|
|
466
|
+
const doc = await createTestDocument();
|
|
467
|
+
addParagraph(doc, '"MyTerm" means something important.');
|
|
468
|
+
addParagraph(doc, "The MyTerm is referenced again here.");
|
|
469
|
+
installSettings(doc, '<w:removePersonalInformation w:val="true"/>');
|
|
470
|
+
|
|
471
|
+
const buf = await doc.save();
|
|
472
|
+
const full_text = await extractTextFromBuffer(buf, false);
|
|
473
|
+
const [, appendix] = split_structural_appendix(full_text);
|
|
474
|
+
|
|
475
|
+
const settingsIdx = appendix.indexOf("## Document Settings");
|
|
476
|
+
const termsIdx = appendix.indexOf("## Defined Terms");
|
|
477
|
+
|
|
478
|
+
expect(settingsIdx).toBeGreaterThan(-1);
|
|
479
|
+
expect(termsIdx).toBeGreaterThan(-1);
|
|
480
|
+
expect(settingsIdx).toBeLessThan(termsIdx);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
});
|