docxmlater 10.1.3 → 10.1.5

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 (371) hide show
  1. package/README.md +759 -754
  2. package/dist/constants/legacyCompatFlags.js +1 -1
  3. package/dist/constants/legacyCompatFlags.js.map +1 -1
  4. package/dist/constants/limits.js.map +1 -1
  5. package/dist/core/Document.d.ts +50 -50
  6. package/dist/core/Document.d.ts.map +1 -1
  7. package/dist/core/Document.js +483 -471
  8. package/dist/core/Document.js.map +1 -1
  9. package/dist/core/DocumentContent.d.ts +9 -9
  10. package/dist/core/DocumentContent.d.ts.map +1 -1
  11. package/dist/core/DocumentContent.js +1 -1
  12. package/dist/core/DocumentContent.js.map +1 -1
  13. package/dist/core/DocumentGenerator.d.ts +11 -11
  14. package/dist/core/DocumentGenerator.d.ts.map +1 -1
  15. package/dist/core/DocumentGenerator.js +251 -251
  16. package/dist/core/DocumentGenerator.js.map +1 -1
  17. package/dist/core/DocumentIdManager.js.map +1 -1
  18. package/dist/core/DocumentParser.d.ts +15 -15
  19. package/dist/core/DocumentParser.d.ts.map +1 -1
  20. package/dist/core/DocumentParser.js +2123 -2155
  21. package/dist/core/DocumentParser.js.map +1 -1
  22. package/dist/core/DocumentValidator.d.ts.map +1 -1
  23. package/dist/core/DocumentValidator.js +2 -5
  24. package/dist/core/DocumentValidator.js.map +1 -1
  25. package/dist/core/Relationship.js.map +1 -1
  26. package/dist/core/RelationshipManager.d.ts.map +1 -1
  27. package/dist/core/RelationshipManager.js +3 -3
  28. package/dist/core/RelationshipManager.js.map +1 -1
  29. package/dist/elements/AlternateContent.js.map +1 -1
  30. package/dist/elements/Bookmark.d.ts.map +1 -1
  31. package/dist/elements/Bookmark.js +3 -1
  32. package/dist/elements/Bookmark.js.map +1 -1
  33. package/dist/elements/BookmarkManager.d.ts.map +1 -1
  34. package/dist/elements/BookmarkManager.js.map +1 -1
  35. package/dist/elements/Comment.d.ts.map +1 -1
  36. package/dist/elements/Comment.js +9 -6
  37. package/dist/elements/Comment.js.map +1 -1
  38. package/dist/elements/CommentManager.d.ts.map +1 -1
  39. package/dist/elements/CommentManager.js +18 -17
  40. package/dist/elements/CommentManager.js.map +1 -1
  41. package/dist/elements/CommonTypes.d.ts +21 -21
  42. package/dist/elements/CommonTypes.d.ts.map +1 -1
  43. package/dist/elements/CommonTypes.js +56 -56
  44. package/dist/elements/CommonTypes.js.map +1 -1
  45. package/dist/elements/CustomXml.js.map +1 -1
  46. package/dist/elements/Endnote.d.ts.map +1 -1
  47. package/dist/elements/Endnote.js +6 -6
  48. package/dist/elements/Endnote.js.map +1 -1
  49. package/dist/elements/EndnoteManager.d.ts.map +1 -1
  50. package/dist/elements/EndnoteManager.js +6 -7
  51. package/dist/elements/EndnoteManager.js.map +1 -1
  52. package/dist/elements/Field.d.ts.map +1 -1
  53. package/dist/elements/Field.js +82 -25
  54. package/dist/elements/Field.js.map +1 -1
  55. package/dist/elements/FieldHelpers.d.ts.map +1 -1
  56. package/dist/elements/FieldHelpers.js.map +1 -1
  57. package/dist/elements/FontManager.d.ts.map +1 -1
  58. package/dist/elements/FontManager.js +1 -1
  59. package/dist/elements/FontManager.js.map +1 -1
  60. package/dist/elements/Footer.js +2 -2
  61. package/dist/elements/Footer.js.map +1 -1
  62. package/dist/elements/Footnote.d.ts.map +1 -1
  63. package/dist/elements/Footnote.js +6 -6
  64. package/dist/elements/Footnote.js.map +1 -1
  65. package/dist/elements/FootnoteManager.d.ts.map +1 -1
  66. package/dist/elements/FootnoteManager.js +6 -7
  67. package/dist/elements/FootnoteManager.js.map +1 -1
  68. package/dist/elements/Header.js +2 -2
  69. package/dist/elements/Header.js.map +1 -1
  70. package/dist/elements/HeaderFooterManager.js.map +1 -1
  71. package/dist/elements/Hyperlink.d.ts +5 -3
  72. package/dist/elements/Hyperlink.d.ts.map +1 -1
  73. package/dist/elements/Hyperlink.js +134 -76
  74. package/dist/elements/Hyperlink.js.map +1 -1
  75. package/dist/elements/Image.d.ts.map +1 -1
  76. package/dist/elements/Image.js +238 -106
  77. package/dist/elements/Image.js.map +1 -1
  78. package/dist/elements/ImageManager.d.ts.map +1 -1
  79. package/dist/elements/ImageManager.js +1 -1
  80. package/dist/elements/ImageManager.js.map +1 -1
  81. package/dist/elements/ImageRun.js +1 -1
  82. package/dist/elements/ImageRun.js.map +1 -1
  83. package/dist/elements/MathElement.js.map +1 -1
  84. package/dist/elements/Paragraph.d.ts +24 -24
  85. package/dist/elements/Paragraph.d.ts.map +1 -1
  86. package/dist/elements/Paragraph.js +181 -188
  87. package/dist/elements/Paragraph.js.map +1 -1
  88. package/dist/elements/PreservedElement.js.map +1 -1
  89. package/dist/elements/PropertyChangeTypes.d.ts.map +1 -1
  90. package/dist/elements/PropertyChangeTypes.js +6 -6
  91. package/dist/elements/PropertyChangeTypes.js.map +1 -1
  92. package/dist/elements/RangeMarker.d.ts.map +1 -1
  93. package/dist/elements/RangeMarker.js.map +1 -1
  94. package/dist/elements/Revision.d.ts.map +1 -1
  95. package/dist/elements/Revision.js +4 -5
  96. package/dist/elements/Revision.js.map +1 -1
  97. package/dist/elements/RevisionContent.js.map +1 -1
  98. package/dist/elements/RevisionManager.d.ts.map +1 -1
  99. package/dist/elements/RevisionManager.js +40 -48
  100. package/dist/elements/RevisionManager.js.map +1 -1
  101. package/dist/elements/Run.d.ts +16 -16
  102. package/dist/elements/Run.d.ts.map +1 -1
  103. package/dist/elements/Run.js +256 -238
  104. package/dist/elements/Run.js.map +1 -1
  105. package/dist/elements/Section.d.ts.map +1 -1
  106. package/dist/elements/Section.js +36 -11
  107. package/dist/elements/Section.js.map +1 -1
  108. package/dist/elements/Shape.d.ts.map +1 -1
  109. package/dist/elements/Shape.js.map +1 -1
  110. package/dist/elements/StructuredDocumentTag.d.ts +6 -6
  111. package/dist/elements/StructuredDocumentTag.d.ts.map +1 -1
  112. package/dist/elements/StructuredDocumentTag.js +99 -104
  113. package/dist/elements/StructuredDocumentTag.js.map +1 -1
  114. package/dist/elements/Table.d.ts +11 -11
  115. package/dist/elements/Table.d.ts.map +1 -1
  116. package/dist/elements/Table.js +102 -107
  117. package/dist/elements/Table.js.map +1 -1
  118. package/dist/elements/TableCell.d.ts +10 -10
  119. package/dist/elements/TableCell.d.ts.map +1 -1
  120. package/dist/elements/TableCell.js +105 -106
  121. package/dist/elements/TableCell.js.map +1 -1
  122. package/dist/elements/TableGridChange.d.ts.map +1 -1
  123. package/dist/elements/TableGridChange.js.map +1 -1
  124. package/dist/elements/TableOfContents.d.ts.map +1 -1
  125. package/dist/elements/TableOfContents.js +4 -4
  126. package/dist/elements/TableOfContents.js.map +1 -1
  127. package/dist/elements/TableOfContentsElement.js.map +1 -1
  128. package/dist/elements/TableRow.d.ts.map +1 -1
  129. package/dist/elements/TableRow.js +13 -6
  130. package/dist/elements/TableRow.js.map +1 -1
  131. package/dist/elements/TextBox.d.ts.map +1 -1
  132. package/dist/elements/TextBox.js +3 -5
  133. package/dist/elements/TextBox.js.map +1 -1
  134. package/dist/formatting/AbstractNumbering.d.ts +4 -4
  135. package/dist/formatting/AbstractNumbering.d.ts.map +1 -1
  136. package/dist/formatting/AbstractNumbering.js +54 -49
  137. package/dist/formatting/AbstractNumbering.js.map +1 -1
  138. package/dist/formatting/NumberingInstance.d.ts.map +1 -1
  139. package/dist/formatting/NumberingInstance.js +1 -3
  140. package/dist/formatting/NumberingInstance.js.map +1 -1
  141. package/dist/formatting/NumberingLevel.d.ts +5 -5
  142. package/dist/formatting/NumberingLevel.d.ts.map +1 -1
  143. package/dist/formatting/NumberingLevel.js +119 -125
  144. package/dist/formatting/NumberingLevel.js.map +1 -1
  145. package/dist/formatting/NumberingManager.d.ts.map +1 -1
  146. package/dist/formatting/NumberingManager.js +9 -9
  147. package/dist/formatting/NumberingManager.js.map +1 -1
  148. package/dist/formatting/Style.d.ts +11 -11
  149. package/dist/formatting/Style.d.ts.map +1 -1
  150. package/dist/formatting/Style.js +219 -247
  151. package/dist/formatting/Style.js.map +1 -1
  152. package/dist/formatting/StylesManager.d.ts +2 -2
  153. package/dist/formatting/StylesManager.d.ts.map +1 -1
  154. package/dist/formatting/StylesManager.js +96 -102
  155. package/dist/formatting/StylesManager.js.map +1 -1
  156. package/dist/helpers/CleanupHelper.d.ts +1 -1
  157. package/dist/helpers/CleanupHelper.d.ts.map +1 -1
  158. package/dist/helpers/CleanupHelper.js +6 -6
  159. package/dist/helpers/CleanupHelper.js.map +1 -1
  160. package/dist/images/ImageOptimizer.js +7 -7
  161. package/dist/images/ImageOptimizer.js.map +1 -1
  162. package/dist/index.d.ts +9 -9
  163. package/dist/index.d.ts.map +1 -1
  164. package/dist/index.js.map +1 -1
  165. package/dist/managers/DrawingManager.js.map +1 -1
  166. package/dist/tracking/DocumentTrackingContext.d.ts.map +1 -1
  167. package/dist/tracking/DocumentTrackingContext.js +23 -7
  168. package/dist/tracking/DocumentTrackingContext.js.map +1 -1
  169. package/dist/tracking/TrackingContext.d.ts.map +1 -1
  170. package/dist/tracking/TrackingContext.js.map +1 -1
  171. package/dist/types/compatibility-types.js.map +1 -1
  172. package/dist/types/formatting.js.map +1 -1
  173. package/dist/types/list-types.d.ts +6 -6
  174. package/dist/types/list-types.js.map +1 -1
  175. package/dist/types/settings-types.js.map +1 -1
  176. package/dist/types/styleConfig.d.ts +2 -2
  177. package/dist/types/styleConfig.js.map +1 -1
  178. package/dist/utils/ChangelogGenerator.d.ts.map +1 -1
  179. package/dist/utils/ChangelogGenerator.js +97 -101
  180. package/dist/utils/ChangelogGenerator.js.map +1 -1
  181. package/dist/utils/CompatibilityUpgrader.d.ts.map +1 -1
  182. package/dist/utils/CompatibilityUpgrader.js +1 -1
  183. package/dist/utils/CompatibilityUpgrader.js.map +1 -1
  184. package/dist/utils/InMemoryRevisionAcceptor.d.ts.map +1 -1
  185. package/dist/utils/InMemoryRevisionAcceptor.js +1 -6
  186. package/dist/utils/InMemoryRevisionAcceptor.js.map +1 -1
  187. package/dist/utils/MoveOperationHelper.d.ts.map +1 -1
  188. package/dist/utils/MoveOperationHelper.js +1 -1
  189. package/dist/utils/MoveOperationHelper.js.map +1 -1
  190. package/dist/utils/RevisionAwareProcessor.d.ts.map +1 -1
  191. package/dist/utils/RevisionAwareProcessor.js +2 -4
  192. package/dist/utils/RevisionAwareProcessor.js.map +1 -1
  193. package/dist/utils/RevisionWalker.d.ts.map +1 -1
  194. package/dist/utils/RevisionWalker.js +4 -12
  195. package/dist/utils/RevisionWalker.js.map +1 -1
  196. package/dist/utils/SelectiveRevisionAcceptor.d.ts.map +1 -1
  197. package/dist/utils/SelectiveRevisionAcceptor.js +2 -6
  198. package/dist/utils/SelectiveRevisionAcceptor.js.map +1 -1
  199. package/dist/utils/ShadingResolver.d.ts.map +1 -1
  200. package/dist/utils/ShadingResolver.js +1 -1
  201. package/dist/utils/ShadingResolver.js.map +1 -1
  202. package/dist/utils/acceptRevisions.d.ts.map +1 -1
  203. package/dist/utils/acceptRevisions.js +23 -12
  204. package/dist/utils/acceptRevisions.js.map +1 -1
  205. package/dist/utils/cnfStyleDecoder.d.ts +1 -1
  206. package/dist/utils/cnfStyleDecoder.d.ts.map +1 -1
  207. package/dist/utils/cnfStyleDecoder.js +40 -40
  208. package/dist/utils/cnfStyleDecoder.js.map +1 -1
  209. package/dist/utils/corruptionDetection.d.ts.map +1 -1
  210. package/dist/utils/corruptionDetection.js.map +1 -1
  211. package/dist/utils/dateFormatting.js.map +1 -1
  212. package/dist/utils/deepClone.js +1 -1
  213. package/dist/utils/deepClone.js.map +1 -1
  214. package/dist/utils/diagnostics.d.ts.map +1 -1
  215. package/dist/utils/diagnostics.js +1 -1
  216. package/dist/utils/diagnostics.js.map +1 -1
  217. package/dist/utils/errorHandling.js.map +1 -1
  218. package/dist/utils/formatting.d.ts.map +1 -1
  219. package/dist/utils/formatting.js +10 -2
  220. package/dist/utils/formatting.js.map +1 -1
  221. package/dist/utils/list-detection.d.ts +2 -2
  222. package/dist/utils/list-detection.d.ts.map +1 -1
  223. package/dist/utils/list-detection.js +21 -23
  224. package/dist/utils/list-detection.js.map +1 -1
  225. package/dist/utils/logger.d.ts.map +1 -1
  226. package/dist/utils/logger.js +12 -7
  227. package/dist/utils/logger.js.map +1 -1
  228. package/dist/utils/parsingHelpers.js.map +1 -1
  229. package/dist/utils/stripTrackedChanges.d.ts.map +1 -1
  230. package/dist/utils/stripTrackedChanges.js +3 -3
  231. package/dist/utils/stripTrackedChanges.js.map +1 -1
  232. package/dist/utils/textDiff.d.ts +1 -1
  233. package/dist/utils/textDiff.js +8 -8
  234. package/dist/utils/textDiff.js.map +1 -1
  235. package/dist/utils/units.js.map +1 -1
  236. package/dist/utils/validation.d.ts.map +1 -1
  237. package/dist/utils/validation.js +24 -7
  238. package/dist/utils/validation.js.map +1 -1
  239. package/dist/utils/xmlSanitization.d.ts.map +1 -1
  240. package/dist/utils/xmlSanitization.js +3 -3
  241. package/dist/utils/xmlSanitization.js.map +1 -1
  242. package/dist/validation/RevisionAutoFixer.d.ts.map +1 -1
  243. package/dist/validation/RevisionAutoFixer.js +5 -5
  244. package/dist/validation/RevisionAutoFixer.js.map +1 -1
  245. package/dist/validation/RevisionValidator.d.ts.map +1 -1
  246. package/dist/validation/RevisionValidator.js +7 -9
  247. package/dist/validation/RevisionValidator.js.map +1 -1
  248. package/dist/validation/ValidationRules.js +3 -3
  249. package/dist/validation/ValidationRules.js.map +1 -1
  250. package/dist/validation/index.js.map +1 -1
  251. package/dist/xml/XMLBuilder.d.ts +1 -1
  252. package/dist/xml/XMLBuilder.d.ts.map +1 -1
  253. package/dist/xml/XMLBuilder.js +98 -100
  254. package/dist/xml/XMLBuilder.js.map +1 -1
  255. package/dist/xml/XMLParser.d.ts.map +1 -1
  256. package/dist/xml/XMLParser.js +61 -66
  257. package/dist/xml/XMLParser.js.map +1 -1
  258. package/dist/zip/ZipHandler.d.ts.map +1 -1
  259. package/dist/zip/ZipHandler.js.map +1 -1
  260. package/dist/zip/ZipReader.d.ts.map +1 -1
  261. package/dist/zip/ZipReader.js +1 -3
  262. package/dist/zip/ZipReader.js.map +1 -1
  263. package/dist/zip/ZipWriter.d.ts +1 -1
  264. package/dist/zip/ZipWriter.d.ts.map +1 -1
  265. package/dist/zip/ZipWriter.js +28 -36
  266. package/dist/zip/ZipWriter.js.map +1 -1
  267. package/dist/zip/types.js +1 -1
  268. package/dist/zip/types.js.map +1 -1
  269. package/package.json +92 -92
  270. package/src/__tests__/helper-methods.test.ts +512 -512
  271. package/src/constants/legacyCompatFlags.ts +138 -138
  272. package/src/constants/limits.ts +50 -50
  273. package/src/core/Document.ts +985 -1145
  274. package/src/core/DocumentContent.ts +461 -467
  275. package/src/core/DocumentGenerator.ts +1133 -1104
  276. package/src/core/DocumentIdManager.ts +158 -158
  277. package/src/core/DocumentParser.ts +2347 -2716
  278. package/src/core/DocumentValidator.ts +363 -372
  279. package/src/core/Relationship.ts +367 -367
  280. package/src/core/RelationshipManager.ts +429 -428
  281. package/src/elements/AlternateContent.ts +42 -42
  282. package/src/elements/Bookmark.ts +212 -210
  283. package/src/elements/BookmarkManager.ts +247 -250
  284. package/src/elements/Comment.ts +356 -359
  285. package/src/elements/CommentManager.ts +499 -502
  286. package/src/elements/CommonTypes.ts +524 -549
  287. package/src/elements/CustomXml.ts +36 -36
  288. package/src/elements/Endnote.ts +221 -217
  289. package/src/elements/EndnoteManager.ts +246 -249
  290. package/src/elements/Field.ts +1292 -1233
  291. package/src/elements/FieldHelpers.ts +329 -333
  292. package/src/elements/FontManager.ts +336 -339
  293. package/src/elements/Footer.ts +269 -269
  294. package/src/elements/Footnote.ts +221 -217
  295. package/src/elements/FootnoteManager.ts +246 -249
  296. package/src/elements/Header.ts +269 -269
  297. package/src/elements/HeaderFooterManager.ts +219 -219
  298. package/src/elements/Hyperlink.ts +1288 -1193
  299. package/src/elements/Image.ts +1982 -1756
  300. package/src/elements/ImageManager.ts +437 -432
  301. package/src/elements/ImageRun.ts +59 -59
  302. package/src/elements/MathElement.ts +65 -65
  303. package/src/elements/Paragraph.ts +4347 -4287
  304. package/src/elements/PreservedElement.ts +53 -53
  305. package/src/elements/PropertyChangeTypes.ts +458 -442
  306. package/src/elements/RangeMarker.ts +382 -400
  307. package/src/elements/Revision.ts +1198 -1217
  308. package/src/elements/RevisionContent.ts +73 -73
  309. package/src/elements/RevisionManager.ts +1070 -1070
  310. package/src/elements/Run.ts +3103 -3073
  311. package/src/elements/Section.ts +1521 -1421
  312. package/src/elements/Shape.ts +884 -873
  313. package/src/elements/StructuredDocumentTag.ts +1176 -1207
  314. package/src/elements/Table.ts +2468 -2524
  315. package/src/elements/TableCell.ts +1617 -1621
  316. package/src/elements/TableGridChange.ts +149 -151
  317. package/src/elements/TableOfContents.ts +701 -691
  318. package/src/elements/TableOfContentsElement.ts +89 -89
  319. package/src/elements/TableRow.ts +960 -929
  320. package/src/elements/TextBox.ts +766 -768
  321. package/src/formatting/AbstractNumbering.ts +580 -579
  322. package/src/formatting/NumberingInstance.ts +295 -299
  323. package/src/formatting/NumberingLevel.ts +981 -1040
  324. package/src/formatting/NumberingManager.ts +833 -827
  325. package/src/formatting/Style.ts +1785 -1879
  326. package/src/formatting/StylesManager.ts +1090 -1130
  327. package/src/helpers/CleanupHelper.ts +524 -524
  328. package/src/images/ImageOptimizer.ts +274 -274
  329. package/src/index.ts +559 -554
  330. package/src/managers/DrawingManager.ts +319 -319
  331. package/src/tracking/DocumentTrackingContext.ts +687 -674
  332. package/src/tracking/TrackingContext.ts +175 -173
  333. package/src/types/compatibility-types.ts +49 -49
  334. package/src/types/formatting.ts +210 -210
  335. package/src/types/list-types.ts +14 -14
  336. package/src/types/settings-types.ts +59 -59
  337. package/src/types/styleConfig.ts +189 -189
  338. package/src/utils/ChangelogGenerator.ts +1583 -1581
  339. package/src/utils/CompatibilityUpgrader.ts +235 -237
  340. package/src/utils/InMemoryRevisionAcceptor.ts +691 -696
  341. package/src/utils/MoveOperationHelper.ts +233 -238
  342. package/src/utils/RevisionAwareProcessor.ts +518 -526
  343. package/src/utils/RevisionWalker.ts +427 -457
  344. package/src/utils/SelectiveRevisionAcceptor.ts +662 -683
  345. package/src/utils/ShadingResolver.ts +105 -107
  346. package/src/utils/acceptRevisions.ts +723 -714
  347. package/src/utils/cnfStyleDecoder.ts +212 -217
  348. package/src/utils/corruptionDetection.ts +346 -345
  349. package/src/utils/dateFormatting.ts +20 -20
  350. package/src/utils/deepClone.ts +77 -78
  351. package/src/utils/diagnostics.ts +125 -129
  352. package/src/utils/errorHandling.ts +80 -80
  353. package/src/utils/formatting.ts +220 -213
  354. package/src/utils/list-detection.ts +32 -42
  355. package/src/utils/logger.ts +412 -404
  356. package/src/utils/parsingHelpers.ts +190 -190
  357. package/src/utils/stripTrackedChanges.ts +356 -353
  358. package/src/utils/textDiff.ts +100 -100
  359. package/src/utils/units.ts +421 -421
  360. package/src/utils/validation.ts +553 -542
  361. package/src/utils/xmlSanitization.ts +179 -182
  362. package/src/validation/RevisionAutoFixer.ts +541 -542
  363. package/src/validation/RevisionValidator.ts +470 -460
  364. package/src/validation/ValidationRules.ts +338 -338
  365. package/src/validation/index.ts +30 -30
  366. package/src/xml/XMLBuilder.ts +857 -871
  367. package/src/xml/XMLParser.ts +877 -919
  368. package/src/zip/ZipHandler.ts +629 -637
  369. package/src/zip/ZipReader.ts +295 -299
  370. package/src/zip/ZipWriter.ts +374 -390
  371. package/src/zip/types.ts +116 -116
@@ -1,1040 +1,981 @@
1
- /**
2
- * NumberingLevel - Defines formatting for a single level in a list
3
- *
4
- * A numbering level specifies how a particular list level (0-8) should be formatted,
5
- * including the numbering format (bullet, decimal, roman, etc.), text template,
6
- * alignment, and indentation.
7
- */
8
-
9
- import { XMLBuilder, XMLElement } from "../xml/XMLBuilder";
10
-
11
- /**
12
- * Word-native bullet character mappings
13
- *
14
- * Microsoft Word uses specific fonts with Private Use Area (PUA) characters
15
- * for bullet points. These mappings ensure 100% compatibility with Word's
16
- * native bullet rendering.
17
- *
18
- * Pattern: Levels 0,3,6 = filled bullet; 1,4,7 = open circle; 2,5,8 = filled square
19
- */
20
- export const WORD_NATIVE_BULLETS = {
21
- /** Filled bullet (levels 0, 3, 6) - Symbol font U+F0B7 */
22
- FILLED_BULLET: { char: "\uF0B7", font: "Symbol" },
23
- /** Open circle (levels 1, 4, 7) - Courier New U+006F (lowercase 'o') */
24
- OPEN_CIRCLE: { char: "\u006F", font: "Courier New" },
25
- /** Filled square (levels 2, 5, 8) - Wingdings U+F0A7 */
26
- FILLED_SQUARE: { char: "\uF0A7", font: "Wingdings" },
27
- } as const;
28
-
29
- /**
30
- * Type for Word-native bullet definition
31
- */
32
- export type WordNativeBullet =
33
- (typeof WORD_NATIVE_BULLETS)[keyof typeof WORD_NATIVE_BULLETS];
34
-
35
- /**
36
- * Numbering format types supported by Word
37
- */
38
- export type NumberFormat =
39
- | "bullet" // Bullet character
40
- | "decimal" // 1, 2, 3, ...
41
- | "lowerRoman" // i, ii, iii, ...
42
- | "upperRoman" // I, II, III, ...
43
- | "lowerLetter" // a, b, c, ...
44
- | "upperLetter" // A, B, C, ...
45
- | "ordinal" // 1st, 2nd, 3rd, ...
46
- | "cardinalText" // One, Two, Three, ...
47
- | "ordinalText" // First, Second, Third, ...
48
- | "hex" // 0x01, 0x02, ...
49
- | "chicago" // *, †, ‡, §, ...
50
- | "decimal zero"; // 01, 02, 03, ...
51
-
52
- /**
53
- * Alignment for the numbering text
54
- */
55
- export type NumberAlignment = "left" | "center" | "right" | "start" | "end";
56
-
57
- /**
58
- * Properties for creating a numbering level
59
- */
60
- export interface NumberingLevelProperties {
61
- /** The level index (0-8, where 0 is the outermost level) */
62
- level: number;
63
-
64
- /** The numbering format */
65
- format: NumberFormat;
66
-
67
- /** The text template (e.g., "%1." for decimal, "•" for bullet) */
68
- text: string;
69
-
70
- /** Alignment of the numbering text */
71
- alignment?: NumberAlignment;
72
-
73
- /** Starting value (for numeric formats, default: 1) */
74
- start?: number;
75
-
76
- /** Left indentation in twips (can be negative for outdents into margin) */
77
- leftIndent?: number;
78
-
79
- /** Hanging indentation in twips (for the text after the number) */
80
- hangingIndent?: number;
81
-
82
- /** Font family for the numbering character (useful for bullets) */
83
- font?: string;
84
-
85
- /** Font size in half-points (e.g., 22 = 11pt) */
86
- fontSize?: number;
87
-
88
- /** Whether to show text after the number (default: true) */
89
- isLegalNumberingStyle?: boolean;
90
-
91
- /** Suffix after the number (tab, space, or nothing) */
92
- suffix?: "tab" | "space" | "nothing";
93
-
94
- /** Text color in hex (without #) */
95
- color?: string;
96
-
97
- /** Whether the numbering text is bold */
98
- bold?: boolean;
99
-
100
- /** Whether the numbering text is italic */
101
- italic?: boolean;
102
-
103
- /** Underline style for numbering text ('single', 'double', 'wave', 'dotted', 'dash', etc.) */
104
- underline?: string;
105
-
106
- /**
107
- * Level at which numbering should restart (w:lvlRestart per ECMA-376 Part 1 §17.9.11)
108
- * Specifies when to restart this level's numbering based on a higher-level change:
109
- * - 0: Never restart (continues throughout document)
110
- * - 1-8: Restart when the specified level changes
111
- * - undefined (default): Restart when level-1 changes (standard behavior)
112
- */
113
- lvlRestart?: number;
114
-
115
- /** Paragraph style ID linked to this numbering level (w:pStyle per ECMA-376 §17.9.23) */
116
- pStyle?: string;
117
- }
118
-
119
- /**
120
- * Represents a single level in a numbering definition
121
- */
122
- export class NumberingLevel {
123
- private properties: Required<Omit<NumberingLevelProperties, 'lvlRestart' | 'underline' | 'pStyle'>> & Pick<NumberingLevelProperties, 'lvlRestart' | 'underline' | 'pStyle'>;
124
-
125
- /**
126
- * Creates a new numbering level
127
- * @param properties The level properties
128
- */
129
- constructor(properties: NumberingLevelProperties) {
130
- // Set defaults
131
- this.properties = {
132
- level: properties.level,
133
- format: properties.format,
134
- text: properties.text,
135
- alignment: properties.alignment || "left",
136
- start: properties.start !== undefined ? properties.start : 1,
137
- leftIndent:
138
- properties.leftIndent !== undefined
139
- ? properties.leftIndent
140
- : 720 + properties.level * 360,
141
- hangingIndent:
142
- properties.hangingIndent !== undefined ? properties.hangingIndent : 360,
143
- font: properties.font || "Calibri",
144
- fontSize: properties.fontSize || 22, // 11pt default
145
- isLegalNumberingStyle:
146
- properties.isLegalNumberingStyle !== undefined
147
- ? properties.isLegalNumberingStyle
148
- : false,
149
- suffix: properties.suffix || "tab",
150
- color: properties.color || "000000",
151
- bold: properties.bold !== undefined ? properties.bold : false,
152
- italic: properties.italic !== undefined ? properties.italic : false,
153
- underline: properties.underline,
154
- lvlRestart: properties.lvlRestart, // undefined means default behavior (restart on level-1 change)
155
- pStyle: properties.pStyle,
156
- };
157
-
158
- this.validate();
159
- }
160
-
161
- /**
162
- * Validates the level properties
163
- */
164
- private validate(): void {
165
- if (this.properties.level < 0 || this.properties.level > 8) {
166
- throw new Error(
167
- `Level must be between 0 and 8, got ${this.properties.level}`
168
- );
169
- }
170
-
171
- // Note: leftIndent CAN be negative (outdent into margin) per ECMA-376
172
- // This is valid and used for hanging indents where bullets appear in margin
173
-
174
- if (this.properties.hangingIndent < 0) {
175
- throw new Error("Hanging indent must be non-negative");
176
- }
177
-
178
- if (this.properties.start < 0) {
179
- throw new Error("Start value must be non-negative");
180
- }
181
- }
182
-
183
- /**
184
- * Calculates safe indentation values for a given level based on page constraints.
185
- *
186
- * Use this instead of default indentation when working with narrow pages or
187
- * deep nesting levels to ensure content stays within page margins.
188
- *
189
- * @param level The level index (0-8)
190
- * @param pageWidthTwips Page width in twips (default: 12240 = 8.5 inches)
191
- * @param leftMarginTwips Left margin in twips (default: 1440 = 1 inch)
192
- * @param rightMarginTwips Right margin in twips (default: 1440 = 1 inch)
193
- * @param minContentWidth Minimum content width in twips (default: 2880 = 2 inches)
194
- * @returns Safe indentation values that won't exceed available space
195
- *
196
- * @example
197
- * ```typescript
198
- * // For a narrow page (6" wide with 0.5" margins)
199
- * const indent = NumberingLevel.calculateSafeIndentation(
200
- * 5, // level 5
201
- * 8640, // 6 inches page width
202
- * 720, // 0.5 inch left margin
203
- * 720 // 0.5 inch right margin
204
- * );
205
- * ```
206
- */
207
- static calculateSafeIndentation(
208
- level: number,
209
- pageWidthTwips = 12240,
210
- leftMarginTwips = 1440,
211
- rightMarginTwips = 1440,
212
- minContentWidth = 2880
213
- ): { leftIndent: number; hangingIndent: number } {
214
- if (level < 0 || level > 8) {
215
- throw new Error(`Invalid level ${level}. Level must be between 0 and 8.`);
216
- }
217
-
218
- // Calculate available content width
219
- const availableWidth = pageWidthTwips - leftMarginTwips - rightMarginTwips;
220
-
221
- // Calculate max safe indent (leave space for content)
222
- const maxSafeIndent = Math.max(0, availableWidth - minContentWidth);
223
-
224
- // Standard indentation
225
- const standardLeftIndent = 720 + level * 360;
226
- const hangingIndent = 360;
227
-
228
- // Cap at safe maximum
229
- const safeLeftIndent = Math.min(standardLeftIndent, maxSafeIndent);
230
-
231
- return {
232
- leftIndent: safeLeftIndent,
233
- hangingIndent,
234
- };
235
- }
236
-
237
- /**
238
- * Gets the level index
239
- */
240
- getLevel(): number {
241
- return this.properties.level;
242
- }
243
-
244
- /**
245
- * Gets the numbering format
246
- */
247
- getFormat(): NumberFormat {
248
- return this.properties.format;
249
- }
250
-
251
- /**
252
- * Gets the level properties
253
- */
254
- getProperties(): Required<Omit<NumberingLevelProperties, 'lvlRestart' | 'underline' | 'pStyle'>> & Pick<NumberingLevelProperties, 'lvlRestart' | 'underline' | 'pStyle'> {
255
- return { ...this.properties };
256
- }
257
-
258
- /**
259
- * Sets the left indentation
260
- * @param twips Indentation in twips
261
- */
262
- setLeftIndent(twips: number): this {
263
- // Note: Negative values are valid (outdent into margin) per ECMA-376
264
- this.properties.leftIndent = twips;
265
- return this;
266
- }
267
-
268
- /**
269
- * Sets the hanging indentation
270
- * @param twips Indentation in twips
271
- */
272
- setHangingIndent(twips: number): this {
273
- if (twips < 0) {
274
- throw new Error("Hanging indent must be non-negative");
275
- }
276
- this.properties.hangingIndent = twips;
277
- return this;
278
- }
279
-
280
- /**
281
- * Sets the font for the numbering character
282
- * @param font Font family name
283
- */
284
- setFont(font: string): this {
285
- this.properties.font = font;
286
- return this;
287
- }
288
-
289
- /**
290
- * Sets the alignment
291
- * @param alignment Alignment type
292
- */
293
- setAlignment(alignment: NumberAlignment): this {
294
- this.properties.alignment = alignment;
295
- return this;
296
- }
297
-
298
- /**
299
- * Sets the font size in half-points
300
- * @param halfPoints Font size in half-points (e.g., 24 = 12pt)
301
- */
302
- setFontSize(halfPoints: number): this {
303
- this.properties.fontSize = halfPoints;
304
- return this;
305
- }
306
-
307
- /**
308
- * Sets the level text (bullet character or number template)
309
- * @param text The text template (e.g., '•' for bullets, '%1.' for numbered)
310
- */
311
- setText(text: string): this {
312
- this.properties.text = text;
313
- return this;
314
- }
315
-
316
- /**
317
- * Sets the text color
318
- * @param color Hex color without # (e.g., '000000' for black)
319
- */
320
- setColor(color: string): this {
321
- this.properties.color = color;
322
- return this;
323
- }
324
-
325
- /**
326
- * Sets whether the numbering text is bold
327
- * @param bold Whether to make bold
328
- */
329
- setBold(bold: boolean): this {
330
- this.properties.bold = bold;
331
- return this;
332
- }
333
-
334
- /**
335
- * Sets whether the numbering text is italic
336
- * @param italic Whether to make italic
337
- */
338
- setItalic(italic: boolean): this {
339
- this.properties.italic = italic;
340
- return this;
341
- }
342
-
343
- /**
344
- * Gets whether the numbering text is italic
345
- */
346
- getItalic(): boolean {
347
- return this.properties.italic ?? false;
348
- }
349
-
350
- /**
351
- * Sets the underline style for numbering text
352
- * @param style Underline style ('single', 'double', 'wave', 'dotted', 'dash', etc.)
353
- */
354
- setUnderline(style: string | undefined): this {
355
- this.properties.underline = style;
356
- return this;
357
- }
358
-
359
- /**
360
- * Gets the underline style
361
- */
362
- getUnderline(): string | undefined {
363
- return this.properties.underline;
364
- }
365
-
366
- /**
367
- * Clears the underline style
368
- */
369
- clearUnderline(): this {
370
- this.properties.underline = undefined;
371
- return this;
372
- }
373
-
374
- /**
375
- * Sets the level restart behavior (w:lvlRestart per ECMA-376 Part 1 §17.9.11)
376
- *
377
- * Controls when this level's numbering restarts based on higher-level changes:
378
- * - 0: Never restart (continues throughout document)
379
- * - 1-8: Restart when the specified level changes
380
- * - undefined: Restart when level-1 changes (standard/default behavior)
381
- *
382
- * @param level The level that triggers restart (0-8), or undefined for default
383
- * @example
384
- * // Level 1 that never restarts (continuous across document)
385
- * level1.setLvlRestart(0);
386
- *
387
- * // Level 2 that restarts when level 0 changes (not level 1)
388
- * level2.setLvlRestart(0);
389
- */
390
- setLvlRestart(level: number | undefined): this {
391
- if (level !== undefined && (level < 0 || level > 8)) {
392
- throw new Error(`lvlRestart must be between 0 and 8, got ${level}`);
393
- }
394
- this.properties.lvlRestart = level;
395
- return this;
396
- }
397
-
398
- /**
399
- * Gets the level restart value
400
- * @returns The level that triggers restart, or undefined for default behavior
401
- */
402
- getLvlRestart(): number | undefined {
403
- return this.properties.lvlRestart;
404
- }
405
-
406
- /**
407
- * Sets the paragraph style ID linked to this numbering level
408
- * Links this level to a paragraph style definition (w:pStyle per ECMA-376 §17.9.23)
409
- * @param styleId The paragraph style ID
410
- */
411
- setParagraphStyle(styleId: string): this {
412
- this.properties.pStyle = styleId;
413
- return this;
414
- }
415
-
416
- /**
417
- * Gets the paragraph style ID linked to this numbering level
418
- */
419
- getParagraphStyle(): string | undefined {
420
- return this.properties.pStyle;
421
- }
422
-
423
- /**
424
- * Sets the numbering format (decimal, lowerLetter, bullet, etc.)
425
- * @param format The numbering format
426
- */
427
- setFormat(format: NumberFormat): this {
428
- this.properties.format = NumberingLevel.normalizeFormat(format);
429
- return this;
430
- }
431
-
432
- /**
433
- * Normalizes display-string format values to ECMA-376 ST_NumberFormat names.
434
- * Accepts both standard format names and common display shortcuts.
435
- */
436
- static normalizeFormat(format: string): NumberFormat {
437
- const corrections: Record<string, NumberFormat> = {
438
- "a.": "lowerLetter",
439
- "A.": "upperLetter",
440
- "i.": "lowerRoman",
441
- "I.": "upperRoman",
442
- "1.": "decimal",
443
- };
444
- return (corrections[format] ?? format) as NumberFormat;
445
- }
446
-
447
- /**
448
- * Generates the WordprocessingML XML for this level
449
- */
450
- toXML(): XMLElement {
451
- // ECMA-376 CT_Lvl element order: start, numFmt, lvlRestart, pStyle, isLgl, suff, lvlText, lvlJc, pPr, rPr
452
- const children: XMLElement[] = [];
453
-
454
- // 1. Start value
455
- children.push(
456
- XMLBuilder.wSelf("start", { "w:val": this.properties.start.toString() })
457
- );
458
-
459
- // 2. Number format
460
- children.push(
461
- XMLBuilder.wSelf("numFmt", { "w:val": this.properties.format })
462
- );
463
-
464
- // 3. Level restart (w:lvlRestart per ECMA-376 Part 1 §17.9.11)
465
- if (this.properties.lvlRestart !== undefined) {
466
- children.push(
467
- XMLBuilder.wSelf("lvlRestart", { "w:val": this.properties.lvlRestart.toString() })
468
- );
469
- }
470
-
471
- // 4. pStyle (paragraph style link)
472
- if (this.properties.pStyle) {
473
- children.push(
474
- XMLBuilder.wSelf("pStyle", { "w:val": this.properties.pStyle })
475
- );
476
- }
477
-
478
- // 5. Legal numbering style
479
- if (this.properties.isLegalNumberingStyle) {
480
- children.push(XMLBuilder.wSelf("isLgl"));
481
- }
482
-
483
- // 6. Suffix (what comes after the number)
484
- if (this.properties.suffix) {
485
- children.push(
486
- XMLBuilder.wSelf("suff", { "w:val": this.properties.suffix })
487
- );
488
- }
489
-
490
- // 7. Level text (e.g., "%1." or "•")
491
- children.push(
492
- XMLBuilder.wSelf("lvlText", { "w:val": this.properties.text })
493
- );
494
-
495
- // 8. Alignment
496
- children.push(
497
- XMLBuilder.wSelf("lvlJc", { "w:val": this.properties.alignment })
498
- );
499
-
500
- // 9. Paragraph properties (indentation)
501
- const ind = XMLBuilder.wSelf("ind", {
502
- "w:left": this.properties.leftIndent.toString(),
503
- "w:hanging": this.properties.hangingIndent.toString(),
504
- });
505
- const pPr = XMLBuilder.w("pPr", undefined, [ind]);
506
- children.push(pPr);
507
-
508
- // 10. Run properties (font)
509
- const rPrChildren: XMLElement[] = [];
510
-
511
- // Font
512
- rPrChildren.push(
513
- XMLBuilder.wSelf("rFonts", {
514
- "w:ascii": this.properties.font,
515
- "w:hAnsi": this.properties.font,
516
- "w:cs": this.properties.font,
517
- "w:hint": "default",
518
- })
519
- );
520
-
521
- // Bold
522
- if (this.properties.bold) {
523
- rPrChildren.push(XMLBuilder.wSelf("b"));
524
- rPrChildren.push(XMLBuilder.wSelf("bCs"));
525
- }
526
-
527
- // Italic
528
- if (this.properties.italic) {
529
- rPrChildren.push(XMLBuilder.wSelf("i"));
530
- rPrChildren.push(XMLBuilder.wSelf("iCs"));
531
- }
532
-
533
- // Underline
534
- if (this.properties.underline) {
535
- rPrChildren.push(
536
- XMLBuilder.wSelf("u", { "w:val": this.properties.underline })
537
- );
538
- }
539
-
540
- // Color
541
- if (this.properties.color) {
542
- rPrChildren.push(
543
- XMLBuilder.wSelf("color", { "w:val": this.properties.color })
544
- );
545
- }
546
-
547
- // Font size
548
- rPrChildren.push(
549
- XMLBuilder.wSelf("sz", { "w:val": this.properties.fontSize.toString() })
550
- );
551
- rPrChildren.push(
552
- XMLBuilder.wSelf("szCs", { "w:val": this.properties.fontSize.toString() })
553
- );
554
-
555
- const rPr = XMLBuilder.w("rPr", undefined, rPrChildren);
556
- children.push(rPr);
557
-
558
- return XMLBuilder.w(
559
- "lvl",
560
- { "w:ilvl": this.properties.level.toString() },
561
- children
562
- );
563
- }
564
-
565
- /**
566
- * Gets the recommended bullet symbol and font for a given level
567
- * @param level The level index (0-8)
568
- * @param style Optional bullet style ('standard', 'circle', 'square', 'arrow', 'check', 'word-native')
569
- * @returns Object with symbol and font properties
570
- */
571
- static getBulletSymbolWithFont(
572
- level: number,
573
- style:
574
- | "standard"
575
- | "circle"
576
- | "square"
577
- | "arrow"
578
- | "check"
579
- | "word-native" = "standard"
580
- ): { symbol: string; font: string } {
581
- const bulletSets = {
582
- // Standard style now uses Word-native encoding for maximum compatibility
583
- standard: [
584
- {
585
- symbol: WORD_NATIVE_BULLETS.FILLED_BULLET.char,
586
- font: WORD_NATIVE_BULLETS.FILLED_BULLET.font,
587
- }, // Level 0: Filled bullet (Symbol U+F0B7)
588
- {
589
- symbol: WORD_NATIVE_BULLETS.OPEN_CIRCLE.char,
590
- font: WORD_NATIVE_BULLETS.OPEN_CIRCLE.font,
591
- }, // Level 1: Open circle (Courier New U+006F)
592
- {
593
- symbol: WORD_NATIVE_BULLETS.FILLED_SQUARE.char,
594
- font: WORD_NATIVE_BULLETS.FILLED_SQUARE.font,
595
- }, // Level 2: Filled square (Wingdings U+F0A7)
596
- {
597
- symbol: WORD_NATIVE_BULLETS.FILLED_BULLET.char,
598
- font: WORD_NATIVE_BULLETS.FILLED_BULLET.font,
599
- }, // Level 3: Filled bullet
600
- {
601
- symbol: WORD_NATIVE_BULLETS.OPEN_CIRCLE.char,
602
- font: WORD_NATIVE_BULLETS.OPEN_CIRCLE.font,
603
- }, // Level 4: Open circle
604
- {
605
- symbol: WORD_NATIVE_BULLETS.FILLED_SQUARE.char,
606
- font: WORD_NATIVE_BULLETS.FILLED_SQUARE.font,
607
- }, // Level 5: Filled square
608
- {
609
- symbol: WORD_NATIVE_BULLETS.FILLED_BULLET.char,
610
- font: WORD_NATIVE_BULLETS.FILLED_BULLET.font,
611
- }, // Level 6: Filled bullet
612
- {
613
- symbol: WORD_NATIVE_BULLETS.OPEN_CIRCLE.char,
614
- font: WORD_NATIVE_BULLETS.OPEN_CIRCLE.font,
615
- }, // Level 7: Open circle
616
- {
617
- symbol: WORD_NATIVE_BULLETS.FILLED_SQUARE.char,
618
- font: WORD_NATIVE_BULLETS.FILLED_SQUARE.font,
619
- }, // Level 8: Filled square
620
- ],
621
- circle: [
622
- { symbol: "●", font: "Calibri" }, // Level 0: Filled circle (bold)
623
- { symbol: "○", font: "Calibri" }, // Level 1: Empty circle
624
- { symbol: "◉", font: "Calibri" }, // Level 2: Fisheye
625
- { symbol: "◯", font: "Calibri" }, // Level 3: Large circle
626
- { symbol: "⦿", font: "Calibri" }, // Level 4: Circled bullet
627
- { symbol: "○", font: "Calibri" }, // Level 5: Empty circle
628
- { symbol: "●", font: "Calibri" }, // Level 6: Filled circle
629
- { symbol: "○", font: "Calibri" }, // Level 7: Empty circle
630
- { symbol: "◉", font: "Calibri" }, // Level 8: Fisheye
631
- ],
632
- square: [
633
- { symbol: "■", font: "Calibri" }, // Level 0: Filled square
634
- { symbol: "□", font: "Calibri" }, // Level 1: Empty square
635
- { symbol: "▪", font: "Calibri" }, // Level 2: Small filled square
636
- { symbol: "▫", font: "Calibri" }, // Level 3: Small empty square
637
- { symbol: "◼", font: "Calibri" }, // Level 4: Medium filled square
638
- { symbol: "◻", font: "Calibri" }, // Level 5: Medium empty square
639
- { symbol: "■", font: "Calibri" }, // Level 6: Filled square
640
- { symbol: "□", font: "Calibri" }, // Level 7: Empty square
641
- { symbol: "▪", font: "Calibri" }, // Level 8: Small filled square
642
- ],
643
- arrow: [
644
- { symbol: "➢", font: "Calibri" }, // Level 0: Right arrow
645
- { symbol: "➣", font: "Calibri" }, // Level 1: Right arrow filled
646
- { symbol: "➤", font: "Calibri" }, // Level 2: Right arrow bold
647
- { symbol: "➔", font: "Calibri" }, // Level 3: Right arrow simple
648
- { symbol: "➜", font: "Calibri" }, // Level 4: Right arrow outline
649
- { symbol: "➢", font: "Calibri" }, // Level 5: Right arrow
650
- { symbol: "➣", font: "Calibri" }, // Level 6: Right arrow filled
651
- { symbol: "➤", font: "Calibri" }, // Level 7: Right arrow bold
652
- { symbol: "➔", font: "Calibri" }, // Level 8: Right arrow simple
653
- ],
654
- check: [
655
- { symbol: "✓", font: "Calibri" }, // Level 0: Check mark
656
- { symbol: "✔", font: "Calibri" }, // Level 1: Heavy check mark
657
- { symbol: "☑", font: "Calibri" }, // Level 2: Checked box
658
- { symbol: "✓", font: "Calibri" }, // Level 3: Check mark
659
- { symbol: "✔", font: "Calibri" }, // Level 4: Heavy check mark
660
- { symbol: "☑", font: "Calibri" }, // Level 5: Checked box
661
- { symbol: "✓", font: "Calibri" }, // Level 6: Check mark
662
- { symbol: "✔", font: "Calibri" }, // Level 7: Heavy check mark
663
- { symbol: "☑", font: "Calibri" }, // Level 8: Checked box
664
- ],
665
- "word-native": [
666
- // Word-native bullets using PUA characters with Symbol/Wingdings fonts
667
- {
668
- symbol: WORD_NATIVE_BULLETS.FILLED_BULLET.char,
669
- font: WORD_NATIVE_BULLETS.FILLED_BULLET.font,
670
- }, // Level 0: Filled bullet (Symbol U+F0B7)
671
- {
672
- symbol: WORD_NATIVE_BULLETS.OPEN_CIRCLE.char,
673
- font: WORD_NATIVE_BULLETS.OPEN_CIRCLE.font,
674
- }, // Level 1: Open circle (Courier New U+006F)
675
- {
676
- symbol: WORD_NATIVE_BULLETS.FILLED_SQUARE.char,
677
- font: WORD_NATIVE_BULLETS.FILLED_SQUARE.font,
678
- }, // Level 2: Filled square (Wingdings U+F0A7)
679
- {
680
- symbol: WORD_NATIVE_BULLETS.FILLED_BULLET.char,
681
- font: WORD_NATIVE_BULLETS.FILLED_BULLET.font,
682
- }, // Level 3: Filled bullet
683
- {
684
- symbol: WORD_NATIVE_BULLETS.OPEN_CIRCLE.char,
685
- font: WORD_NATIVE_BULLETS.OPEN_CIRCLE.font,
686
- }, // Level 4: Open circle
687
- {
688
- symbol: WORD_NATIVE_BULLETS.FILLED_SQUARE.char,
689
- font: WORD_NATIVE_BULLETS.FILLED_SQUARE.font,
690
- }, // Level 5: Filled square
691
- {
692
- symbol: WORD_NATIVE_BULLETS.FILLED_BULLET.char,
693
- font: WORD_NATIVE_BULLETS.FILLED_BULLET.font,
694
- }, // Level 6: Filled bullet
695
- {
696
- symbol: WORD_NATIVE_BULLETS.OPEN_CIRCLE.char,
697
- font: WORD_NATIVE_BULLETS.OPEN_CIRCLE.font,
698
- }, // Level 7: Open circle
699
- {
700
- symbol: WORD_NATIVE_BULLETS.FILLED_SQUARE.char,
701
- font: WORD_NATIVE_BULLETS.FILLED_SQUARE.font,
702
- }, // Level 8: Filled square
703
- ],
704
- };
705
-
706
- const selectedSet = bulletSets[style];
707
- const levelIndex = level % selectedSet.length;
708
- const result = selectedSet[levelIndex];
709
-
710
- // Fallback to standard bullet if somehow undefined
711
- return result || { symbol: "•", font: "Calibri" };
712
- }
713
-
714
- /**
715
- * Calculates the standard indentation values for a given level
716
- * @param level The level index (0-8)
717
- * @returns Object with leftIndent and hangingIndent in twips
718
- * @example
719
- * const indent = NumberingLevel.calculateStandardIndentation(0);
720
- * // Returns: { leftIndent: 720, hangingIndent: 360 } (0.5" left, 0.25" hanging)
721
- *
722
- * const indent2 = NumberingLevel.calculateStandardIndentation(2);
723
- * // Returns: { leftIndent: 1440, hangingIndent: 360 } (1.0" left, 0.25" hanging)
724
- */
725
- static calculateStandardIndentation(level: number): {
726
- leftIndent: number;
727
- hangingIndent: number;
728
- } {
729
- if (level < 0 || level > 8) {
730
- throw new Error(`Invalid level ${level}. Level must be between 0 and 8.`);
731
- }
732
-
733
- return {
734
- leftIndent: 720 + level * 360,
735
- hangingIndent: 360,
736
- };
737
- }
738
-
739
- /**
740
- * Gets the standard number format for a given level
741
- * @param level The level index (0-8)
742
- * @returns The recommended number format for this level
743
- * @example
744
- * NumberingLevel.getStandardNumberFormat(0); // 'decimal' (1., 2., 3.)
745
- * NumberingLevel.getStandardNumberFormat(1); // 'lowerLetter' (a., b., c.)
746
- * NumberingLevel.getStandardNumberFormat(2); // 'lowerRoman' (i., ii., iii.)
747
- * NumberingLevel.getStandardNumberFormat(3); // 'upperLetter' (A., B., C.)
748
- * NumberingLevel.getStandardNumberFormat(4); // 'upperRoman' (I., II., III.)
749
- */
750
- static getStandardNumberFormat(level: number): NumberFormat {
751
- if (level < 0 || level > 8) {
752
- throw new Error(`Invalid level ${level}. Level must be between 0 and 8.`);
753
- }
754
-
755
- const formats: NumberFormat[] = [
756
- "decimal", // Level 0: 1., 2., 3.
757
- "lowerLetter", // Level 1: a., b., c.
758
- "lowerRoman", // Level 2: i., ii., iii.
759
- "upperLetter", // Level 3: A., B., C.
760
- "upperRoman", // Level 4: I., II., III.
761
- ];
762
-
763
- const result = formats[level % formats.length];
764
- return result || "decimal"; // Fallback to decimal (should never happen)
765
- }
766
-
767
- /**
768
- * Creates a bullet list level
769
- *
770
- * When called without parameters, uses Word-native encoding for the level:
771
- * - Levels 0, 3, 6: Filled bullet (Symbol font, U+F0B7)
772
- * - Levels 1, 4, 7: Open circle (Courier New, U+006F)
773
- * - Levels 2, 5, 8: Filled square (Wingdings, U+F0A7)
774
- *
775
- * @param level The level index (0-8)
776
- * @param bullet Optional bullet character (defaults to Word-native for level)
777
- * @param font Optional font to use for the bullet (defaults to Word-native for level)
778
- */
779
- static createBulletLevel(
780
- level: number,
781
- bullet?: string,
782
- font?: string
783
- ): NumberingLevel {
784
- // Use Word-native defaults when not specified
785
- const defaults = NumberingLevel.getBulletSymbolWithFont(level, "standard");
786
- const actualBullet = bullet ?? defaults.symbol;
787
- const actualFont = font ?? defaults.font;
788
-
789
- return new NumberingLevel({
790
- level,
791
- format: "bullet",
792
- text: actualBullet,
793
- alignment: "left",
794
- font: actualFont,
795
- fontSize: 24, // 12pt
796
- bold: false,
797
- color: "000000",
798
- leftIndent: 720 + level * 360,
799
- hangingIndent: 360,
800
- });
801
- }
802
-
803
- /**
804
- * Creates a decimal list level (1, 2, 3, ...)
805
- * @param level The level index (0-8)
806
- * @param template The text template (default: '%1.')
807
- */
808
- static createDecimalLevel(
809
- level: number,
810
- template = `%${level + 1}.`
811
- ): NumberingLevel {
812
- return new NumberingLevel({
813
- level,
814
- format: "decimal",
815
- text: template,
816
- alignment: "left",
817
- font: "Verdana",
818
- fontSize: 24, // 12pt
819
- bold: false,
820
- color: "000000",
821
- leftIndent: 720 + level * 360,
822
- hangingIndent: 360,
823
- });
824
- }
825
-
826
- /**
827
- * Creates a lower roman list level (i, ii, iii, ...)
828
- * @param level The level index (0-8)
829
- * @param template The text template (default: '%1.')
830
- */
831
- static createLowerRomanLevel(
832
- level: number,
833
- template = `%${level + 1}.`
834
- ): NumberingLevel {
835
- return new NumberingLevel({
836
- level,
837
- format: "lowerRoman",
838
- text: template,
839
- alignment: "left",
840
- font: "Verdana",
841
- fontSize: 24, // 12pt
842
- bold: false,
843
- color: "000000",
844
- leftIndent: 720 + level * 360,
845
- hangingIndent: 360,
846
- });
847
- }
848
-
849
- /**
850
- * Creates an upper roman list level (I, II, III, ...)
851
- * @param level The level index (0-8)
852
- * @param template The text template (default: '%1.')
853
- */
854
- static createUpperRomanLevel(
855
- level: number,
856
- template = `%${level + 1}.`
857
- ): NumberingLevel {
858
- return new NumberingLevel({
859
- level,
860
- format: "upperRoman",
861
- text: template,
862
- alignment: "left",
863
- font: "Verdana",
864
- fontSize: 24, // 12pt
865
- bold: false,
866
- color: "000000",
867
- leftIndent: 720 + level * 360,
868
- hangingIndent: 360,
869
- });
870
- }
871
-
872
- /**
873
- * Creates a lower letter list level (a, b, c, ...)
874
- * @param level The level index (0-8)
875
- * @param template The text template (default: '%1.')
876
- */
877
- static createLowerLetterLevel(
878
- level: number,
879
- template = `%${level + 1}.`
880
- ): NumberingLevel {
881
- return new NumberingLevel({
882
- level,
883
- format: "lowerLetter",
884
- text: template,
885
- alignment: "left",
886
- font: "Verdana",
887
- fontSize: 24, // 12pt
888
- bold: false,
889
- color: "000000",
890
- leftIndent: 720 + level * 360,
891
- hangingIndent: 360,
892
- });
893
- }
894
-
895
- /**
896
- * Creates an upper letter list level (A, B, C, ...)
897
- * @param level The level index (0-8)
898
- * @param template The text template (default: '%1.')
899
- */
900
- static createUpperLetterLevel(
901
- level: number,
902
- template = `%${level + 1}.`
903
- ): NumberingLevel {
904
- return new NumberingLevel({
905
- level,
906
- format: "upperLetter",
907
- text: template,
908
- alignment: "left",
909
- font: "Verdana",
910
- fontSize: 24, // 12pt
911
- bold: false,
912
- color: "000000",
913
- leftIndent: 720 + level * 360,
914
- hangingIndent: 360,
915
- });
916
- }
917
-
918
- /**
919
- * Factory method for creating a numbering level
920
- * @param properties The level properties
921
- */
922
- static create(properties: NumberingLevelProperties): NumberingLevel {
923
- return new NumberingLevel(properties);
924
- }
925
-
926
- /**
927
- * Creates a NumberingLevel from XML element
928
- * @param xml The XML string of the <w:lvl> element
929
- * @returns NumberingLevel instance
930
- */
931
- static fromXML(xml: string): NumberingLevel {
932
- // Extract level index (required)
933
- const ilvlMatch = /<w:lvl[^>]*w:ilvl="([^"]+)"/.exec(xml);
934
- if (!ilvlMatch?.[1]) {
935
- throw new Error("Missing required w:ilvl attribute");
936
- }
937
- const level = parseInt(ilvlMatch[1], 10);
938
-
939
- // Extract number format (required)
940
- const numFmtMatch = /<w:numFmt[^>]*w:val="([^"]+)"/.exec(xml);
941
- if (!numFmtMatch?.[1]) {
942
- throw new Error("Missing required w:numFmt element");
943
- }
944
- const format = NumberingLevel.normalizeFormat(numFmtMatch[1]);
945
-
946
- // Extract level text (optional - can be empty for placeholder levels)
947
- const lvlTextMatch = /<w:lvlText[^>]*w:val="([^"]*)"/.exec(xml);
948
- const text = lvlTextMatch?.[1] !== undefined ? lvlTextMatch[1] : "";
949
-
950
- // Extract alignment (optional, default: left)
951
- const lvlJcMatch = /<w:lvlJc[^>]*w:val="([^"]+)"/.exec(xml);
952
- const alignment = (
953
- lvlJcMatch?.[1] ? lvlJcMatch[1] : "left"
954
- ) as NumberAlignment;
955
-
956
- // Extract start value (optional, default: 1)
957
- const startMatch = /<w:start[^>]*w:val="([^"]+)"/.exec(xml);
958
- const start = startMatch?.[1] ? parseInt(startMatch[1], 10) : 1;
959
-
960
- // Extract suffix (optional, default: tab)
961
- const suffixMatch = /<w:suff[^>]*w:val="([^"]+)"/.exec(xml);
962
- const suffix =
963
- suffixMatch?.[1]
964
- ? (suffixMatch[1] as "tab" | "space" | "nothing")
965
- : "tab";
966
-
967
- // Extract level restart (w:lvlRestart per ECMA-376 Part 1 §17.9.11)
968
- let lvlRestart: number | undefined;
969
- const lvlRestartMatch = /<w:lvlRestart[^>]*w:val="([^"]+)"/.exec(xml);
970
- if (lvlRestartMatch?.[1]) {
971
- lvlRestart = parseInt(lvlRestartMatch[1], 10);
972
- }
973
-
974
- // Extract pStyle (paragraph style link)
975
- let pStyle: string | undefined;
976
- const pStyleMatch = /<w:pStyle[^>]*w:val="([^"]+)"/.exec(xml);
977
- if (pStyleMatch?.[1]) {
978
- pStyle = pStyleMatch[1];
979
- }
980
-
981
- // Extract indentation from <w:pPr><w:ind>
982
- let leftIndent = 720 + level * 360; // default
983
- let hangingIndent = 360; // default
984
- const indMatch = /<w:ind[^>]*\/>/.exec(xml);
985
- if (indMatch) {
986
- const indElement = indMatch[0];
987
- const leftMatch = /w:left="([^"]+)"/.exec(indElement);
988
- const hangingMatch = /w:hanging="([^"]+)"/.exec(indElement);
989
-
990
- if (leftMatch?.[1]) leftIndent = parseInt(leftMatch[1], 10);
991
- if (hangingMatch?.[1])
992
- hangingIndent = parseInt(hangingMatch[1], 10);
993
- }
994
-
995
- // Extract font and size from <w:rPr>
996
- let font = "Calibri";
997
- let fontSize = 22;
998
-
999
- const rFontsMatch = /<w:rFonts[^>]*\/>/.exec(xml);
1000
- if (rFontsMatch) {
1001
- const rFontsElement = rFontsMatch[0];
1002
- const asciiMatch = /w:ascii="([^"]+)"/.exec(rFontsElement);
1003
- if (asciiMatch?.[1]) font = asciiMatch[1];
1004
- }
1005
-
1006
- const szMatch = /<w:sz[^>]*w:val="([^"]+)"/.exec(xml);
1007
- if (szMatch?.[1]) fontSize = parseInt(szMatch[1], 10);
1008
-
1009
- // List prefixes should never have bold, italic, or underline - ignore source XML formatting
1010
- const bold = false;
1011
- const italic = false;
1012
- const underline: string | undefined = undefined;
1013
-
1014
- // Extract color from <w:rPr>
1015
- let color: string | undefined;
1016
- const colorMatch = /<w:color[^>]*w:val="([^"]+)"/.exec(xml);
1017
- if (colorMatch?.[1]) {
1018
- color = colorMatch[1];
1019
- }
1020
-
1021
- return new NumberingLevel({
1022
- level,
1023
- format,
1024
- text,
1025
- alignment,
1026
- start,
1027
- leftIndent,
1028
- hangingIndent,
1029
- font,
1030
- fontSize,
1031
- suffix,
1032
- bold,
1033
- italic,
1034
- underline,
1035
- color,
1036
- lvlRestart,
1037
- pStyle,
1038
- });
1039
- }
1040
- }
1
+ /**
2
+ * NumberingLevel - Defines formatting for a single level in a list
3
+ *
4
+ * A numbering level specifies how a particular list level (0-8) should be formatted,
5
+ * including the numbering format (bullet, decimal, roman, etc.), text template,
6
+ * alignment, and indentation.
7
+ */
8
+
9
+ import { XMLBuilder, XMLElement } from '../xml/XMLBuilder';
10
+
11
+ /**
12
+ * Word-native bullet character mappings
13
+ *
14
+ * Microsoft Word uses specific fonts with Private Use Area (PUA) characters
15
+ * for bullet points. These mappings ensure 100% compatibility with Word's
16
+ * native bullet rendering.
17
+ *
18
+ * Pattern: Levels 0,3,6 = filled bullet; 1,4,7 = open circle; 2,5,8 = filled square
19
+ */
20
+ export const WORD_NATIVE_BULLETS = {
21
+ /** Filled bullet (levels 0, 3, 6) - Symbol font U+F0B7 */
22
+ FILLED_BULLET: { char: '\uF0B7', font: 'Symbol' },
23
+ /** Open circle (levels 1, 4, 7) - Courier New U+006F (lowercase 'o') */
24
+ OPEN_CIRCLE: { char: '\u006F', font: 'Courier New' },
25
+ /** Filled square (levels 2, 5, 8) - Wingdings U+F0A7 */
26
+ FILLED_SQUARE: { char: '\uF0A7', font: 'Wingdings' },
27
+ } as const;
28
+
29
+ /**
30
+ * Type for Word-native bullet definition
31
+ */
32
+ export type WordNativeBullet = (typeof WORD_NATIVE_BULLETS)[keyof typeof WORD_NATIVE_BULLETS];
33
+
34
+ /**
35
+ * Numbering format types supported by Word
36
+ */
37
+ export type NumberFormat =
38
+ | 'bullet' // Bullet character
39
+ | 'decimal' // 1, 2, 3, ...
40
+ | 'lowerRoman' // i, ii, iii, ...
41
+ | 'upperRoman' // I, II, III, ...
42
+ | 'lowerLetter' // a, b, c, ...
43
+ | 'upperLetter' // A, B, C, ...
44
+ | 'ordinal' // 1st, 2nd, 3rd, ...
45
+ | 'cardinalText' // One, Two, Three, ...
46
+ | 'ordinalText' // First, Second, Third, ...
47
+ | 'hex' // 0x01, 0x02, ...
48
+ | 'chicago' // *, †, ‡, §, ...
49
+ | 'decimal zero'; // 01, 02, 03, ...
50
+
51
+ /**
52
+ * Alignment for the numbering text
53
+ */
54
+ export type NumberAlignment = 'left' | 'center' | 'right' | 'start' | 'end';
55
+
56
+ /**
57
+ * Properties for creating a numbering level
58
+ */
59
+ export interface NumberingLevelProperties {
60
+ /** The level index (0-8, where 0 is the outermost level) */
61
+ level: number;
62
+
63
+ /** The numbering format */
64
+ format: NumberFormat;
65
+
66
+ /** The text template (e.g., "%1." for decimal, "•" for bullet) */
67
+ text: string;
68
+
69
+ /** Alignment of the numbering text */
70
+ alignment?: NumberAlignment;
71
+
72
+ /** Starting value (for numeric formats, default: 1) */
73
+ start?: number;
74
+
75
+ /** Left indentation in twips (can be negative for outdents into margin) */
76
+ leftIndent?: number;
77
+
78
+ /** Hanging indentation in twips (for the text after the number) */
79
+ hangingIndent?: number;
80
+
81
+ /** Font family for the numbering character (useful for bullets) */
82
+ font?: string;
83
+
84
+ /** Font size in half-points (e.g., 22 = 11pt) */
85
+ fontSize?: number;
86
+
87
+ /** Whether to show text after the number (default: true) */
88
+ isLegalNumberingStyle?: boolean;
89
+
90
+ /** Suffix after the number (tab, space, or nothing) */
91
+ suffix?: 'tab' | 'space' | 'nothing';
92
+
93
+ /** Text color in hex (without #) */
94
+ color?: string;
95
+
96
+ /** Whether the numbering text is bold */
97
+ bold?: boolean;
98
+
99
+ /** Whether the numbering text is italic */
100
+ italic?: boolean;
101
+
102
+ /** Underline style for numbering text ('single', 'double', 'wave', 'dotted', 'dash', etc.) */
103
+ underline?: string;
104
+
105
+ /**
106
+ * Level at which numbering should restart (w:lvlRestart per ECMA-376 Part 1 §17.9.11)
107
+ * Specifies when to restart this level's numbering based on a higher-level change:
108
+ * - 0: Never restart (continues throughout document)
109
+ * - 1-8: Restart when the specified level changes
110
+ * - undefined (default): Restart when level-1 changes (standard behavior)
111
+ */
112
+ lvlRestart?: number;
113
+
114
+ /** Paragraph style ID linked to this numbering level (w:pStyle per ECMA-376 §17.9.23) */
115
+ pStyle?: string;
116
+ }
117
+
118
+ /**
119
+ * Represents a single level in a numbering definition
120
+ */
121
+ export class NumberingLevel {
122
+ private properties: Required<
123
+ Omit<NumberingLevelProperties, 'lvlRestart' | 'underline' | 'pStyle'>
124
+ > &
125
+ Pick<NumberingLevelProperties, 'lvlRestart' | 'underline' | 'pStyle'>;
126
+
127
+ /**
128
+ * Creates a new numbering level
129
+ * @param properties The level properties
130
+ */
131
+ constructor(properties: NumberingLevelProperties) {
132
+ // Set defaults
133
+ this.properties = {
134
+ level: properties.level,
135
+ format: properties.format,
136
+ text: properties.text,
137
+ alignment: properties.alignment || 'left',
138
+ start: properties.start !== undefined ? properties.start : 1,
139
+ leftIndent:
140
+ properties.leftIndent !== undefined ? properties.leftIndent : 720 + properties.level * 360,
141
+ hangingIndent: properties.hangingIndent !== undefined ? properties.hangingIndent : 360,
142
+ font: properties.font || 'Calibri',
143
+ fontSize: properties.fontSize || 22, // 11pt default
144
+ isLegalNumberingStyle:
145
+ properties.isLegalNumberingStyle !== undefined ? properties.isLegalNumberingStyle : false,
146
+ suffix: properties.suffix || 'tab',
147
+ color: properties.color || '000000',
148
+ bold: properties.bold !== undefined ? properties.bold : false,
149
+ italic: properties.italic !== undefined ? properties.italic : false,
150
+ underline: properties.underline,
151
+ lvlRestart: properties.lvlRestart, // undefined means default behavior (restart on level-1 change)
152
+ pStyle: properties.pStyle,
153
+ };
154
+
155
+ this.validate();
156
+ }
157
+
158
+ /**
159
+ * Validates the level properties
160
+ */
161
+ private validate(): void {
162
+ if (this.properties.level < 0 || this.properties.level > 8) {
163
+ throw new Error(`Level must be between 0 and 8, got ${this.properties.level}`);
164
+ }
165
+
166
+ // Note: leftIndent CAN be negative (outdent into margin) per ECMA-376
167
+ // This is valid and used for hanging indents where bullets appear in margin
168
+
169
+ if (this.properties.hangingIndent < 0) {
170
+ throw new Error('Hanging indent must be non-negative');
171
+ }
172
+
173
+ if (this.properties.start < 0) {
174
+ throw new Error('Start value must be non-negative');
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Calculates safe indentation values for a given level based on page constraints.
180
+ *
181
+ * Use this instead of default indentation when working with narrow pages or
182
+ * deep nesting levels to ensure content stays within page margins.
183
+ *
184
+ * @param level The level index (0-8)
185
+ * @param pageWidthTwips Page width in twips (default: 12240 = 8.5 inches)
186
+ * @param leftMarginTwips Left margin in twips (default: 1440 = 1 inch)
187
+ * @param rightMarginTwips Right margin in twips (default: 1440 = 1 inch)
188
+ * @param minContentWidth Minimum content width in twips (default: 2880 = 2 inches)
189
+ * @returns Safe indentation values that won't exceed available space
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * // For a narrow page (6" wide with 0.5" margins)
194
+ * const indent = NumberingLevel.calculateSafeIndentation(
195
+ * 5, // level 5
196
+ * 8640, // 6 inches page width
197
+ * 720, // 0.5 inch left margin
198
+ * 720 // 0.5 inch right margin
199
+ * );
200
+ * ```
201
+ */
202
+ static calculateSafeIndentation(
203
+ level: number,
204
+ pageWidthTwips = 12240,
205
+ leftMarginTwips = 1440,
206
+ rightMarginTwips = 1440,
207
+ minContentWidth = 2880
208
+ ): { leftIndent: number; hangingIndent: number } {
209
+ if (level < 0 || level > 8) {
210
+ throw new Error(`Invalid level ${level}. Level must be between 0 and 8.`);
211
+ }
212
+
213
+ // Calculate available content width
214
+ const availableWidth = pageWidthTwips - leftMarginTwips - rightMarginTwips;
215
+
216
+ // Calculate max safe indent (leave space for content)
217
+ const maxSafeIndent = Math.max(0, availableWidth - minContentWidth);
218
+
219
+ // Standard indentation
220
+ const standardLeftIndent = 720 + level * 360;
221
+ const hangingIndent = 360;
222
+
223
+ // Cap at safe maximum
224
+ const safeLeftIndent = Math.min(standardLeftIndent, maxSafeIndent);
225
+
226
+ return {
227
+ leftIndent: safeLeftIndent,
228
+ hangingIndent,
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Gets the level index
234
+ */
235
+ getLevel(): number {
236
+ return this.properties.level;
237
+ }
238
+
239
+ /**
240
+ * Gets the numbering format
241
+ */
242
+ getFormat(): NumberFormat {
243
+ return this.properties.format;
244
+ }
245
+
246
+ /**
247
+ * Gets the level properties
248
+ */
249
+ getProperties(): Required<Omit<NumberingLevelProperties, 'lvlRestart' | 'underline' | 'pStyle'>> &
250
+ Pick<NumberingLevelProperties, 'lvlRestart' | 'underline' | 'pStyle'> {
251
+ return { ...this.properties };
252
+ }
253
+
254
+ /**
255
+ * Sets the left indentation
256
+ * @param twips Indentation in twips
257
+ */
258
+ setLeftIndent(twips: number): this {
259
+ // Note: Negative values are valid (outdent into margin) per ECMA-376
260
+ this.properties.leftIndent = twips;
261
+ return this;
262
+ }
263
+
264
+ /**
265
+ * Sets the hanging indentation
266
+ * @param twips Indentation in twips
267
+ */
268
+ setHangingIndent(twips: number): this {
269
+ if (twips < 0) {
270
+ throw new Error('Hanging indent must be non-negative');
271
+ }
272
+ this.properties.hangingIndent = twips;
273
+ return this;
274
+ }
275
+
276
+ /**
277
+ * Sets the font for the numbering character
278
+ * @param font Font family name
279
+ */
280
+ setFont(font: string): this {
281
+ this.properties.font = font;
282
+ return this;
283
+ }
284
+
285
+ /**
286
+ * Sets the alignment
287
+ * @param alignment Alignment type
288
+ */
289
+ setAlignment(alignment: NumberAlignment): this {
290
+ this.properties.alignment = alignment;
291
+ return this;
292
+ }
293
+
294
+ /**
295
+ * Sets the font size in half-points
296
+ * @param halfPoints Font size in half-points (e.g., 24 = 12pt)
297
+ */
298
+ setFontSize(halfPoints: number): this {
299
+ this.properties.fontSize = halfPoints;
300
+ return this;
301
+ }
302
+
303
+ /**
304
+ * Sets the level text (bullet character or number template)
305
+ * @param text The text template (e.g., '•' for bullets, '%1.' for numbered)
306
+ */
307
+ setText(text: string): this {
308
+ this.properties.text = text;
309
+ return this;
310
+ }
311
+
312
+ /**
313
+ * Sets the text color
314
+ * @param color Hex color without # (e.g., '000000' for black)
315
+ */
316
+ setColor(color: string): this {
317
+ this.properties.color = color;
318
+ return this;
319
+ }
320
+
321
+ /**
322
+ * Sets whether the numbering text is bold
323
+ * @param bold Whether to make bold
324
+ */
325
+ setBold(bold: boolean): this {
326
+ this.properties.bold = bold;
327
+ return this;
328
+ }
329
+
330
+ /**
331
+ * Sets whether the numbering text is italic
332
+ * @param italic Whether to make italic
333
+ */
334
+ setItalic(italic: boolean): this {
335
+ this.properties.italic = italic;
336
+ return this;
337
+ }
338
+
339
+ /**
340
+ * Gets whether the numbering text is italic
341
+ */
342
+ getItalic(): boolean {
343
+ return this.properties.italic ?? false;
344
+ }
345
+
346
+ /**
347
+ * Sets the underline style for numbering text
348
+ * @param style Underline style ('single', 'double', 'wave', 'dotted', 'dash', etc.)
349
+ */
350
+ setUnderline(style: string | undefined): this {
351
+ this.properties.underline = style;
352
+ return this;
353
+ }
354
+
355
+ /**
356
+ * Gets the underline style
357
+ */
358
+ getUnderline(): string | undefined {
359
+ return this.properties.underline;
360
+ }
361
+
362
+ /**
363
+ * Clears the underline style
364
+ */
365
+ clearUnderline(): this {
366
+ this.properties.underline = undefined;
367
+ return this;
368
+ }
369
+
370
+ /**
371
+ * Sets the level restart behavior (w:lvlRestart per ECMA-376 Part 1 §17.9.11)
372
+ *
373
+ * Controls when this level's numbering restarts based on higher-level changes:
374
+ * - 0: Never restart (continues throughout document)
375
+ * - 1-8: Restart when the specified level changes
376
+ * - undefined: Restart when level-1 changes (standard/default behavior)
377
+ *
378
+ * @param level The level that triggers restart (0-8), or undefined for default
379
+ * @example
380
+ * // Level 1 that never restarts (continuous across document)
381
+ * level1.setLvlRestart(0);
382
+ *
383
+ * // Level 2 that restarts when level 0 changes (not level 1)
384
+ * level2.setLvlRestart(0);
385
+ */
386
+ setLvlRestart(level: number | undefined): this {
387
+ if (level !== undefined && (level < 0 || level > 8)) {
388
+ throw new Error(`lvlRestart must be between 0 and 8, got ${level}`);
389
+ }
390
+ this.properties.lvlRestart = level;
391
+ return this;
392
+ }
393
+
394
+ /**
395
+ * Gets the level restart value
396
+ * @returns The level that triggers restart, or undefined for default behavior
397
+ */
398
+ getLvlRestart(): number | undefined {
399
+ return this.properties.lvlRestart;
400
+ }
401
+
402
+ /**
403
+ * Sets the paragraph style ID linked to this numbering level
404
+ * Links this level to a paragraph style definition (w:pStyle per ECMA-376 §17.9.23)
405
+ * @param styleId The paragraph style ID
406
+ */
407
+ setParagraphStyle(styleId: string): this {
408
+ this.properties.pStyle = styleId;
409
+ return this;
410
+ }
411
+
412
+ /**
413
+ * Gets the paragraph style ID linked to this numbering level
414
+ */
415
+ getParagraphStyle(): string | undefined {
416
+ return this.properties.pStyle;
417
+ }
418
+
419
+ /**
420
+ * Sets the numbering format (decimal, lowerLetter, bullet, etc.)
421
+ * @param format The numbering format
422
+ */
423
+ setFormat(format: NumberFormat): this {
424
+ this.properties.format = NumberingLevel.normalizeFormat(format);
425
+ return this;
426
+ }
427
+
428
+ /**
429
+ * Normalizes display-string format values to ECMA-376 ST_NumberFormat names.
430
+ * Accepts both standard format names and common display shortcuts.
431
+ */
432
+ static normalizeFormat(format: string): NumberFormat {
433
+ const corrections: Record<string, NumberFormat> = {
434
+ 'a.': 'lowerLetter',
435
+ 'A.': 'upperLetter',
436
+ 'i.': 'lowerRoman',
437
+ 'I.': 'upperRoman',
438
+ '1.': 'decimal',
439
+ };
440
+ return (corrections[format] ?? format) as NumberFormat;
441
+ }
442
+
443
+ /**
444
+ * Generates the WordprocessingML XML for this level
445
+ */
446
+ toXML(): XMLElement {
447
+ // ECMA-376 CT_Lvl element order: start, numFmt, lvlRestart, pStyle, isLgl, suff, lvlText, lvlJc, pPr, rPr
448
+ const children: XMLElement[] = [];
449
+
450
+ // 1. Start value
451
+ children.push(XMLBuilder.wSelf('start', { 'w:val': this.properties.start.toString() }));
452
+
453
+ // 2. Number format
454
+ children.push(XMLBuilder.wSelf('numFmt', { 'w:val': this.properties.format }));
455
+
456
+ // 3. Level restart (w:lvlRestart per ECMA-376 Part 1 §17.9.11)
457
+ if (this.properties.lvlRestart !== undefined) {
458
+ children.push(
459
+ XMLBuilder.wSelf('lvlRestart', { 'w:val': this.properties.lvlRestart.toString() })
460
+ );
461
+ }
462
+
463
+ // 4. pStyle (paragraph style link)
464
+ if (this.properties.pStyle) {
465
+ children.push(XMLBuilder.wSelf('pStyle', { 'w:val': this.properties.pStyle }));
466
+ }
467
+
468
+ // 5. Legal numbering style
469
+ if (this.properties.isLegalNumberingStyle) {
470
+ children.push(XMLBuilder.wSelf('isLgl'));
471
+ }
472
+
473
+ // 6. Suffix (what comes after the number)
474
+ if (this.properties.suffix) {
475
+ children.push(XMLBuilder.wSelf('suff', { 'w:val': this.properties.suffix }));
476
+ }
477
+
478
+ // 7. Level text (e.g., "%1." or "•")
479
+ children.push(XMLBuilder.wSelf('lvlText', { 'w:val': this.properties.text }));
480
+
481
+ // 8. Alignment
482
+ children.push(XMLBuilder.wSelf('lvlJc', { 'w:val': this.properties.alignment }));
483
+
484
+ // 9. Paragraph properties (indentation)
485
+ const ind = XMLBuilder.wSelf('ind', {
486
+ 'w:left': this.properties.leftIndent.toString(),
487
+ 'w:hanging': this.properties.hangingIndent.toString(),
488
+ });
489
+ const pPr = XMLBuilder.w('pPr', undefined, [ind]);
490
+ children.push(pPr);
491
+
492
+ // 10. Run properties (font)
493
+ const rPrChildren: XMLElement[] = [];
494
+
495
+ // Font
496
+ rPrChildren.push(
497
+ XMLBuilder.wSelf('rFonts', {
498
+ 'w:ascii': this.properties.font,
499
+ 'w:hAnsi': this.properties.font,
500
+ 'w:cs': this.properties.font,
501
+ 'w:hint': 'default',
502
+ })
503
+ );
504
+
505
+ // Bold
506
+ if (this.properties.bold) {
507
+ rPrChildren.push(XMLBuilder.wSelf('b'));
508
+ rPrChildren.push(XMLBuilder.wSelf('bCs'));
509
+ }
510
+
511
+ // Italic
512
+ if (this.properties.italic) {
513
+ rPrChildren.push(XMLBuilder.wSelf('i'));
514
+ rPrChildren.push(XMLBuilder.wSelf('iCs'));
515
+ }
516
+
517
+ // Underline
518
+ if (this.properties.underline) {
519
+ rPrChildren.push(XMLBuilder.wSelf('u', { 'w:val': this.properties.underline }));
520
+ }
521
+
522
+ // Color
523
+ if (this.properties.color) {
524
+ rPrChildren.push(XMLBuilder.wSelf('color', { 'w:val': this.properties.color }));
525
+ }
526
+
527
+ // Font size
528
+ rPrChildren.push(XMLBuilder.wSelf('sz', { 'w:val': this.properties.fontSize.toString() }));
529
+ rPrChildren.push(XMLBuilder.wSelf('szCs', { 'w:val': this.properties.fontSize.toString() }));
530
+
531
+ const rPr = XMLBuilder.w('rPr', undefined, rPrChildren);
532
+ children.push(rPr);
533
+
534
+ return XMLBuilder.w('lvl', { 'w:ilvl': this.properties.level.toString() }, children);
535
+ }
536
+
537
+ /**
538
+ * Gets the recommended bullet symbol and font for a given level
539
+ * @param level The level index (0-8)
540
+ * @param style Optional bullet style ('standard', 'circle', 'square', 'arrow', 'check', 'word-native')
541
+ * @returns Object with symbol and font properties
542
+ */
543
+ static getBulletSymbolWithFont(
544
+ level: number,
545
+ style: 'standard' | 'circle' | 'square' | 'arrow' | 'check' | 'word-native' = 'standard'
546
+ ): { symbol: string; font: string } {
547
+ const bulletSets = {
548
+ // Standard style now uses Word-native encoding for maximum compatibility
549
+ standard: [
550
+ {
551
+ symbol: WORD_NATIVE_BULLETS.FILLED_BULLET.char,
552
+ font: WORD_NATIVE_BULLETS.FILLED_BULLET.font,
553
+ }, // Level 0: Filled bullet (Symbol U+F0B7)
554
+ {
555
+ symbol: WORD_NATIVE_BULLETS.OPEN_CIRCLE.char,
556
+ font: WORD_NATIVE_BULLETS.OPEN_CIRCLE.font,
557
+ }, // Level 1: Open circle (Courier New U+006F)
558
+ {
559
+ symbol: WORD_NATIVE_BULLETS.FILLED_SQUARE.char,
560
+ font: WORD_NATIVE_BULLETS.FILLED_SQUARE.font,
561
+ }, // Level 2: Filled square (Wingdings U+F0A7)
562
+ {
563
+ symbol: WORD_NATIVE_BULLETS.FILLED_BULLET.char,
564
+ font: WORD_NATIVE_BULLETS.FILLED_BULLET.font,
565
+ }, // Level 3: Filled bullet
566
+ {
567
+ symbol: WORD_NATIVE_BULLETS.OPEN_CIRCLE.char,
568
+ font: WORD_NATIVE_BULLETS.OPEN_CIRCLE.font,
569
+ }, // Level 4: Open circle
570
+ {
571
+ symbol: WORD_NATIVE_BULLETS.FILLED_SQUARE.char,
572
+ font: WORD_NATIVE_BULLETS.FILLED_SQUARE.font,
573
+ }, // Level 5: Filled square
574
+ {
575
+ symbol: WORD_NATIVE_BULLETS.FILLED_BULLET.char,
576
+ font: WORD_NATIVE_BULLETS.FILLED_BULLET.font,
577
+ }, // Level 6: Filled bullet
578
+ {
579
+ symbol: WORD_NATIVE_BULLETS.OPEN_CIRCLE.char,
580
+ font: WORD_NATIVE_BULLETS.OPEN_CIRCLE.font,
581
+ }, // Level 7: Open circle
582
+ {
583
+ symbol: WORD_NATIVE_BULLETS.FILLED_SQUARE.char,
584
+ font: WORD_NATIVE_BULLETS.FILLED_SQUARE.font,
585
+ }, // Level 8: Filled square
586
+ ],
587
+ circle: [
588
+ { symbol: '●', font: 'Calibri' }, // Level 0: Filled circle (bold)
589
+ { symbol: '○', font: 'Calibri' }, // Level 1: Empty circle
590
+ { symbol: '◉', font: 'Calibri' }, // Level 2: Fisheye
591
+ { symbol: '◯', font: 'Calibri' }, // Level 3: Large circle
592
+ { symbol: '⦿', font: 'Calibri' }, // Level 4: Circled bullet
593
+ { symbol: '○', font: 'Calibri' }, // Level 5: Empty circle
594
+ { symbol: '●', font: 'Calibri' }, // Level 6: Filled circle
595
+ { symbol: '○', font: 'Calibri' }, // Level 7: Empty circle
596
+ { symbol: '◉', font: 'Calibri' }, // Level 8: Fisheye
597
+ ],
598
+ square: [
599
+ { symbol: '■', font: 'Calibri' }, // Level 0: Filled square
600
+ { symbol: '□', font: 'Calibri' }, // Level 1: Empty square
601
+ { symbol: '▪', font: 'Calibri' }, // Level 2: Small filled square
602
+ { symbol: '▫', font: 'Calibri' }, // Level 3: Small empty square
603
+ { symbol: '◼', font: 'Calibri' }, // Level 4: Medium filled square
604
+ { symbol: '◻', font: 'Calibri' }, // Level 5: Medium empty square
605
+ { symbol: '■', font: 'Calibri' }, // Level 6: Filled square
606
+ { symbol: '□', font: 'Calibri' }, // Level 7: Empty square
607
+ { symbol: '▪', font: 'Calibri' }, // Level 8: Small filled square
608
+ ],
609
+ arrow: [
610
+ { symbol: '➢', font: 'Calibri' }, // Level 0: Right arrow
611
+ { symbol: '➣', font: 'Calibri' }, // Level 1: Right arrow filled
612
+ { symbol: '➤', font: 'Calibri' }, // Level 2: Right arrow bold
613
+ { symbol: '➔', font: 'Calibri' }, // Level 3: Right arrow simple
614
+ { symbol: '➜', font: 'Calibri' }, // Level 4: Right arrow outline
615
+ { symbol: '➢', font: 'Calibri' }, // Level 5: Right arrow
616
+ { symbol: '➣', font: 'Calibri' }, // Level 6: Right arrow filled
617
+ { symbol: '➤', font: 'Calibri' }, // Level 7: Right arrow bold
618
+ { symbol: '➔', font: 'Calibri' }, // Level 8: Right arrow simple
619
+ ],
620
+ check: [
621
+ { symbol: '✓', font: 'Calibri' }, // Level 0: Check mark
622
+ { symbol: '✔', font: 'Calibri' }, // Level 1: Heavy check mark
623
+ { symbol: '☑', font: 'Calibri' }, // Level 2: Checked box
624
+ { symbol: '✓', font: 'Calibri' }, // Level 3: Check mark
625
+ { symbol: '✔', font: 'Calibri' }, // Level 4: Heavy check mark
626
+ { symbol: '☑', font: 'Calibri' }, // Level 5: Checked box
627
+ { symbol: '✓', font: 'Calibri' }, // Level 6: Check mark
628
+ { symbol: '✔', font: 'Calibri' }, // Level 7: Heavy check mark
629
+ { symbol: '☑', font: 'Calibri' }, // Level 8: Checked box
630
+ ],
631
+ 'word-native': [
632
+ // Word-native bullets using PUA characters with Symbol/Wingdings fonts
633
+ {
634
+ symbol: WORD_NATIVE_BULLETS.FILLED_BULLET.char,
635
+ font: WORD_NATIVE_BULLETS.FILLED_BULLET.font,
636
+ }, // Level 0: Filled bullet (Symbol U+F0B7)
637
+ {
638
+ symbol: WORD_NATIVE_BULLETS.OPEN_CIRCLE.char,
639
+ font: WORD_NATIVE_BULLETS.OPEN_CIRCLE.font,
640
+ }, // Level 1: Open circle (Courier New U+006F)
641
+ {
642
+ symbol: WORD_NATIVE_BULLETS.FILLED_SQUARE.char,
643
+ font: WORD_NATIVE_BULLETS.FILLED_SQUARE.font,
644
+ }, // Level 2: Filled square (Wingdings U+F0A7)
645
+ {
646
+ symbol: WORD_NATIVE_BULLETS.FILLED_BULLET.char,
647
+ font: WORD_NATIVE_BULLETS.FILLED_BULLET.font,
648
+ }, // Level 3: Filled bullet
649
+ {
650
+ symbol: WORD_NATIVE_BULLETS.OPEN_CIRCLE.char,
651
+ font: WORD_NATIVE_BULLETS.OPEN_CIRCLE.font,
652
+ }, // Level 4: Open circle
653
+ {
654
+ symbol: WORD_NATIVE_BULLETS.FILLED_SQUARE.char,
655
+ font: WORD_NATIVE_BULLETS.FILLED_SQUARE.font,
656
+ }, // Level 5: Filled square
657
+ {
658
+ symbol: WORD_NATIVE_BULLETS.FILLED_BULLET.char,
659
+ font: WORD_NATIVE_BULLETS.FILLED_BULLET.font,
660
+ }, // Level 6: Filled bullet
661
+ {
662
+ symbol: WORD_NATIVE_BULLETS.OPEN_CIRCLE.char,
663
+ font: WORD_NATIVE_BULLETS.OPEN_CIRCLE.font,
664
+ }, // Level 7: Open circle
665
+ {
666
+ symbol: WORD_NATIVE_BULLETS.FILLED_SQUARE.char,
667
+ font: WORD_NATIVE_BULLETS.FILLED_SQUARE.font,
668
+ }, // Level 8: Filled square
669
+ ],
670
+ };
671
+
672
+ const selectedSet = bulletSets[style];
673
+ const levelIndex = level % selectedSet.length;
674
+ const result = selectedSet[levelIndex];
675
+
676
+ // Fallback to standard bullet if somehow undefined
677
+ return result || { symbol: '•', font: 'Calibri' };
678
+ }
679
+
680
+ /**
681
+ * Calculates the standard indentation values for a given level
682
+ * @param level The level index (0-8)
683
+ * @returns Object with leftIndent and hangingIndent in twips
684
+ * @example
685
+ * const indent = NumberingLevel.calculateStandardIndentation(0);
686
+ * // Returns: { leftIndent: 720, hangingIndent: 360 } (0.5" left, 0.25" hanging)
687
+ *
688
+ * const indent2 = NumberingLevel.calculateStandardIndentation(2);
689
+ * // Returns: { leftIndent: 1440, hangingIndent: 360 } (1.0" left, 0.25" hanging)
690
+ */
691
+ static calculateStandardIndentation(level: number): {
692
+ leftIndent: number;
693
+ hangingIndent: number;
694
+ } {
695
+ if (level < 0 || level > 8) {
696
+ throw new Error(`Invalid level ${level}. Level must be between 0 and 8.`);
697
+ }
698
+
699
+ return {
700
+ leftIndent: 720 + level * 360,
701
+ hangingIndent: 360,
702
+ };
703
+ }
704
+
705
+ /**
706
+ * Gets the standard number format for a given level
707
+ * @param level The level index (0-8)
708
+ * @returns The recommended number format for this level
709
+ * @example
710
+ * NumberingLevel.getStandardNumberFormat(0); // 'decimal' (1., 2., 3.)
711
+ * NumberingLevel.getStandardNumberFormat(1); // 'lowerLetter' (a., b., c.)
712
+ * NumberingLevel.getStandardNumberFormat(2); // 'lowerRoman' (i., ii., iii.)
713
+ * NumberingLevel.getStandardNumberFormat(3); // 'upperLetter' (A., B., C.)
714
+ * NumberingLevel.getStandardNumberFormat(4); // 'upperRoman' (I., II., III.)
715
+ */
716
+ static getStandardNumberFormat(level: number): NumberFormat {
717
+ if (level < 0 || level > 8) {
718
+ throw new Error(`Invalid level ${level}. Level must be between 0 and 8.`);
719
+ }
720
+
721
+ const formats: NumberFormat[] = [
722
+ 'decimal', // Level 0: 1., 2., 3.
723
+ 'lowerLetter', // Level 1: a., b., c.
724
+ 'lowerRoman', // Level 2: i., ii., iii.
725
+ 'upperLetter', // Level 3: A., B., C.
726
+ 'upperRoman', // Level 4: I., II., III.
727
+ ];
728
+
729
+ const result = formats[level % formats.length];
730
+ return result || 'decimal'; // Fallback to decimal (should never happen)
731
+ }
732
+
733
+ /**
734
+ * Creates a bullet list level
735
+ *
736
+ * When called without parameters, uses Word-native encoding for the level:
737
+ * - Levels 0, 3, 6: Filled bullet (Symbol font, U+F0B7)
738
+ * - Levels 1, 4, 7: Open circle (Courier New, U+006F)
739
+ * - Levels 2, 5, 8: Filled square (Wingdings, U+F0A7)
740
+ *
741
+ * @param level The level index (0-8)
742
+ * @param bullet Optional bullet character (defaults to Word-native for level)
743
+ * @param font Optional font to use for the bullet (defaults to Word-native for level)
744
+ */
745
+ static createBulletLevel(level: number, bullet?: string, font?: string): NumberingLevel {
746
+ // Use Word-native defaults when not specified
747
+ const defaults = NumberingLevel.getBulletSymbolWithFont(level, 'standard');
748
+ const actualBullet = bullet ?? defaults.symbol;
749
+ const actualFont = font ?? defaults.font;
750
+
751
+ return new NumberingLevel({
752
+ level,
753
+ format: 'bullet',
754
+ text: actualBullet,
755
+ alignment: 'left',
756
+ font: actualFont,
757
+ fontSize: 24, // 12pt
758
+ bold: false,
759
+ color: '000000',
760
+ leftIndent: 720 + level * 360,
761
+ hangingIndent: 360,
762
+ });
763
+ }
764
+
765
+ /**
766
+ * Creates a decimal list level (1, 2, 3, ...)
767
+ * @param level The level index (0-8)
768
+ * @param template The text template (default: '%1.')
769
+ */
770
+ static createDecimalLevel(level: number, template = `%${level + 1}.`): NumberingLevel {
771
+ return new NumberingLevel({
772
+ level,
773
+ format: 'decimal',
774
+ text: template,
775
+ alignment: 'left',
776
+ font: 'Verdana',
777
+ fontSize: 24, // 12pt
778
+ bold: false,
779
+ color: '000000',
780
+ leftIndent: 720 + level * 360,
781
+ hangingIndent: 360,
782
+ });
783
+ }
784
+
785
+ /**
786
+ * Creates a lower roman list level (i, ii, iii, ...)
787
+ * @param level The level index (0-8)
788
+ * @param template The text template (default: '%1.')
789
+ */
790
+ static createLowerRomanLevel(level: number, template = `%${level + 1}.`): NumberingLevel {
791
+ return new NumberingLevel({
792
+ level,
793
+ format: 'lowerRoman',
794
+ text: template,
795
+ alignment: 'left',
796
+ font: 'Verdana',
797
+ fontSize: 24, // 12pt
798
+ bold: false,
799
+ color: '000000',
800
+ leftIndent: 720 + level * 360,
801
+ hangingIndent: 360,
802
+ });
803
+ }
804
+
805
+ /**
806
+ * Creates an upper roman list level (I, II, III, ...)
807
+ * @param level The level index (0-8)
808
+ * @param template The text template (default: '%1.')
809
+ */
810
+ static createUpperRomanLevel(level: number, template = `%${level + 1}.`): NumberingLevel {
811
+ return new NumberingLevel({
812
+ level,
813
+ format: 'upperRoman',
814
+ text: template,
815
+ alignment: 'left',
816
+ font: 'Verdana',
817
+ fontSize: 24, // 12pt
818
+ bold: false,
819
+ color: '000000',
820
+ leftIndent: 720 + level * 360,
821
+ hangingIndent: 360,
822
+ });
823
+ }
824
+
825
+ /**
826
+ * Creates a lower letter list level (a, b, c, ...)
827
+ * @param level The level index (0-8)
828
+ * @param template The text template (default: '%1.')
829
+ */
830
+ static createLowerLetterLevel(level: number, template = `%${level + 1}.`): NumberingLevel {
831
+ return new NumberingLevel({
832
+ level,
833
+ format: 'lowerLetter',
834
+ text: template,
835
+ alignment: 'left',
836
+ font: 'Verdana',
837
+ fontSize: 24, // 12pt
838
+ bold: false,
839
+ color: '000000',
840
+ leftIndent: 720 + level * 360,
841
+ hangingIndent: 360,
842
+ });
843
+ }
844
+
845
+ /**
846
+ * Creates an upper letter list level (A, B, C, ...)
847
+ * @param level The level index (0-8)
848
+ * @param template The text template (default: '%1.')
849
+ */
850
+ static createUpperLetterLevel(level: number, template = `%${level + 1}.`): NumberingLevel {
851
+ return new NumberingLevel({
852
+ level,
853
+ format: 'upperLetter',
854
+ text: template,
855
+ alignment: 'left',
856
+ font: 'Verdana',
857
+ fontSize: 24, // 12pt
858
+ bold: false,
859
+ color: '000000',
860
+ leftIndent: 720 + level * 360,
861
+ hangingIndent: 360,
862
+ });
863
+ }
864
+
865
+ /**
866
+ * Factory method for creating a numbering level
867
+ * @param properties The level properties
868
+ */
869
+ static create(properties: NumberingLevelProperties): NumberingLevel {
870
+ return new NumberingLevel(properties);
871
+ }
872
+
873
+ /**
874
+ * Creates a NumberingLevel from XML element
875
+ * @param xml The XML string of the <w:lvl> element
876
+ * @returns NumberingLevel instance
877
+ */
878
+ static fromXML(xml: string): NumberingLevel {
879
+ // Extract level index (required)
880
+ const ilvlMatch = /<w:lvl[^>]*w:ilvl="([^"]+)"/.exec(xml);
881
+ if (!ilvlMatch?.[1]) {
882
+ throw new Error('Missing required w:ilvl attribute');
883
+ }
884
+ const level = parseInt(ilvlMatch[1], 10);
885
+
886
+ // Extract number format (required)
887
+ const numFmtMatch = /<w:numFmt[^>]*w:val="([^"]+)"/.exec(xml);
888
+ if (!numFmtMatch?.[1]) {
889
+ throw new Error('Missing required w:numFmt element');
890
+ }
891
+ const format = NumberingLevel.normalizeFormat(numFmtMatch[1]);
892
+
893
+ // Extract level text (optional - can be empty for placeholder levels)
894
+ const lvlTextMatch = /<w:lvlText[^>]*w:val="([^"]*)"/.exec(xml);
895
+ const text = lvlTextMatch?.[1] !== undefined ? lvlTextMatch[1] : '';
896
+
897
+ // Extract alignment (optional, default: left)
898
+ const lvlJcMatch = /<w:lvlJc[^>]*w:val="([^"]+)"/.exec(xml);
899
+ const alignment = (lvlJcMatch?.[1] ? lvlJcMatch[1] : 'left') as NumberAlignment;
900
+
901
+ // Extract start value (optional, default: 1)
902
+ const startMatch = /<w:start[^>]*w:val="([^"]+)"/.exec(xml);
903
+ const start = startMatch?.[1] ? parseInt(startMatch[1], 10) : 1;
904
+
905
+ // Extract suffix (optional, default: tab)
906
+ const suffixMatch = /<w:suff[^>]*w:val="([^"]+)"/.exec(xml);
907
+ const suffix = suffixMatch?.[1] ? (suffixMatch[1] as 'tab' | 'space' | 'nothing') : 'tab';
908
+
909
+ // Extract level restart (w:lvlRestart per ECMA-376 Part 1 §17.9.11)
910
+ let lvlRestart: number | undefined;
911
+ const lvlRestartMatch = /<w:lvlRestart[^>]*w:val="([^"]+)"/.exec(xml);
912
+ if (lvlRestartMatch?.[1]) {
913
+ lvlRestart = parseInt(lvlRestartMatch[1], 10);
914
+ }
915
+
916
+ // Extract pStyle (paragraph style link)
917
+ let pStyle: string | undefined;
918
+ const pStyleMatch = /<w:pStyle[^>]*w:val="([^"]+)"/.exec(xml);
919
+ if (pStyleMatch?.[1]) {
920
+ pStyle = pStyleMatch[1];
921
+ }
922
+
923
+ // Extract indentation from <w:pPr><w:ind>
924
+ let leftIndent = 720 + level * 360; // default
925
+ let hangingIndent = 360; // default
926
+ const indMatch = /<w:ind[^>]*\/>/.exec(xml);
927
+ if (indMatch) {
928
+ const indElement = indMatch[0];
929
+ const leftMatch = /w:left="([^"]+)"/.exec(indElement);
930
+ const hangingMatch = /w:hanging="([^"]+)"/.exec(indElement);
931
+
932
+ if (leftMatch?.[1]) leftIndent = parseInt(leftMatch[1], 10);
933
+ if (hangingMatch?.[1]) hangingIndent = parseInt(hangingMatch[1], 10);
934
+ }
935
+
936
+ // Extract font and size from <w:rPr>
937
+ let font = 'Calibri';
938
+ let fontSize = 22;
939
+
940
+ const rFontsMatch = /<w:rFonts[^>]*\/>/.exec(xml);
941
+ if (rFontsMatch) {
942
+ const rFontsElement = rFontsMatch[0];
943
+ const asciiMatch = /w:ascii="([^"]+)"/.exec(rFontsElement);
944
+ if (asciiMatch?.[1]) font = asciiMatch[1];
945
+ }
946
+
947
+ const szMatch = /<w:sz[^>]*w:val="([^"]+)"/.exec(xml);
948
+ if (szMatch?.[1]) fontSize = parseInt(szMatch[1], 10);
949
+
950
+ // List prefixes should never have bold, italic, or underline - ignore source XML formatting
951
+ const bold = false;
952
+ const italic = false;
953
+ const underline: string | undefined = undefined;
954
+
955
+ // Extract color from <w:rPr>
956
+ let color: string | undefined;
957
+ const colorMatch = /<w:color[^>]*w:val="([^"]+)"/.exec(xml);
958
+ if (colorMatch?.[1]) {
959
+ color = colorMatch[1];
960
+ }
961
+
962
+ return new NumberingLevel({
963
+ level,
964
+ format,
965
+ text,
966
+ alignment,
967
+ start,
968
+ leftIndent,
969
+ hangingIndent,
970
+ font,
971
+ fontSize,
972
+ suffix,
973
+ bold,
974
+ italic,
975
+ underline,
976
+ color,
977
+ lvlRestart,
978
+ pStyle,
979
+ });
980
+ }
981
+ }