@ansonlai/docx-redline-js 0.4.0 → 0.5.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.
Files changed (104) hide show
  1. package/AGENTS.md +646 -288
  2. package/ARCHITECTURE.md +215 -9
  3. package/CHANGELOG.md +319 -0
  4. package/README.md +604 -360
  5. package/adapters/config.js +45 -43
  6. package/bin/docx-redline.js +3 -0
  7. package/core/list-targeting.js +101 -110
  8. package/core/paragraph-targeting.js +501 -61
  9. package/core/paragraph-text.js +209 -0
  10. package/core/redline-validation.js +11 -5
  11. package/core/revision-cloning.js +38 -0
  12. package/core/types.js +64 -10
  13. package/core/word-xml.js +43 -15
  14. package/dist/docx-redline-js.esm.js +3145 -505
  15. package/dist/docx-redline-js.esm.js.map +4 -4
  16. package/dist/docx-redline-js.esm.min.js +88 -76
  17. package/dist/docx-redline-js.esm.min.js.map +4 -4
  18. package/docs/TESTING.md +342 -23
  19. package/docs/plans/2026-09-05-structural-revisions-and-fidelity-oracles.md +1669 -0
  20. package/docs/plans/2026-09-08-cross-author-revision-slicing.md +505 -0
  21. package/docs/plans/completed/2026-09-01-performance-and-complexity-reduction.md +669 -0
  22. package/docs/plans/completed/2026-09-03-agent-friendly-document-workflows.md +427 -0
  23. package/docs/plans/completed/2026-09-04-comment-anchor-and-cli-reliability.md +519 -0
  24. package/docs/plans/completed/PERFORMANCE-CONSOLIDATION.md +69 -0
  25. package/docs/plans/completed/structural-revision-capability-matrix.md +115 -0
  26. package/docs/schemas/document-operations.schema.json +109 -0
  27. package/docs/test-comparison-dashboard.html +4250 -7
  28. package/engine/formatting-removal.js +11 -2
  29. package/engine/oxml-engine.js +508 -336
  30. package/engine/reconstruction-mode.js +15 -14
  31. package/engine/reconstruction-writer.js +247 -142
  32. package/engine/route-selection.js +35 -0
  33. package/engine/rpr-helpers.js +334 -35
  34. package/engine/run-builders.js +239 -196
  35. package/engine/surgical-diff-application.js +407 -50
  36. package/engine/surgical-mode.js +142 -6
  37. package/engine/surgical-run-splitting.js +103 -0
  38. package/engine/surgical-spans.js +52 -1
  39. package/engine/table-cell-context.js +3 -6
  40. package/engine/table-mode.js +1 -1
  41. package/index.d.ts +234 -6
  42. package/index.js +24 -1
  43. package/node/cli.js +322 -0
  44. package/node/docx-document.js +302 -0
  45. package/node/index.d.ts +31 -0
  46. package/node/index.js +2 -0
  47. package/node/zip-archive.js +52 -0
  48. package/orchestration/list-markdown.js +10 -16
  49. package/orchestration/list-parsing.js +7 -12
  50. package/orchestration/list-structural-fallback.js +21 -10
  51. package/package.json +123 -102
  52. package/pipeline/content-analysis.js +12 -17
  53. package/pipeline/ingestion-export.js +3 -31
  54. package/pipeline/ingestion-paragraph.js +10 -5
  55. package/pipeline/list-generation.js +150 -55
  56. package/pipeline/list-markers.js +70 -3
  57. package/pipeline/serialization.js +4 -2
  58. package/pipeline/structured-content.js +160 -0
  59. package/scripts/apply_changes.mjs +27 -0
  60. package/scripts/benchmark-operation-session.mjs +137 -0
  61. package/scripts/benchmark-targeting-browser.html +74 -0
  62. package/scripts/benchmark-targeting-hot-paths.mjs +67 -0
  63. package/scripts/benchmark-test-runner.mjs +59 -0
  64. package/scripts/build-test-dashboard.mjs +23 -0
  65. package/scripts/export-lane1-fixtures.mjs +380 -0
  66. package/scripts/export-reredline-stress-fixtures.mjs +317 -0
  67. package/scripts/export-validation-fixtures.mjs +1 -1
  68. package/scripts/extract_text.mjs +7 -0
  69. package/scripts/generate-cross-author-slicing-fixtures.ps1 +256 -0
  70. package/scripts/generate-paragraph-boundary-fixtures.ps1 +215 -0
  71. package/scripts/generate-test-dashboard.mjs +362 -11
  72. package/scripts/lib/word-coverage-catalogue.mjs +6 -2
  73. package/scripts/profile-route-selection.mjs +19 -0
  74. package/scripts/render-agenda-multilevel.mjs +0 -5
  75. package/scripts/render-multilevel-cases.mjs +0 -1
  76. package/scripts/run-tests.mjs +107 -35
  77. package/scripts/word-com-corpus-suite.ps1 +3 -0
  78. package/scripts/word-com-differential.ps1 +64 -4
  79. package/scripts/word-com-suite.ps1 +3 -0
  80. package/services/batch-operation-orchestrator.js +513 -0
  81. package/services/capture-engine.js +226 -0
  82. package/services/comment-builders.js +23 -6
  83. package/services/comment-engine.js +108 -47
  84. package/services/comment-locator.js +187 -82
  85. package/services/comment-replies.js +95 -0
  86. package/services/document-inspection.js +258 -0
  87. package/services/document-operation-applier.js +372 -0
  88. package/services/document-operation-contract.js +345 -0
  89. package/services/document-operation-mutations.js +1749 -0
  90. package/services/document-operation-session.js +258 -0
  91. package/services/numbering-service.js +14 -5
  92. package/services/operation-heuristics.js +173 -0
  93. package/services/operation-preflight.js +390 -0
  94. package/services/receipt-collector.js +288 -0
  95. package/services/revision-comment-management.js +77 -5
  96. package/services/revision-token.js +290 -0
  97. package/services/standalone-docx-plumbing.js +123 -8
  98. package/services/standalone-operation-runner.d.ts +296 -0
  99. package/services/standalone-operation-runner.js +10 -1455
  100. package/services/table-reconciliation.js +15 -6
  101. package/docs/VALIDATION.md +0 -183
  102. package/docs/WORD-MANUAL-REVIEW.md +0 -138
  103. package/docs/plans/2026-09-01-performance-and-complexity-reduction.md +0 -210
  104. /package/docs/plans/{2026-08-30-reliability-testing-improvements.md → completed/2026-08-30-reliability-testing-improvements.md} +0 -0
@@ -2,12 +2,12 @@
2
2
  * Run property (w:rPr) helper utilities.
3
3
  *
4
4
  * This module owns low-level formatting element operations, including
5
- * schema-order insertion, format extraction, and format add/remove transforms.
6
- */
7
-
8
- import { createWordElement } from '../core/word-xml.js';
9
-
10
- /**
5
+ * schema-order insertion, format extraction, and format add/remove transforms.
6
+ */
7
+
8
+ import { createWordElement } from '../core/word-xml.js';
9
+
10
+ /**
11
11
  * Canonical OOXML run-property schema ordering.
12
12
  * Shared by all rPr synchronizers.
13
13
  */
@@ -17,7 +17,7 @@ export const RPR_SCHEMA_ORDER = [
17
17
  'w:snapToGrid', 'w:vanish', 'w:webHidden', 'w:color', 'w:spacing', 'w:w', 'w:kern',
18
18
  'w:position', 'w:sz', 'w:szCs', 'w:highlight', 'w:u', 'w:effect', 'w:bdr', 'w:shd',
19
19
  'w:fitText', 'w:vertAlign', 'w:rtl', 'w:cs', 'w:em', 'w:lang', 'w:eastAsianLayout',
20
- 'w:specVanish', 'w:oMath'
20
+ 'w:specVanish', 'w:oMath', 'w:rPrChange'
21
21
  ];
22
22
 
23
23
  /**
@@ -84,33 +84,33 @@ function _applyOverrides(xmlDoc, rPr, formatFlags, mode) {
84
84
  }
85
85
  }
86
86
 
87
- if (applyBold) {
88
- const b = createWordElement(xmlDoc, 'w:b');
89
- b.setAttribute('w:val', mode === 'add' ? '1' : '0');
90
- insertRPrChildInOrder(rPr, b);
91
-
92
- const bCs = createWordElement(xmlDoc, 'w:bCs');
93
- bCs.setAttribute('w:val', mode === 'add' ? '1' : '0');
94
- insertRPrChildInOrder(rPr, bCs);
95
- }
96
- if (applyItalic) {
97
- const i = createWordElement(xmlDoc, 'w:i');
98
- i.setAttribute('w:val', mode === 'add' ? '1' : '0');
99
- insertRPrChildInOrder(rPr, i);
100
-
101
- const iCs = createWordElement(xmlDoc, 'w:iCs');
102
- iCs.setAttribute('w:val', mode === 'add' ? '1' : '0');
103
- insertRPrChildInOrder(rPr, iCs);
104
- }
105
- if (applyUnderline) {
106
- const u = createWordElement(xmlDoc, 'w:u');
107
- u.setAttribute('w:val', mode === 'add' ? 'single' : 'none');
108
- insertRPrChildInOrder(rPr, u);
109
- }
110
- if (applyStrike) {
111
- const strike = createWordElement(xmlDoc, 'w:strike');
112
- strike.setAttribute('w:val', mode === 'add' ? '1' : '0');
113
- insertRPrChildInOrder(rPr, strike);
87
+ if (applyBold) {
88
+ const b = createWordElement(xmlDoc, 'w:b');
89
+ b.setAttribute('w:val', mode === 'add' ? '1' : '0');
90
+ insertRPrChildInOrder(rPr, b);
91
+
92
+ const bCs = createWordElement(xmlDoc, 'w:bCs');
93
+ bCs.setAttribute('w:val', mode === 'add' ? '1' : '0');
94
+ insertRPrChildInOrder(rPr, bCs);
95
+ }
96
+ if (applyItalic) {
97
+ const i = createWordElement(xmlDoc, 'w:i');
98
+ i.setAttribute('w:val', mode === 'add' ? '1' : '0');
99
+ insertRPrChildInOrder(rPr, i);
100
+
101
+ const iCs = createWordElement(xmlDoc, 'w:iCs');
102
+ iCs.setAttribute('w:val', mode === 'add' ? '1' : '0');
103
+ insertRPrChildInOrder(rPr, iCs);
104
+ }
105
+ if (applyUnderline) {
106
+ const u = createWordElement(xmlDoc, 'w:u');
107
+ u.setAttribute('w:val', mode === 'add' ? 'single' : 'none');
108
+ insertRPrChildInOrder(rPr, u);
109
+ }
110
+ if (applyStrike) {
111
+ const strike = createWordElement(xmlDoc, 'w:strike');
112
+ strike.setAttribute('w:val', mode === 'add' ? '1' : '0');
113
+ insertRPrChildInOrder(rPr, strike);
114
114
  }
115
115
  }
116
116
 
@@ -125,7 +125,7 @@ function _applyOverrides(xmlDoc, rPr, formatFlags, mode) {
125
125
  */
126
126
  export function buildOverrideRPrXml(xmlDoc, originalRun, formatToRemove, serializer) {
127
127
  const baseRPr = originalRun.getElementsByTagName('w:rPr')[0] || null;
128
- const rPr = baseRPr ? baseRPr.cloneNode(true) : createWordElement(xmlDoc, 'w:rPr');
128
+ const rPr = baseRPr ? baseRPr.cloneNode(true) : createWordElement(xmlDoc, 'w:rPr');
129
129
  _applyOverrides(xmlDoc, rPr, formatToRemove, 'remove');
130
130
 
131
131
  let rPrXml = serializer.serializeToString(rPr);
@@ -194,3 +194,302 @@ function isFormattingElementEnabled(element, isUnderline) {
194
194
 
195
195
  return value !== '0' && value !== 'false' && value !== 'off';
196
196
  }
197
+
198
+ /**
199
+ * Canonical OOXML paragraph-property schema ordering.
200
+ */
201
+ export const PPR_SCHEMA_ORDER = [
202
+ 'w:pStyle', 'w:keepNext', 'w:keepLines', 'w:pageBreakBefore', 'w:framePr',
203
+ 'w:widowControl', 'w:numPr', 'w:suppressLineNumbers', 'w:pBdr', 'w:shd',
204
+ 'w:tabs', 'w:suppressAutoHyphens', 'w:kinsoku', 'w:wordWrap', 'w:overflowPunct',
205
+ 'w:topLinePunct', 'w:autoSpaceDE', 'w:autoSpaceDN', 'w:bidi', 'w:adjustRightInd',
206
+ 'w:snapToGrid', 'w:spacing', 'w:ind', 'w:contextualSpacing', 'w:mirrorIndents',
207
+ 'w:suppressOverlap', 'w:jc', 'w:textDirection', 'w:textAlignment', 'w:textboxTightWrap',
208
+ 'w:outlineLvl', 'w:divId', 'w:cnfStyle', 'w:rPr', 'w:sectPr', 'w:pPrChange'
209
+ ];
210
+
211
+ /**
212
+ * Inserts a pPr child node in schema order.
213
+ *
214
+ * @param {Element} pPr - Paragraph properties element
215
+ * @param {Element} el - Child element to insert
216
+ */
217
+ export function insertPPrChildInOrder(pPr, el) {
218
+ const myIndex = PPR_SCHEMA_ORDER.indexOf(el.nodeName);
219
+ const myPriority = myIndex === -1 ? 999 : myIndex;
220
+
221
+ let inserted = false;
222
+ for (const child of Array.from(pPr.childNodes)) {
223
+ if (child.nodeType !== 1) continue;
224
+ const childIndex = PPR_SCHEMA_ORDER.indexOf(child.nodeName);
225
+ const childPriority = childIndex === -1 ? 999 : childIndex;
226
+ if (childPriority > myPriority) {
227
+ pPr.insertBefore(el, child);
228
+ inserted = true;
229
+ break;
230
+ }
231
+ }
232
+ if (!inserted) pPr.appendChild(el);
233
+ }
234
+
235
+ /**
236
+ * Checks if applying character properties to an rPr will change anything.
237
+ *
238
+ * @param {Element|null} rPr - Run properties
239
+ * @param {Object} properties - Desired properties
240
+ * @returns {boolean}
241
+ */
242
+ export function checkRunPropertiesChanged(rPr, properties) {
243
+ if (!properties) return false;
244
+ const current = extractFormatFromRPr(rPr);
245
+
246
+ if (properties.bold !== undefined && current.bold !== !!properties.bold) return true;
247
+ if (properties.italic !== undefined && current.italic !== !!properties.italic) return true;
248
+ if (properties.underline !== undefined && current.underline !== !!properties.underline) return true;
249
+
250
+ const desiredStrike = properties.strike !== undefined ? properties.strike : properties.strikethrough;
251
+ if (desiredStrike !== undefined && current.strikethrough !== !!desiredStrike) return true;
252
+
253
+ if (properties.highlight !== undefined) {
254
+ const hlEl = Array.from(rPr?.childNodes || []).find(c => c.nodeType === 1 && (c.nodeName === 'w:highlight' || c.localName === 'highlight'));
255
+ const currentHl = hlEl ? (hlEl.getAttribute('w:val') || hlEl.getAttribute('val')) : null;
256
+ const desiredHl = properties.highlight && properties.highlight !== 'none' ? String(properties.highlight).toLowerCase() : null;
257
+ if ((currentHl || null) !== desiredHl) return true;
258
+ }
259
+
260
+ if (properties.color !== undefined) {
261
+ const colEl = Array.from(rPr?.childNodes || []).find(c => c.nodeType === 1 && (c.nodeName === 'w:color' || c.localName === 'color'));
262
+ const currentCol = colEl ? (colEl.getAttribute('w:val') || colEl.getAttribute('val')) : null;
263
+ const desiredCol = properties.color && properties.color !== 'auto' ? String(properties.color).replace(/^#/, '').toUpperCase() : null;
264
+ if ((currentCol?.toUpperCase() || null) !== desiredCol) return true;
265
+ }
266
+
267
+ if (properties.fontSize !== undefined) {
268
+ const szEl = Array.from(rPr?.childNodes || []).find(c => c.nodeType === 1 && (c.nodeName === 'w:sz' || c.localName === 'sz'));
269
+ const currentSz = szEl ? (szEl.getAttribute('w:val') || szEl.getAttribute('val')) : null;
270
+ const desiredSz = properties.fontSize !== null && properties.fontSize !== '' ? String(properties.fontSize) : null;
271
+ if (currentSz !== desiredSz) return true;
272
+ }
273
+
274
+ if (properties.fontFamily !== undefined) {
275
+ const fEl = Array.from(rPr?.childNodes || []).find(c => c.nodeType === 1 && (c.nodeName === 'w:rFonts' || c.localName === 'rFonts'));
276
+ const currentFont = fEl ? (fEl.getAttribute('w:ascii') || fEl.getAttribute('ascii')) : null;
277
+ const desiredFont = properties.fontFamily || null;
278
+ if (currentFont !== desiredFont) return true;
279
+ }
280
+
281
+ return false;
282
+ }
283
+
284
+ /**
285
+ * Applies explicit character formatting to an rPr in schema order.
286
+ *
287
+ * @param {Document} xmlDoc - XML document
288
+ * @param {Element} rPr - Run properties
289
+ * @param {Object} properties - Desired properties
290
+ */
291
+ export function applyCharacterFormatToRPr(xmlDoc, rPr, properties) {
292
+ if (!rPr || !properties) return;
293
+
294
+ const removeTags = (tags) => {
295
+ const toRemove = [];
296
+ for (const child of Array.from(rPr.childNodes)) {
297
+ if (child.nodeType === 1 && tags.includes(child.nodeName)) {
298
+ toRemove.push(child);
299
+ }
300
+ }
301
+ for (const el of toRemove) rPr.removeChild(el);
302
+ };
303
+
304
+ if (properties.bold !== undefined) {
305
+ removeTags(['w:b', 'w:bCs']);
306
+ const b = createWordElement(xmlDoc, 'w:b');
307
+ b.setAttribute('w:val', properties.bold ? '1' : '0');
308
+ insertRPrChildInOrder(rPr, b);
309
+ const bCs = createWordElement(xmlDoc, 'w:bCs');
310
+ bCs.setAttribute('w:val', properties.bold ? '1' : '0');
311
+ insertRPrChildInOrder(rPr, bCs);
312
+ }
313
+
314
+ if (properties.italic !== undefined) {
315
+ removeTags(['w:i', 'w:iCs']);
316
+ const i = createWordElement(xmlDoc, 'w:i');
317
+ i.setAttribute('w:val', properties.italic ? '1' : '0');
318
+ insertRPrChildInOrder(rPr, i);
319
+ const iCs = createWordElement(xmlDoc, 'w:iCs');
320
+ iCs.setAttribute('w:val', properties.italic ? '1' : '0');
321
+ insertRPrChildInOrder(rPr, iCs);
322
+ }
323
+
324
+ if (properties.underline !== undefined) {
325
+ removeTags(['w:u']);
326
+ const u = createWordElement(xmlDoc, 'w:u');
327
+ u.setAttribute('w:val', properties.underline ? 'single' : 'none');
328
+ insertRPrChildInOrder(rPr, u);
329
+ }
330
+
331
+ const strikeVal = properties.strike !== undefined ? properties.strike : properties.strikethrough;
332
+ if (strikeVal !== undefined) {
333
+ removeTags(['w:strike', 'w:dstrike']);
334
+ const strike = createWordElement(xmlDoc, 'w:strike');
335
+ strike.setAttribute('w:val', strikeVal ? '1' : '0');
336
+ insertRPrChildInOrder(rPr, strike);
337
+ }
338
+
339
+ if (properties.highlight !== undefined) {
340
+ removeTags(['w:highlight']);
341
+ if (properties.highlight && properties.highlight !== 'none') {
342
+ const hl = createWordElement(xmlDoc, 'w:highlight');
343
+ hl.setAttribute('w:val', String(properties.highlight).toLowerCase());
344
+ insertRPrChildInOrder(rPr, hl);
345
+ }
346
+ }
347
+
348
+ if (properties.color !== undefined) {
349
+ removeTags(['w:color']);
350
+ if (properties.color && properties.color !== 'auto') {
351
+ const col = createWordElement(xmlDoc, 'w:color');
352
+ col.setAttribute('w:val', String(properties.color).replace(/^#/, ''));
353
+ insertRPrChildInOrder(rPr, col);
354
+ }
355
+ }
356
+
357
+ if (properties.fontSize !== undefined) {
358
+ removeTags(['w:sz', 'w:szCs']);
359
+ if (properties.fontSize !== null && properties.fontSize !== '') {
360
+ const szVal = String(properties.fontSize);
361
+ const sz = createWordElement(xmlDoc, 'w:sz');
362
+ sz.setAttribute('w:val', szVal);
363
+ insertRPrChildInOrder(rPr, sz);
364
+ const szCs = createWordElement(xmlDoc, 'w:szCs');
365
+ szCs.setAttribute('w:val', szVal);
366
+ insertRPrChildInOrder(rPr, szCs);
367
+ }
368
+ }
369
+
370
+ if (properties.fontFamily !== undefined) {
371
+ removeTags(['w:rFonts']);
372
+ if (properties.fontFamily) {
373
+ const fonts = createWordElement(xmlDoc, 'w:rFonts');
374
+ fonts.setAttribute('w:ascii', properties.fontFamily);
375
+ fonts.setAttribute('w:hAnsi', properties.fontFamily);
376
+ fonts.setAttribute('w:cs', properties.fontFamily);
377
+ insertRPrChildInOrder(rPr, fonts);
378
+ }
379
+ }
380
+ }
381
+
382
+ /**
383
+ * Checks if applying paragraph properties to a pPr will change anything.
384
+ *
385
+ * @param {Element|null} pPr - Paragraph properties
386
+ * @param {Object} properties - Desired properties
387
+ * @returns {boolean}
388
+ */
389
+ export function checkParagraphPropertiesChanged(pPr, properties) {
390
+ if (!properties) return false;
391
+
392
+ if (properties.alignment !== undefined) {
393
+ const jc = Array.from(pPr?.childNodes || []).find(c => c.nodeType === 1 && (c.nodeName === 'w:jc' || c.localName === 'jc'));
394
+ const currentVal = jc ? (jc.getAttribute('w:val') || jc.getAttribute('val')) : null;
395
+ if (currentVal !== properties.alignment) return true;
396
+ }
397
+
398
+ if (properties.keepNext !== undefined) {
399
+ const kn = Array.from(pPr?.childNodes || []).find(c => c.nodeType === 1 && (c.nodeName === 'w:keepNext' || c.localName === 'keepNext'));
400
+ const currentVal = kn ? isFormattingElementEnabled(kn, false) : false;
401
+ if (currentVal !== !!properties.keepNext) return true;
402
+ }
403
+
404
+ if (properties.keepLines !== undefined) {
405
+ const kl = Array.from(pPr?.childNodes || []).find(c => c.nodeType === 1 && (c.nodeName === 'w:keepLines' || c.localName === 'keepLines'));
406
+ const currentVal = kl ? isFormattingElementEnabled(kl, false) : false;
407
+ if (currentVal !== !!properties.keepLines) return true;
408
+ }
409
+
410
+ if (properties.pageBreakBefore !== undefined) {
411
+ const pbb = Array.from(pPr?.childNodes || []).find(c => c.nodeType === 1 && (c.nodeName === 'w:pageBreakBefore' || c.localName === 'pageBreakBefore'));
412
+ const currentVal = pbb ? isFormattingElementEnabled(pbb, false) : false;
413
+ if (currentVal !== !!properties.pageBreakBefore) return true;
414
+ }
415
+
416
+ if (properties.style !== undefined) {
417
+ const pStyle = Array.from(pPr?.childNodes || []).find(c => c.nodeType === 1 && (c.nodeName === 'w:pStyle' || c.localName === 'pStyle'));
418
+ const currentVal = pStyle ? (pStyle.getAttribute('w:val') || pStyle.getAttribute('val')) : null;
419
+ if (currentVal !== properties.style) return true;
420
+ }
421
+
422
+ return false;
423
+ }
424
+
425
+ /**
426
+ * Applies explicit paragraph formatting to a pPr in schema order.
427
+ *
428
+ * @param {Document} xmlDoc - XML document
429
+ * @param {Element} pPr - Paragraph properties
430
+ * @param {Object} properties - Desired properties
431
+ */
432
+ export function applyParagraphPropertiesToPPr(xmlDoc, pPr, properties) {
433
+ if (!pPr || !properties) return;
434
+
435
+ const removeTags = (tags) => {
436
+ const toRemove = [];
437
+ for (const child of Array.from(pPr.childNodes)) {
438
+ if (child.nodeType === 1 && tags.includes(child.nodeName)) {
439
+ toRemove.push(child);
440
+ }
441
+ }
442
+ for (const el of toRemove) pPr.removeChild(el);
443
+ };
444
+
445
+ if (properties.alignment !== undefined) {
446
+ removeTags(['w:jc']);
447
+ if (properties.alignment) {
448
+ const jc = createWordElement(xmlDoc, 'w:jc');
449
+ jc.setAttribute('w:val', properties.alignment);
450
+ insertPPrChildInOrder(pPr, jc);
451
+ }
452
+ }
453
+
454
+ if (properties.keepNext !== undefined) {
455
+ removeTags(['w:keepNext']);
456
+ const kn = createWordElement(xmlDoc, 'w:keepNext');
457
+ if (properties.keepNext) {
458
+ insertPPrChildInOrder(pPr, kn);
459
+ } else {
460
+ kn.setAttribute('w:val', '0');
461
+ insertPPrChildInOrder(pPr, kn);
462
+ }
463
+ }
464
+
465
+ if (properties.keepLines !== undefined) {
466
+ removeTags(['w:keepLines']);
467
+ const kl = createWordElement(xmlDoc, 'w:keepLines');
468
+ if (properties.keepLines) {
469
+ insertPPrChildInOrder(pPr, kl);
470
+ } else {
471
+ kl.setAttribute('w:val', '0');
472
+ insertPPrChildInOrder(pPr, kl);
473
+ }
474
+ }
475
+
476
+ if (properties.pageBreakBefore !== undefined) {
477
+ removeTags(['w:pageBreakBefore']);
478
+ const pbb = createWordElement(xmlDoc, 'w:pageBreakBefore');
479
+ if (properties.pageBreakBefore) {
480
+ insertPPrChildInOrder(pPr, pbb);
481
+ } else {
482
+ pbb.setAttribute('w:val', '0');
483
+ insertPPrChildInOrder(pPr, pbb);
484
+ }
485
+ }
486
+
487
+ if (properties.style !== undefined) {
488
+ removeTags(['w:pStyle']);
489
+ if (properties.style) {
490
+ const pStyle = createWordElement(xmlDoc, 'w:pStyle');
491
+ pStyle.setAttribute('w:val', properties.style);
492
+ insertPPrChildInOrder(pPr, pStyle);
493
+ }
494
+ }
495
+ }