@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.
@@ -1,55 +1,69 @@
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
-
9
- function addBookmark(paragraph: Element, name: string, idVal: string = "0", text: string = "") {
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('w:bookmarkStart');
12
- start.setAttribute('w:name', name);
13
- start.setAttribute('w:id', idVal);
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('w:r');
18
- const t = doc.createElement('w:t');
23
+ const r = doc.createElement("w:r");
24
+ const t = doc.createElement("w:t");
19
25
  t.textContent = text;
20
- if (text.includes(' ')) t.setAttribute('xml:space', 'preserve');
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('w:bookmarkEnd');
26
- end.setAttribute('w:id', idVal);
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('w:fldSimple');
33
- fld.setAttribute('w:instr', ` REF ${refName} \\h `);
34
- const r = doc.createElement('w:r');
35
- const t = doc.createElement('w:t');
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(' ')) t.setAttribute('xml:space', 'preserve');
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(docObj: DocumentObject, paragraph: Element, url: string, text: string) {
44
- const idStr = docObj.relateToExternal(url, 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink');
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('w:hyperlink');
48
- hyperlink.setAttribute('r:id', idStr);
49
- const r = doc.createElement('w:r');
50
- const t = doc.createElement('w:t');
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(' ')) t.setAttribute('xml:space', 'preserve');
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 = '/word/footnotes.xml';
70
- const ctype = 'application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml';
71
- const relType = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes';
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('w:pPr'), p1.firstChild);
83
- addParagraph(doc, '"Affiliate" means any entity that controls, is controlled by, or is under common control.');
84
- addParagraph(doc, "“Confidential Information” means all non-public information disclosed by one party to the other.");
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('w:pPr'), p2.firstChild);
89
- addParagraph(doc, "The Affiliate shall protect the Confidential Information to the highest standard.");
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('w:r');
95
- const t3 = p3.ownerDocument!.createElement('w:t');
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('xml:space', 'preserve');
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('w:r');
104
- const t4 = p4.ownerDocument!.createElement('w:t');
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('xml:space', 'preserve');
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('w:r');
121
- const ref = pFn.ownerDocument!.createElement('w:footnoteReference');
122
- ref.setAttribute('w:id', "1");
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('Domain Semantics Engine', () => {
138
- it('extracts and projects structural appendix and diagnostics correctly', async () => {
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
- { target: "# Document Structure (Read-Only)", newText: "# Modified Document Structure", errChecker: (m: string) => m.includes('read-only boundary') || m.includes('appendix') },
175
- { target: "Sentence with footnote[^fn-1]", newText: "Sentence with footnote", errChecker: (m: string) => m.includes('footnote') && (m.includes('delete') || m.includes('remove')) },
176
- { target: "Sentence with footnote", newText: "Sentence with footnote[^fn-99]", errChecker: (m: string) => m.includes('footnote') && (m.includes('insert') || m.includes('create')) },
177
- { target: "Some text.", newText: "Some text.{#_Ref99999}", errChecker: (m: string) => m.includes('anchor') },
178
- { target: "Section 5. Indemnification{#_Ref12345}", newText: "Section 5. Indemnification{#_Ref99999}", errChecker: (m: string) => m.includes('anchor') },
179
- { target: "[~Section 5~](#_Ref12345)", newText: "[~Section 6~](#_Ref12345)", errChecker: (m: string) => m.includes('cross-reference') || m.includes('rejected') },
180
- { target: "[~Section 5~](#_Ref12345)", newText: "[~Section 5~](#_Ref99999)", errChecker: (m: string) => m.includes('dependency corruption') || m.includes('rejected') },
181
- { target: "As detailed in [~Section 5~](#_Ref12345)", newText: "As detailed in [~Section 5~](#_Ref12345) and [~Section 6~](#_Ref999)", errChecker: (m: string) => m.includes('cross-reference') || m.includes('read-only') },
182
- { target: "As detailed in [~Section 5~](#_Ref12345)", newText: "As detailed in nothing", errChecker: (m: string) => m.includes('cross-reference') || m.includes('delete') },
183
- { target: "Please visit [Adeu HQ](https://adeu.com)", newText: "Please visit [Adeu HQ](https://adeu.com) and [Google](https://google.com)", errChecker: (m: string) => m.includes('hyperlink') || m.includes('insert') },
184
- { target: "Please visit [Adeu HQ](https://adeu.com)", newText: "Please visit nothing", errChecker: (m: string) => m.includes('hyperlink') || m.includes('delete') },
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 = { type: 'modify', target_text: tc.target, new_text: tc.newText };
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('\n').toLowerCase();
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('safely edits footnotes and accepts changes', async () => {
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 = { type: 'modify', target_text: "Footnote content.", new_text: "This is an edited footnote." };
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('extracts defined terms and finds typos correctly', async () => {
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(doc, 'We will act as the disclosing party (jäljempänä "Discloser").');
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(doc, "We shared Confidential Information with the Affiliate. The Discloser is happy.");
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("[Error] Duplicate Definition: 'Agreement' is defined multiple times.");
256
- expect(appendix).toContain("[Info] Possible Typos for 'Agreement': Found 'Agrement'");
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('reduces typo noise for short acronyms', async () => {
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("[Info] Possible Typos for 'Party': Found 'Pary'");
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
+ });