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,1130 +1,1090 @@
1
- /**
2
- * StylesManager - Manages the collection of styles in a document
3
- * Handles style registration, retrieval, and styles.xml generation
4
- */
5
-
6
- import { Paragraph } from "../elements/Paragraph";
7
- import { XMLBuilder } from "../xml/XMLBuilder";
8
- import { XMLParser } from "../xml/XMLParser";
9
- import { Style, StyleType } from "./Style";
10
-
11
- /**
12
- * Configuration for latent styles (w:latentStyles per ECMA-376 §17.7.4.6)
13
- * Controls which built-in styles are shown in the Word UI gallery
14
- */
15
- export interface LatentStylesConfig {
16
- /** Default locked state for built-in styles */
17
- defaultLockedState?: boolean;
18
- /** Default UI priority for built-in styles */
19
- defaultUiPriority?: number;
20
- /** Default semi-hidden state */
21
- defaultSemiHidden?: boolean;
22
- /** Default unhide-when-used state */
23
- defaultUnhideWhenUsed?: boolean;
24
- /** Default quick format flag */
25
- defaultQFormat?: boolean;
26
- /** Total count of style definitions */
27
- count?: number;
28
- }
29
-
30
- /**
31
- * Exception to latent style defaults (w:lsdException per ECMA-376 §17.7.4.7)
32
- */
33
- export interface LatentStyleException {
34
- /** Style name */
35
- name: string;
36
- /** Override: locked state */
37
- locked?: boolean;
38
- /** Override: UI priority */
39
- uiPriority?: number;
40
- /** Override: semi-hidden */
41
- semiHidden?: boolean;
42
- /** Override: unhide when used */
43
- unhideWhenUsed?: boolean;
44
- /** Override: quick format */
45
- qFormat?: boolean;
46
- }
47
-
48
- /**
49
- * Result of XML validation
50
- */
51
- export interface ValidationResult {
52
- /** Whether the XML is valid */
53
- isValid: boolean;
54
- /** Validation errors if any */
55
- errors: string[];
56
- /** Validation warnings if any */
57
- warnings: string[];
58
- /** Number of styles found */
59
- styleCount: number;
60
- /** List of style IDs found */
61
- styleIds: string[];
62
- }
63
-
64
- /**
65
- * Manages document styles
66
- */
67
- export class StylesManager {
68
- private styles = new Map<string, Style>();
69
- private includeBuiltInStyles: boolean;
70
-
71
- // Track if styles have been modified (for XML preservation)
72
- private _modified = false;
73
-
74
- // Track which specific styles have been modified (for selective merging)
75
- private _modifiedStyleIds = new Set<string>();
76
-
77
- // Latent styles configuration
78
- private latentStyles?: LatentStylesConfig;
79
- private latentStyleExceptions: LatentStyleException[] = [];
80
-
81
- /**
82
- * Registry of built-in style factory functions
83
- * Maps style ID to factory function for lazy loading
84
- */
85
- private static readonly BUILT_IN_STYLE_FACTORIES = new Map<
86
- string,
87
- () => Style
88
- >([
89
- ["Normal", () => Style.createNormalStyle()],
90
- ["Heading1", () => Style.createHeadingStyle(1)],
91
- ["Heading2", () => Style.createHeadingStyle(2)],
92
- ["Heading3", () => Style.createHeadingStyle(3)],
93
- ["Heading4", () => Style.createHeadingStyle(4)],
94
- ["Heading5", () => Style.createHeadingStyle(5)],
95
- ["Heading6", () => Style.createHeadingStyle(6)],
96
- ["Heading7", () => Style.createHeadingStyle(7)],
97
- ["Heading8", () => Style.createHeadingStyle(8)],
98
- ["Heading9", () => Style.createHeadingStyle(9)],
99
- ["Heading1Char", () => Style.createHeadingCharStyle(1)],
100
- ["Heading2Char", () => Style.createHeadingCharStyle(2)],
101
- ["Heading3Char", () => Style.createHeadingCharStyle(3)],
102
- ["Heading4Char", () => Style.createHeadingCharStyle(4)],
103
- ["Heading5Char", () => Style.createHeadingCharStyle(5)],
104
- ["Heading6Char", () => Style.createHeadingCharStyle(6)],
105
- ["Heading7Char", () => Style.createHeadingCharStyle(7)],
106
- ["Heading8Char", () => Style.createHeadingCharStyle(8)],
107
- ["Heading9Char", () => Style.createHeadingCharStyle(9)],
108
- ["Title", () => Style.createTitleStyle()],
109
- ["Subtitle", () => Style.createSubtitleStyle()],
110
- ["ListParagraph", () => Style.createListParagraphStyle()],
111
- ["TOCHeading", () => Style.createTOCHeadingStyle()],
112
- ["TableNormal", () => Style.createTableNormalStyle()],
113
- ["TableGrid", () => Style.createTableGridStyle()],
114
- ]);
115
-
116
- /**
117
- * Creates a new StylesManager
118
- * @param includeBuiltInStyles - Whether to include built-in styles (default: true)
119
- */
120
- constructor(includeBuiltInStyles = true) {
121
- this.includeBuiltInStyles = includeBuiltInStyles;
122
-
123
- // Always load Normal style if built-in styles are enabled
124
- // Normal is required and referenced by most other styles
125
- if (includeBuiltInStyles) {
126
- this.ensureStyleLoaded("Normal");
127
- }
128
- }
129
-
130
- /**
131
- * Ensures a built-in style is loaded (lazy loading)
132
- * @param styleId - Style ID to load
133
- */
134
- private ensureStyleLoaded(styleId: string): void {
135
- // Already loaded?
136
- if (this.styles.has(styleId)) {
137
- return;
138
- }
139
-
140
- // Built-in styles disabled?
141
- if (!this.includeBuiltInStyles) {
142
- return;
143
- }
144
-
145
- // Is this a built-in style?
146
- const factory = StylesManager.BUILT_IN_STYLE_FACTORIES.get(styleId);
147
- if (factory) {
148
- this.styles.set(styleId, factory());
149
- }
150
- }
151
-
152
- /**
153
- * Adds a style to the collection
154
- * @param style - Style to add
155
- * @returns This manager for chaining
156
- */
157
- addStyle(style: Style): this {
158
- const existing = this.styles.get(style.getStyleId());
159
- if (existing) {
160
- // Preserve isDefault flag if replacing an existing default style
161
- if (existing.getIsDefault() && !style.getIsDefault()) {
162
- style.setIsDefault(true);
163
- }
164
- // Preserve structural properties from existing style if not explicitly set on new
165
- const newProps = style.getProperties();
166
- const existingProps = existing.getProperties();
167
- if (newProps.basedOn === undefined && existingProps.basedOn !== undefined) {
168
- style.setBasedOn(existingProps.basedOn);
169
- }
170
- if (newProps.next === undefined && existingProps.next !== undefined) {
171
- style.setNext(existingProps.next);
172
- }
173
- if (newProps.link === undefined && existingProps.link !== undefined) {
174
- style.setLink(existingProps.link);
175
- }
176
- if (newProps.uiPriority === undefined && existingProps.uiPriority !== undefined) {
177
- style.setUiPriority(existingProps.uiPriority);
178
- }
179
- }
180
- this.styles.set(style.getStyleId(), style);
181
- this._modifiedStyleIds.add(style.getStyleId());
182
- this._modified = true;
183
- return this;
184
- }
185
-
186
- /**
187
- * Gets a style by ID
188
- * Lazy-loads built-in styles on first access
189
- * @param styleId - Style ID to retrieve
190
- * @returns The style, or undefined if not found
191
- */
192
- getStyle(styleId: string): Style | undefined {
193
- // Ensure built-in style is loaded if applicable
194
- this.ensureStyleLoaded(styleId);
195
- return this.styles.get(styleId);
196
- }
197
-
198
- /**
199
- * Checks if a style exists or can be loaded
200
- * @param styleId - Style ID to check
201
- * @returns True if the style exists or is a built-in style
202
- */
203
- hasStyle(styleId: string): boolean {
204
- // Check if already loaded
205
- if (this.styles.has(styleId)) {
206
- return true;
207
- }
208
-
209
- // Check if it's a built-in style that can be loaded
210
- return (
211
- this.includeBuiltInStyles &&
212
- StylesManager.BUILT_IN_STYLE_FACTORIES.has(styleId)
213
- );
214
- }
215
-
216
- /**
217
- * Removes a style from the collection
218
- * @param styleId - Style ID to remove
219
- * @returns True if the style was removed
220
- */
221
- removeStyle(styleId: string): boolean {
222
- return this.styles.delete(styleId);
223
- }
224
-
225
- /**
226
- * Gets all styles
227
- * @returns Array of all styles
228
- */
229
- getAllStyles(): Style[] {
230
- return Array.from(this.styles.values());
231
- }
232
-
233
- /**
234
- * Checks if styles have been modified since loading
235
- * Used for XML preservation optimization
236
- * @returns True if styles were added or modified
237
- */
238
- isModified(): boolean {
239
- return this._modified;
240
- }
241
-
242
- /**
243
- * Resets the modified flag
244
- * Called after parsing to indicate that loaded styles don't count as modifications
245
- */
246
- resetModified(): void {
247
- this._modified = false;
248
- this._modifiedStyleIds.clear();
249
- }
250
-
251
- /**
252
- * Gets the IDs of styles that have been modified since loading
253
- * Used for selective merging with original styles.xml
254
- * @returns Set of modified style IDs
255
- */
256
- getModifiedStyleIds(): Set<string> {
257
- return new Set(this._modifiedStyleIds);
258
- }
259
-
260
- /**
261
- * Gets styles by type
262
- * @param type - Style type to filter by
263
- * @returns Array of styles matching the type
264
- */
265
- getStylesByType(type: StyleType): Style[] {
266
- return this.getAllStyles().filter((style) => style.getType() === type);
267
- }
268
-
269
- /**
270
- * Gets quick styles (styles that appear in the style gallery)
271
- * A style appears in the gallery when qFormat=true AND semiHidden=false
272
- * @returns Array of quick styles
273
- */
274
- getQuickStyles(): Style[] {
275
- return this.getAllStyles().filter((style) => {
276
- const props = style.getProperties();
277
- const isQuick =
278
- props.qFormat === true ||
279
- (!props.customStyle && props.qFormat !== false);
280
- const isVisible = !props.semiHidden;
281
- return isQuick && isVisible;
282
- });
283
- }
284
-
285
- /**
286
- * Gets visible styles (not semi-hidden)
287
- * @returns Array of visible styles
288
- */
289
- getVisibleStyles(): Style[] {
290
- return this.getAllStyles().filter((style) => {
291
- const props = style.getProperties();
292
- return !props.semiHidden;
293
- });
294
- }
295
-
296
- /**
297
- * Gets styles sorted by UI priority
298
- * Lower priority values appear first (higher importance)
299
- * Styles without priority appear last
300
- * @returns Array of styles sorted by priority
301
- */
302
- getStylesByPriority(): Style[] {
303
- return this.getAllStyles().sort((a, b) => {
304
- const propsA = a.getProperties();
305
- const propsB = b.getProperties();
306
-
307
- const priorityA = propsA.uiPriority ?? 999;
308
- const priorityB = propsB.uiPriority ?? 999;
309
-
310
- return priorityA - priorityB;
311
- });
312
- }
313
-
314
- /**
315
- * Gets the linked style for a given style
316
- * @param styleId - Style ID to find the linked style for
317
- * @returns The linked style, or undefined if not found
318
- */
319
- getLinkedStyle(styleId: string): Style | undefined {
320
- const style = this.getStyle(styleId);
321
- if (!style) {
322
- return undefined;
323
- }
324
-
325
- const props = style.getProperties();
326
- if (!props.link) {
327
- return undefined;
328
- }
329
-
330
- return this.getStyle(props.link);
331
- }
332
-
333
- /**
334
- * Gets all table styles (Phase 5.1)
335
- * @returns Array of table styles
336
- */
337
- getTableStyles(): Style[] {
338
- return this.getAllStyles().filter((style) => style.getType() === "table");
339
- }
340
-
341
- /**
342
- * Creates and adds a table style (Phase 5.1)
343
- * @param styleId - Style ID
344
- * @param name - Style name
345
- * @param basedOn - Base style ID (optional)
346
- * @returns The created table style
347
- */
348
- createTableStyle(styleId: string, name: string, basedOn?: string): Style {
349
- const style = Style.create({
350
- styleId,
351
- name,
352
- type: "table",
353
- basedOn,
354
- customStyle: true,
355
- });
356
- this.addStyle(style);
357
- return style;
358
- }
359
-
360
- /**
361
- * Gets the number of styles
362
- * @returns Number of styles
363
- */
364
- getStyleCount(): number {
365
- return this.styles.size;
366
- }
367
-
368
- /**
369
- * Clears all styles
370
- * @returns This manager for chaining
371
- */
372
- clear(): this {
373
- this.styles.clear();
374
- return this;
375
- }
376
-
377
- /**
378
- * Gets all available built-in style IDs
379
- * @returns Array of built-in style IDs
380
- */
381
- static getBuiltInStyleIds(): string[] {
382
- return Array.from(StylesManager.BUILT_IN_STYLE_FACTORIES.keys());
383
- }
384
-
385
- /**
386
- * Checks if a style ID is a built-in style
387
- * @param styleId - Style ID to check
388
- * @returns True if the style is a built-in style
389
- */
390
- static isBuiltInStyle(styleId: string): boolean {
391
- return StylesManager.BUILT_IN_STYLE_FACTORIES.has(styleId);
392
- }
393
-
394
- /**
395
- * Gets statistics about loaded vs available styles
396
- * @returns Object with style statistics
397
- */
398
- getStats(): {
399
- loadedStyles: number;
400
- availableBuiltInStyles: number;
401
- customStyles: number;
402
- } {
403
- const loadedStyles = this.styles.size;
404
- const customStyles = this.getAllStyles().filter(
405
- (s) => s.getProperties().customStyle
406
- ).length;
407
-
408
- return {
409
- loadedStyles,
410
- availableBuiltInStyles: this.includeBuiltInStyles
411
- ? StylesManager.BUILT_IN_STYLE_FACTORIES.size
412
- : 0,
413
- customStyles,
414
- };
415
- }
416
-
417
- /**
418
- * Creates a new paragraph style
419
- * @param styleId - Unique style ID
420
- * @param name - Display name
421
- * @param basedOn - Parent style ID (optional)
422
- * @returns The created style
423
- */
424
- createParagraphStyle(styleId: string, name: string, basedOn?: string): Style {
425
- const style = Style.create({
426
- styleId,
427
- name,
428
- type: "paragraph",
429
- basedOn,
430
- customStyle: true,
431
- });
432
- this.addStyle(style);
433
- return style;
434
- }
435
-
436
- /**
437
- * Creates a new character style
438
- * @param styleId - Unique style ID
439
- * @param name - Display name
440
- * @param basedOn - Parent style ID (optional)
441
- * @returns The created style
442
- */
443
- createCharacterStyle(styleId: string, name: string, basedOn?: string): Style {
444
- const style = Style.create({
445
- styleId,
446
- name,
447
- type: "character",
448
- basedOn,
449
- customStyle: true,
450
- });
451
- this.addStyle(style);
452
- return style;
453
- }
454
-
455
- /**
456
- * Sets the latent styles configuration
457
- * @param config - Latent styles configuration
458
- */
459
- setLatentStyles(config: LatentStylesConfig): this {
460
- this.latentStyles = config;
461
- this._modified = true;
462
- return this;
463
- }
464
-
465
- /**
466
- * Gets the latent styles configuration
467
- */
468
- getLatentStyles(): LatentStylesConfig | undefined {
469
- return this.latentStyles;
470
- }
471
-
472
- /**
473
- * Adds a latent style exception
474
- * @param exception - The exception to add
475
- */
476
- addLatentStyleException(exception: LatentStyleException): this {
477
- // Replace existing exception for same name
478
- const idx = this.latentStyleExceptions.findIndex(e => e.name === exception.name);
479
- if (idx >= 0) {
480
- this.latentStyleExceptions[idx] = exception;
481
- } else {
482
- this.latentStyleExceptions.push(exception);
483
- }
484
- this._modified = true;
485
- return this;
486
- }
487
-
488
- /**
489
- * Gets all latent style exceptions
490
- */
491
- getLatentStyleExceptions(): LatentStyleException[] {
492
- return [...this.latentStyleExceptions];
493
- }
494
-
495
- /**
496
- * Generates the complete styles.xml file
497
- * @returns XML string for word/styles.xml
498
- */
499
- generateStylesXml(): string {
500
- const builder = new XMLBuilder();
501
-
502
- // Create styles element with namespace
503
- const stylesChildren = [];
504
-
505
- // Add document defaults
506
- stylesChildren.push(this.generateDocDefaults());
507
-
508
- // Add latent styles if configured (per ECMA-376 CT_Styles order: docDefaults, latentStyles, style*)
509
- if (this.latentStyles) {
510
- stylesChildren.push(this.generateLatentStyles());
511
- }
512
-
513
- // Add all styles
514
- for (const style of this.getAllStyles()) {
515
- stylesChildren.push(style.toXML());
516
- }
517
-
518
- builder.element(
519
- "w:styles",
520
- {
521
- "xmlns:w":
522
- "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
523
- "xmlns:r":
524
- "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
525
- },
526
- stylesChildren
527
- );
528
-
529
- return builder.build(true);
530
- }
531
-
532
- /**
533
- * Generates document defaults
534
- */
535
- private generateDocDefaults() {
536
- const rPrDefaultChildren = [
537
- XMLBuilder.wSelf("rFonts", {
538
- "w:ascii": "Calibri",
539
- "w:hAnsi": "Calibri",
540
- "w:eastAsia": "Calibri",
541
- "w:cs": "Calibri",
542
- }),
543
- XMLBuilder.wSelf("sz", { "w:val": "22" }), // 11pt
544
- XMLBuilder.wSelf("szCs", { "w:val": "22" }),
545
- XMLBuilder.wSelf("lang", {
546
- "w:val": "en-US",
547
- "w:eastAsia": "en-US",
548
- "w:bidi": "ar-SA",
549
- }),
550
- ];
551
-
552
- const pPrDefaultChildren = [
553
- XMLBuilder.wSelf("spacing", {
554
- "w:after": "200",
555
- "w:line": "276",
556
- "w:lineRule": "auto",
557
- }),
558
- ];
559
-
560
- return XMLBuilder.w("docDefaults", undefined, [
561
- XMLBuilder.w("rPrDefault", undefined, [
562
- XMLBuilder.w("rPr", undefined, rPrDefaultChildren),
563
- ]),
564
- XMLBuilder.w("pPrDefault", undefined, [
565
- XMLBuilder.w("pPr", undefined, pPrDefaultChildren),
566
- ]),
567
- ]);
568
- }
569
-
570
- /**
571
- * Generates the latent styles XML element
572
- */
573
- private generateLatentStyles() {
574
- if (!this.latentStyles) return XMLBuilder.w("latentStyles", {}, []);
575
-
576
- const attrs: Record<string, string> = {};
577
- if (this.latentStyles.defaultLockedState !== undefined) {
578
- attrs["w:defLockedState"] = this.latentStyles.defaultLockedState ? "1" : "0";
579
- }
580
- if (this.latentStyles.defaultUiPriority !== undefined) {
581
- attrs["w:defUIPriority"] = this.latentStyles.defaultUiPriority.toString();
582
- }
583
- if (this.latentStyles.defaultSemiHidden !== undefined) {
584
- attrs["w:defSemiHidden"] = this.latentStyles.defaultSemiHidden ? "1" : "0";
585
- }
586
- if (this.latentStyles.defaultUnhideWhenUsed !== undefined) {
587
- attrs["w:defUnhideWhenUsed"] = this.latentStyles.defaultUnhideWhenUsed ? "1" : "0";
588
- }
589
- if (this.latentStyles.defaultQFormat !== undefined) {
590
- attrs["w:defQFormat"] = this.latentStyles.defaultQFormat ? "1" : "0";
591
- }
592
- if (this.latentStyles.count !== undefined) {
593
- attrs["w:count"] = this.latentStyles.count.toString();
594
- }
595
-
596
- const children = this.latentStyleExceptions.map(exc => {
597
- const excAttrs: Record<string, string> = { "w:name": exc.name };
598
- if (exc.locked !== undefined) excAttrs["w:locked"] = exc.locked ? "1" : "0";
599
- if (exc.uiPriority !== undefined) excAttrs["w:uiPriority"] = exc.uiPriority.toString();
600
- if (exc.semiHidden !== undefined) excAttrs["w:semiHidden"] = exc.semiHidden ? "1" : "0";
601
- if (exc.unhideWhenUsed !== undefined) excAttrs["w:unhideWhenUsed"] = exc.unhideWhenUsed ? "1" : "0";
602
- if (exc.qFormat !== undefined) excAttrs["w:qFormat"] = exc.qFormat ? "1" : "0";
603
- return XMLBuilder.wSelf("lsdException", excAttrs);
604
- });
605
-
606
- return XMLBuilder.w("latentStyles", attrs, children);
607
- }
608
-
609
- /**
610
- * Creates a new StylesManager with built-in styles
611
- * @returns New StylesManager instance
612
- */
613
- static create(): StylesManager {
614
- return new StylesManager(true);
615
- }
616
-
617
- /**
618
- * Creates an empty StylesManager (no built-in styles)
619
- * @returns New empty StylesManager instance
620
- */
621
- static createEmpty(): StylesManager {
622
- return new StylesManager(false);
623
- }
624
-
625
- /**
626
- * Validates styles.xml content for structure and correctness
627
- *
628
- * This performs string-based validation to avoid XML parsing corruption.
629
- * It checks for:
630
- * - Well-formed XML structure
631
- * - Required w:styles root element
632
- * - Valid style definitions
633
- * - No duplicate style IDs
634
- * - Required attributes
635
- *
636
- * @param xml - The raw styles.xml content to validate
637
- * @returns ValidationResult with details about validity
638
- */
639
- static validate(xml: string): ValidationResult {
640
- const result: ValidationResult = {
641
- isValid: true,
642
- errors: [],
643
- warnings: [],
644
- styleCount: 0,
645
- styleIds: [],
646
- };
647
-
648
- // Check for empty or null
649
- if (!xml || xml.trim().length === 0) {
650
- result.isValid = false;
651
- result.errors.push("Styles XML is empty or null");
652
- return result;
653
- }
654
-
655
- // Check for common corruption patterns FIRST (before parsing)
656
- // This catches double-encoding issues that would break the parser
657
- if (xml.includes("&lt;w:") || xml.includes("&gt;")) {
658
- result.isValid = false;
659
- result.errors.push(
660
- "XML contains escaped tags - possible double-encoding corruption"
661
- );
662
- return result;
663
- }
664
-
665
- // Skip complex XML structure validation - focus on w:styles specific validation
666
- // Checking balanced tags with regex is unreliable and can give false positives
667
-
668
- // Use XMLParser to extract root element
669
- const stylesContent = XMLParser.extractBetweenTags(
670
- xml,
671
- "<w:styles",
672
- "</w:styles>"
673
- );
674
- if (!stylesContent) {
675
- result.isValid = false;
676
- result.errors.push("Missing required <w:styles> root element");
677
- return result;
678
- }
679
-
680
- // Check for namespace declaration
681
- if (!xml.includes("xmlns:w=")) {
682
- result.warnings.push("Missing WordprocessingML namespace declaration");
683
- }
684
-
685
- // Use XMLParser to extract all style elements
686
- const styleElements = XMLParser.extractElements(stylesContent, "w:style");
687
- result.styleCount = styleElements.length;
688
-
689
- // Check if any styles found
690
- if (styleElements.length === 0) {
691
- result.warnings.push("No styles found in document");
692
- return result;
693
- }
694
-
695
- // Check for styles without attributes (invalid)
696
- const styleWithoutAttrs = styleElements.filter((el) => {
697
- // Check if element has any attributes
698
- const openTagEnd = el.indexOf(">");
699
- const openTag = el.substring(0, openTagEnd);
700
- return !openTag.includes("w:type") || !openTag.includes("w:styleId");
701
- });
702
-
703
- if (styleWithoutAttrs.length > 0) {
704
- result.isValid = false;
705
- result.errors.push(
706
- "Style found without any attributes - w:type and w:styleId are required"
707
- );
708
- }
709
-
710
- // Process each style element
711
- const foundStyleIds = new Set<string>();
712
-
713
- for (const styleElement of styleElements) {
714
- // Extract styleId using XMLParser
715
- const styleId = XMLParser.extractAttribute(styleElement, "w:styleId");
716
- if (styleId) {
717
- // Check for duplicates
718
- if (foundStyleIds.has(styleId)) {
719
- result.isValid = false;
720
- result.errors.push(`Duplicate style ID found: "${styleId}"`);
721
- } else {
722
- foundStyleIds.add(styleId);
723
- result.styleIds.push(styleId);
724
- }
725
- } else {
726
- result.isValid = false;
727
- result.errors.push("Style found without required w:styleId attribute");
728
- }
729
-
730
- // Extract and validate type using XMLParser
731
- const type = XMLParser.extractAttribute(styleElement, "w:type");
732
- if (type) {
733
- if (!["paragraph", "character", "table", "numbering"].includes(type)) {
734
- result.warnings.push(`Invalid style type: "${type}"`);
735
- }
736
- } else {
737
- result.isValid = false;
738
- result.errors.push("Style found without required w:type attribute");
739
- }
740
-
741
- // Check for circular references - extract basedOn value
742
- const basedOnElement = XMLParser.extractElements(
743
- styleElement,
744
- "w:basedOn"
745
- )[0];
746
- if (basedOnElement && styleId) {
747
- const basedOn = XMLParser.extractAttribute(basedOnElement, "w:val");
748
- if (basedOn && styleId === basedOn) {
749
- result.isValid = false;
750
- result.errors.push(
751
- `Circular reference detected: style "${styleId}" based on itself`
752
- );
753
- }
754
- }
755
- }
756
-
757
- // Check for required Normal style
758
- if (!foundStyleIds.has("Normal")) {
759
- result.warnings.push(
760
- 'Missing "Normal" style - document may not render correctly'
761
- );
762
- }
763
-
764
- // Check for BOM or invalid characters
765
- if (xml.charCodeAt(0) === 0xfeff) {
766
- result.warnings.push(
767
- "XML contains BOM (Byte Order Mark) - may cause parsing issues"
768
- );
769
- }
770
-
771
- // Summary
772
- if (result.styleCount === 0) {
773
- result.warnings.push("No styles found in document");
774
- }
775
-
776
- return result;
777
- }
778
-
779
- /**
780
- * Searches styles by name (case-insensitive)
781
- * @param searchTerm - Text to search for in style names
782
- * @returns Array of styles whose names contain the search term
783
- * @example
784
- * ```typescript
785
- * const headings = stylesManager.searchByName('heading');
786
- * console.log(`Found ${headings.length} heading styles`);
787
- * ```
788
- */
789
- searchByName(searchTerm: string): Style[] {
790
- const term = searchTerm.toLowerCase();
791
- return this.getAllStyles().filter((style) =>
792
- style.getName().toLowerCase().includes(term)
793
- );
794
- }
795
-
796
- /**
797
- * Finds styles using a specific font
798
- * @param fontName - Font family name to search for
799
- * @returns Array of styles that use the specified font
800
- * @example
801
- * ```typescript
802
- * const arialStyles = stylesManager.findByFont('Arial');
803
- * console.log(`Found ${arialStyles.length} styles using Arial font`);
804
- * ```
805
- */
806
- findByFont(fontName: string): Style[] {
807
- return this.getAllStyles().filter((style) => {
808
- const runFormatting = style.getRunFormatting();
809
- return runFormatting?.font === fontName;
810
- });
811
- }
812
-
813
- /**
814
- * Finds styles with specific properties using a predicate function
815
- * @param predicate - Filter function that returns true for styles to include
816
- * @returns Array of styles matching the predicate
817
- * @example
818
- * ```typescript
819
- * // Find all paragraph styles
820
- * const paraStyles = stylesManager.findStyles(s => s.getType() === 'paragraph');
821
- *
822
- * // Find styles with custom formatting
823
- * const customStyles = stylesManager.findStyles(s => s.getProperties().customStyle);
824
- *
825
- * // Find styles with specific formatting
826
- * const boldStyles = stylesManager.findStyles(s => s.getRunFormatting()?.bold);
827
- * ```
828
- */
829
- findStyles(predicate: (style: Style) => boolean): Style[] {
830
- return this.getAllStyles().filter(predicate);
831
- }
832
-
833
- /**
834
- * Finds unused styles (not referenced by any paragraphs)
835
- * @param paragraphs - All paragraphs in the document to check against
836
- * @returns Array of unused style IDs
837
- * @example
838
- * ```typescript
839
- * const doc = Document.create();
840
- * const unused = stylesManager.findUnusedStyles(doc.getAllParagraphs());
841
- * console.log(`Found ${unused.length} unused styles: ${unused.join(', ')}`);
842
- * ```
843
- */
844
- findUnusedStyles(paragraphs: Paragraph[]): string[] {
845
- const usedStyles = new Set<string>();
846
-
847
- // Collect all style IDs used by paragraphs
848
- for (const para of paragraphs) {
849
- const styleId = para.getStyle();
850
- if (styleId) {
851
- usedStyles.add(styleId);
852
- }
853
- }
854
-
855
- // Get all style IDs and filter out used ones
856
- const allStyleIds = this.getAllStyles().map((style) => style.getStyleId());
857
- return allStyleIds.filter((styleId) => !usedStyles.has(styleId));
858
- }
859
-
860
- /**
861
- * Removes all unused styles from the document
862
- * @param paragraphs - All paragraphs in the document to check against
863
- * @returns Number of styles removed
864
- * @example
865
- * ```typescript
866
- * const doc = Document.create();
867
- * const removedCount = stylesManager.cleanupUnusedStyles(doc.getAllParagraphs());
868
- * console.log(`Cleaned up ${removedCount} unused styles`);
869
- * ```
870
- */
871
- cleanupUnusedStyles(paragraphs: Paragraph[]): number {
872
- const unused = this.findUnusedStyles(paragraphs);
873
- let count = 0;
874
-
875
- for (const styleId of unused) {
876
- // Don't remove built-in styles
877
- if (!StylesManager.isBuiltInStyle(styleId)) {
878
- if (this.removeStyle(styleId)) {
879
- count++;
880
- }
881
- }
882
- }
883
-
884
- return count;
885
- }
886
-
887
- /**
888
- * Validates all style references for broken or circular dependencies
889
- * @returns Validation result with broken references and circular dependencies
890
- * @example
891
- * ```typescript
892
- * const validation = stylesManager.validateStyleReferences();
893
- * if (!validation.valid) {
894
- * console.log('Broken references:', validation.brokenReferences);
895
- * console.log('Circular references:', validation.circularReferences);
896
- * }
897
- * ```
898
- */
899
- validateStyleReferences(): {
900
- valid: boolean;
901
- brokenReferences: { styleId: string; basedOn: string }[];
902
- circularReferences: string[][];
903
- } {
904
- const broken: { styleId: string; basedOn: string }[] = [];
905
- const circular: string[][] = [];
906
- const checkedForCycles = new Set<string>();
907
-
908
- for (const style of this.getAllStyles()) {
909
- const props = style.getProperties();
910
-
911
- // Check basedOn references exist
912
- if (props.basedOn && !this.hasStyle(props.basedOn)) {
913
- broken.push({ styleId: props.styleId, basedOn: props.basedOn });
914
- }
915
-
916
- // Check for circular references (only if not already checked as part of another cycle)
917
- if (!checkedForCycles.has(props.styleId)) {
918
- const result = this.hasCircularReference(props.styleId);
919
- if (result.hasCircularRef && result.cyclePath) {
920
- circular.push(result.cyclePath);
921
- // Mark all styles in this cycle as checked
922
- result.cyclePath.forEach((id) => checkedForCycles.add(id));
923
- }
924
- }
925
- }
926
-
927
- return {
928
- valid: broken.length === 0 && circular.length === 0,
929
- brokenReferences: broken,
930
- circularReferences: circular,
931
- };
932
- }
933
-
934
- /**
935
- * Gets the complete inheritance chain for a style
936
- * @param styleId - Style ID to analyze
937
- * @returns Array of styles from base to derived (base style first)
938
- * @throws Error if style doesn't exist or circular reference detected
939
- * @example
940
- * ```typescript
941
- * const chain = stylesManager.getInheritanceChain('Heading1');
942
- * console.log('Inheritance chain:', chain.map(s => s.getName()));
943
- * // Output: ['Normal', 'Heading1'] (Normal is base, Heading1 inherits from it)
944
- * ```
945
- */
946
- getInheritanceChain(styleId: string): Style[] {
947
- const chain: Style[] = [];
948
- const visited = new Set<string>(); // Track visited styles to detect cycles
949
- let current = this.getStyle(styleId);
950
-
951
- if (!current) {
952
- throw new Error(`Style '${styleId}' not found`);
953
- }
954
-
955
- while (current) {
956
- const currentId = current.getStyleId();
957
-
958
- // Detect circular reference
959
- if (visited.has(currentId)) {
960
- const cycle = [...chain.map((s) => s.getStyleId()), currentId];
961
- throw new Error(
962
- `Circular style reference detected: ${cycle.join(" -> ")}. ` +
963
- `Style '${currentId}' references itself through inheritance chain.`
964
- );
965
- }
966
-
967
- visited.add(currentId);
968
- chain.unshift(current); // Add to beginning to maintain base-to-derived order
969
- const props = current.getProperties();
970
- current = props.basedOn ? this.getStyle(props.basedOn) : undefined;
971
- }
972
-
973
- return chain;
974
- }
975
-
976
- /**
977
- * Checks if a style has circular references in its inheritance chain
978
- * @param styleId - Style ID to check
979
- * @returns Object with hasCircularRef flag and the cycle path if found
980
- * @example
981
- * ```typescript
982
- * const result = stylesManager.hasCircularReference('MyStyle');
983
- * if (result.hasCircularRef) {
984
- * console.log('Circular reference found:', result.cyclePath?.join(' -> '));
985
- * }
986
- * ```
987
- */
988
- hasCircularReference(styleId: string): {
989
- hasCircularRef: boolean;
990
- cyclePath?: string[];
991
- } {
992
- const visited = new Set<string>();
993
- const path: string[] = [];
994
- let current = this.getStyle(styleId);
995
-
996
- while (current) {
997
- const currentId = current.getStyleId();
998
-
999
- if (visited.has(currentId)) {
1000
- path.push(currentId);
1001
- return { hasCircularRef: true, cyclePath: path };
1002
- }
1003
-
1004
- visited.add(currentId);
1005
- path.push(currentId);
1006
- const props = current.getProperties();
1007
- current = props.basedOn ? this.getStyle(props.basedOn) : undefined;
1008
- }
1009
-
1010
- return { hasCircularRef: false };
1011
- }
1012
-
1013
- /**
1014
- * Gets all styles that inherit from a base style
1015
- * @param baseStyleId - Base style ID to find children for
1016
- * @returns Array of styles that inherit from the base style
1017
- * @example
1018
- * ```typescript
1019
- * const children = stylesManager.getDerivedStyles('Normal');
1020
- * console.log(`Styles based on Normal: ${children.map(s => s.getName()).join(', ')}`);
1021
- * ```
1022
- */
1023
- getDerivedStyles(baseStyleId: string): Style[] {
1024
- return this.getAllStyles().filter(
1025
- (style) => style.getProperties().basedOn === baseStyleId
1026
- );
1027
- }
1028
-
1029
- /**
1030
- * Exports a single style as JSON string
1031
- * @param styleId - Style ID to export
1032
- * @returns JSON representation of the style
1033
- * @throws Error if style doesn't exist
1034
- * @example
1035
- * ```typescript
1036
- * const json = stylesManager.exportStyle('Heading1');
1037
- * console.log('Style JSON:', json);
1038
- *
1039
- * // Save to file
1040
- * await fs.writeFile('heading1-style.json', json);
1041
- * ```
1042
- */
1043
- exportStyle(styleId: string): string {
1044
- const style = this.getStyle(styleId);
1045
- if (!style) {
1046
- throw new Error(`Style '${styleId}' not found`);
1047
- }
1048
- return JSON.stringify(style.getProperties(), null, 2);
1049
- }
1050
-
1051
- /**
1052
- * Imports a style from JSON string
1053
- * @param json - JSON string containing style properties
1054
- * @returns The imported style
1055
- * @throws Error if JSON is invalid or style creation fails
1056
- * @example
1057
- * ```typescript
1058
- * const json = await fs.readFile('custom-style.json', 'utf8');
1059
- * const style = stylesManager.importStyle(json);
1060
- * console.log(`Imported style: ${style.getName()}`);
1061
- * ```
1062
- */
1063
- importStyle(json: string): Style {
1064
- try {
1065
- const props = JSON.parse(json);
1066
- const style = Style.create(props);
1067
- this.addStyle(style);
1068
- return style;
1069
- } catch (error: unknown) {
1070
- throw new Error(
1071
- `Failed to import style: ${
1072
- error instanceof Error ? error.message : "Invalid JSON"
1073
- }`
1074
- );
1075
- }
1076
- }
1077
-
1078
- /**
1079
- * Exports all styles as JSON string
1080
- * @returns JSON array of all style properties
1081
- * @example
1082
- * ```typescript
1083
- * const allStylesJson = stylesManager.exportAllStyles();
1084
- * console.log('All styles exported');
1085
- *
1086
- * // Save to file
1087
- * await fs.writeFile('all-styles.json', allStylesJson);
1088
- * ```
1089
- */
1090
- exportAllStyles(): string {
1091
- const styles = this.getAllStyles().map((style) => style.getProperties());
1092
- return JSON.stringify(styles, null, 2);
1093
- }
1094
-
1095
- /**
1096
- * Imports multiple styles from JSON array string
1097
- * @param json - JSON string containing array of style properties
1098
- * @returns Array of imported styles
1099
- * @throws Error if JSON is invalid or style creation fails
1100
- * @example
1101
- * ```typescript
1102
- * const json = await fs.readFile('styles-collection.json', 'utf8');
1103
- * const styles = stylesManager.importStyles(json);
1104
- * console.log(`Imported ${styles.length} styles`);
1105
- * ```
1106
- */
1107
- importStyles(json: string): Style[] {
1108
- try {
1109
- const propsArray = JSON.parse(json);
1110
- if (!Array.isArray(propsArray)) {
1111
- throw new Error("JSON must contain an array of style properties");
1112
- }
1113
-
1114
- const styles: Style[] = [];
1115
- for (const props of propsArray) {
1116
- const style = Style.create(props);
1117
- this.addStyle(style);
1118
- styles.push(style);
1119
- }
1120
-
1121
- return styles;
1122
- } catch (error: unknown) {
1123
- throw new Error(
1124
- `Failed to import styles: ${
1125
- error instanceof Error ? error.message : "Invalid JSON"
1126
- }`
1127
- );
1128
- }
1129
- }
1130
- }
1
+ /**
2
+ * StylesManager - Manages the collection of styles in a document
3
+ * Handles style registration, retrieval, and styles.xml generation
4
+ */
5
+
6
+ import { Paragraph } from '../elements/Paragraph';
7
+ import { XMLBuilder } from '../xml/XMLBuilder';
8
+ import { XMLParser } from '../xml/XMLParser';
9
+ import { Style, StyleType } from './Style';
10
+
11
+ /**
12
+ * Configuration for latent styles (w:latentStyles per ECMA-376 §17.7.4.6)
13
+ * Controls which built-in styles are shown in the Word UI gallery
14
+ */
15
+ export interface LatentStylesConfig {
16
+ /** Default locked state for built-in styles */
17
+ defaultLockedState?: boolean;
18
+ /** Default UI priority for built-in styles */
19
+ defaultUiPriority?: number;
20
+ /** Default semi-hidden state */
21
+ defaultSemiHidden?: boolean;
22
+ /** Default unhide-when-used state */
23
+ defaultUnhideWhenUsed?: boolean;
24
+ /** Default quick format flag */
25
+ defaultQFormat?: boolean;
26
+ /** Total count of style definitions */
27
+ count?: number;
28
+ }
29
+
30
+ /**
31
+ * Exception to latent style defaults (w:lsdException per ECMA-376 §17.7.4.7)
32
+ */
33
+ export interface LatentStyleException {
34
+ /** Style name */
35
+ name: string;
36
+ /** Override: locked state */
37
+ locked?: boolean;
38
+ /** Override: UI priority */
39
+ uiPriority?: number;
40
+ /** Override: semi-hidden */
41
+ semiHidden?: boolean;
42
+ /** Override: unhide when used */
43
+ unhideWhenUsed?: boolean;
44
+ /** Override: quick format */
45
+ qFormat?: boolean;
46
+ }
47
+
48
+ /**
49
+ * Result of XML validation
50
+ */
51
+ export interface ValidationResult {
52
+ /** Whether the XML is valid */
53
+ isValid: boolean;
54
+ /** Validation errors if any */
55
+ errors: string[];
56
+ /** Validation warnings if any */
57
+ warnings: string[];
58
+ /** Number of styles found */
59
+ styleCount: number;
60
+ /** List of style IDs found */
61
+ styleIds: string[];
62
+ }
63
+
64
+ /**
65
+ * Manages document styles
66
+ */
67
+ export class StylesManager {
68
+ private styles = new Map<string, Style>();
69
+ private includeBuiltInStyles: boolean;
70
+
71
+ // Track if styles have been modified (for XML preservation)
72
+ private _modified = false;
73
+
74
+ // Track which specific styles have been modified (for selective merging)
75
+ private _modifiedStyleIds = new Set<string>();
76
+
77
+ // Latent styles configuration
78
+ private latentStyles?: LatentStylesConfig;
79
+ private latentStyleExceptions: LatentStyleException[] = [];
80
+
81
+ /**
82
+ * Registry of built-in style factory functions
83
+ * Maps style ID to factory function for lazy loading
84
+ */
85
+ private static readonly BUILT_IN_STYLE_FACTORIES = new Map<string, () => Style>([
86
+ ['Normal', () => Style.createNormalStyle()],
87
+ ['Heading1', () => Style.createHeadingStyle(1)],
88
+ ['Heading2', () => Style.createHeadingStyle(2)],
89
+ ['Heading3', () => Style.createHeadingStyle(3)],
90
+ ['Heading4', () => Style.createHeadingStyle(4)],
91
+ ['Heading5', () => Style.createHeadingStyle(5)],
92
+ ['Heading6', () => Style.createHeadingStyle(6)],
93
+ ['Heading7', () => Style.createHeadingStyle(7)],
94
+ ['Heading8', () => Style.createHeadingStyle(8)],
95
+ ['Heading9', () => Style.createHeadingStyle(9)],
96
+ ['Heading1Char', () => Style.createHeadingCharStyle(1)],
97
+ ['Heading2Char', () => Style.createHeadingCharStyle(2)],
98
+ ['Heading3Char', () => Style.createHeadingCharStyle(3)],
99
+ ['Heading4Char', () => Style.createHeadingCharStyle(4)],
100
+ ['Heading5Char', () => Style.createHeadingCharStyle(5)],
101
+ ['Heading6Char', () => Style.createHeadingCharStyle(6)],
102
+ ['Heading7Char', () => Style.createHeadingCharStyle(7)],
103
+ ['Heading8Char', () => Style.createHeadingCharStyle(8)],
104
+ ['Heading9Char', () => Style.createHeadingCharStyle(9)],
105
+ ['Title', () => Style.createTitleStyle()],
106
+ ['Subtitle', () => Style.createSubtitleStyle()],
107
+ ['ListParagraph', () => Style.createListParagraphStyle()],
108
+ ['TOCHeading', () => Style.createTOCHeadingStyle()],
109
+ ['TableNormal', () => Style.createTableNormalStyle()],
110
+ ['TableGrid', () => Style.createTableGridStyle()],
111
+ ]);
112
+
113
+ /**
114
+ * Creates a new StylesManager
115
+ * @param includeBuiltInStyles - Whether to include built-in styles (default: true)
116
+ */
117
+ constructor(includeBuiltInStyles = true) {
118
+ this.includeBuiltInStyles = includeBuiltInStyles;
119
+
120
+ // Always load Normal style if built-in styles are enabled
121
+ // Normal is required and referenced by most other styles
122
+ if (includeBuiltInStyles) {
123
+ this.ensureStyleLoaded('Normal');
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Ensures a built-in style is loaded (lazy loading)
129
+ * @param styleId - Style ID to load
130
+ */
131
+ private ensureStyleLoaded(styleId: string): void {
132
+ // Already loaded?
133
+ if (this.styles.has(styleId)) {
134
+ return;
135
+ }
136
+
137
+ // Built-in styles disabled?
138
+ if (!this.includeBuiltInStyles) {
139
+ return;
140
+ }
141
+
142
+ // Is this a built-in style?
143
+ const factory = StylesManager.BUILT_IN_STYLE_FACTORIES.get(styleId);
144
+ if (factory) {
145
+ this.styles.set(styleId, factory());
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Adds a style to the collection
151
+ * @param style - Style to add
152
+ * @returns This manager for chaining
153
+ */
154
+ addStyle(style: Style): this {
155
+ const existing = this.styles.get(style.getStyleId());
156
+ if (existing) {
157
+ // Preserve isDefault flag if replacing an existing default style
158
+ if (existing.getIsDefault() && !style.getIsDefault()) {
159
+ style.setIsDefault(true);
160
+ }
161
+ // Preserve structural properties from existing style if not explicitly set on new
162
+ const newProps = style.getProperties();
163
+ const existingProps = existing.getProperties();
164
+ if (newProps.basedOn === undefined && existingProps.basedOn !== undefined) {
165
+ style.setBasedOn(existingProps.basedOn);
166
+ }
167
+ if (newProps.next === undefined && existingProps.next !== undefined) {
168
+ style.setNext(existingProps.next);
169
+ }
170
+ if (newProps.link === undefined && existingProps.link !== undefined) {
171
+ style.setLink(existingProps.link);
172
+ }
173
+ if (newProps.uiPriority === undefined && existingProps.uiPriority !== undefined) {
174
+ style.setUiPriority(existingProps.uiPriority);
175
+ }
176
+ }
177
+ this.styles.set(style.getStyleId(), style);
178
+ this._modifiedStyleIds.add(style.getStyleId());
179
+ this._modified = true;
180
+ return this;
181
+ }
182
+
183
+ /**
184
+ * Gets a style by ID
185
+ * Lazy-loads built-in styles on first access
186
+ * @param styleId - Style ID to retrieve
187
+ * @returns The style, or undefined if not found
188
+ */
189
+ getStyle(styleId: string): Style | undefined {
190
+ // Ensure built-in style is loaded if applicable
191
+ this.ensureStyleLoaded(styleId);
192
+ return this.styles.get(styleId);
193
+ }
194
+
195
+ /**
196
+ * Checks if a style exists or can be loaded
197
+ * @param styleId - Style ID to check
198
+ * @returns True if the style exists or is a built-in style
199
+ */
200
+ hasStyle(styleId: string): boolean {
201
+ // Check if already loaded
202
+ if (this.styles.has(styleId)) {
203
+ return true;
204
+ }
205
+
206
+ // Check if it's a built-in style that can be loaded
207
+ return this.includeBuiltInStyles && StylesManager.BUILT_IN_STYLE_FACTORIES.has(styleId);
208
+ }
209
+
210
+ /**
211
+ * Removes a style from the collection
212
+ * @param styleId - Style ID to remove
213
+ * @returns True if the style was removed
214
+ */
215
+ removeStyle(styleId: string): boolean {
216
+ return this.styles.delete(styleId);
217
+ }
218
+
219
+ /**
220
+ * Gets all styles
221
+ * @returns Array of all styles
222
+ */
223
+ getAllStyles(): Style[] {
224
+ return Array.from(this.styles.values());
225
+ }
226
+
227
+ /**
228
+ * Checks if styles have been modified since loading
229
+ * Used for XML preservation optimization
230
+ * @returns True if styles were added or modified
231
+ */
232
+ isModified(): boolean {
233
+ return this._modified;
234
+ }
235
+
236
+ /**
237
+ * Resets the modified flag
238
+ * Called after parsing to indicate that loaded styles don't count as modifications
239
+ */
240
+ resetModified(): void {
241
+ this._modified = false;
242
+ this._modifiedStyleIds.clear();
243
+ }
244
+
245
+ /**
246
+ * Gets the IDs of styles that have been modified since loading
247
+ * Used for selective merging with original styles.xml
248
+ * @returns Set of modified style IDs
249
+ */
250
+ getModifiedStyleIds(): Set<string> {
251
+ return new Set(this._modifiedStyleIds);
252
+ }
253
+
254
+ /**
255
+ * Gets styles by type
256
+ * @param type - Style type to filter by
257
+ * @returns Array of styles matching the type
258
+ */
259
+ getStylesByType(type: StyleType): Style[] {
260
+ return this.getAllStyles().filter((style) => style.getType() === type);
261
+ }
262
+
263
+ /**
264
+ * Gets quick styles (styles that appear in the style gallery)
265
+ * A style appears in the gallery when qFormat=true AND semiHidden=false
266
+ * @returns Array of quick styles
267
+ */
268
+ getQuickStyles(): Style[] {
269
+ return this.getAllStyles().filter((style) => {
270
+ const props = style.getProperties();
271
+ const isQuick = props.qFormat === true || (!props.customStyle && props.qFormat !== false);
272
+ const isVisible = !props.semiHidden;
273
+ return isQuick && isVisible;
274
+ });
275
+ }
276
+
277
+ /**
278
+ * Gets visible styles (not semi-hidden)
279
+ * @returns Array of visible styles
280
+ */
281
+ getVisibleStyles(): Style[] {
282
+ return this.getAllStyles().filter((style) => {
283
+ const props = style.getProperties();
284
+ return !props.semiHidden;
285
+ });
286
+ }
287
+
288
+ /**
289
+ * Gets styles sorted by UI priority
290
+ * Lower priority values appear first (higher importance)
291
+ * Styles without priority appear last
292
+ * @returns Array of styles sorted by priority
293
+ */
294
+ getStylesByPriority(): Style[] {
295
+ return this.getAllStyles().sort((a, b) => {
296
+ const propsA = a.getProperties();
297
+ const propsB = b.getProperties();
298
+
299
+ const priorityA = propsA.uiPriority ?? 999;
300
+ const priorityB = propsB.uiPriority ?? 999;
301
+
302
+ return priorityA - priorityB;
303
+ });
304
+ }
305
+
306
+ /**
307
+ * Gets the linked style for a given style
308
+ * @param styleId - Style ID to find the linked style for
309
+ * @returns The linked style, or undefined if not found
310
+ */
311
+ getLinkedStyle(styleId: string): Style | undefined {
312
+ const style = this.getStyle(styleId);
313
+ if (!style) {
314
+ return undefined;
315
+ }
316
+
317
+ const props = style.getProperties();
318
+ if (!props.link) {
319
+ return undefined;
320
+ }
321
+
322
+ return this.getStyle(props.link);
323
+ }
324
+
325
+ /**
326
+ * Gets all table styles (Phase 5.1)
327
+ * @returns Array of table styles
328
+ */
329
+ getTableStyles(): Style[] {
330
+ return this.getAllStyles().filter((style) => style.getType() === 'table');
331
+ }
332
+
333
+ /**
334
+ * Creates and adds a table style (Phase 5.1)
335
+ * @param styleId - Style ID
336
+ * @param name - Style name
337
+ * @param basedOn - Base style ID (optional)
338
+ * @returns The created table style
339
+ */
340
+ createTableStyle(styleId: string, name: string, basedOn?: string): Style {
341
+ const style = Style.create({
342
+ styleId,
343
+ name,
344
+ type: 'table',
345
+ basedOn,
346
+ customStyle: true,
347
+ });
348
+ this.addStyle(style);
349
+ return style;
350
+ }
351
+
352
+ /**
353
+ * Gets the number of styles
354
+ * @returns Number of styles
355
+ */
356
+ getStyleCount(): number {
357
+ return this.styles.size;
358
+ }
359
+
360
+ /**
361
+ * Clears all styles
362
+ * @returns This manager for chaining
363
+ */
364
+ clear(): this {
365
+ this.styles.clear();
366
+ return this;
367
+ }
368
+
369
+ /**
370
+ * Gets all available built-in style IDs
371
+ * @returns Array of built-in style IDs
372
+ */
373
+ static getBuiltInStyleIds(): string[] {
374
+ return Array.from(StylesManager.BUILT_IN_STYLE_FACTORIES.keys());
375
+ }
376
+
377
+ /**
378
+ * Checks if a style ID is a built-in style
379
+ * @param styleId - Style ID to check
380
+ * @returns True if the style is a built-in style
381
+ */
382
+ static isBuiltInStyle(styleId: string): boolean {
383
+ return StylesManager.BUILT_IN_STYLE_FACTORIES.has(styleId);
384
+ }
385
+
386
+ /**
387
+ * Gets statistics about loaded vs available styles
388
+ * @returns Object with style statistics
389
+ */
390
+ getStats(): {
391
+ loadedStyles: number;
392
+ availableBuiltInStyles: number;
393
+ customStyles: number;
394
+ } {
395
+ const loadedStyles = this.styles.size;
396
+ const customStyles = this.getAllStyles().filter((s) => s.getProperties().customStyle).length;
397
+
398
+ return {
399
+ loadedStyles,
400
+ availableBuiltInStyles: this.includeBuiltInStyles
401
+ ? StylesManager.BUILT_IN_STYLE_FACTORIES.size
402
+ : 0,
403
+ customStyles,
404
+ };
405
+ }
406
+
407
+ /**
408
+ * Creates a new paragraph style
409
+ * @param styleId - Unique style ID
410
+ * @param name - Display name
411
+ * @param basedOn - Parent style ID (optional)
412
+ * @returns The created style
413
+ */
414
+ createParagraphStyle(styleId: string, name: string, basedOn?: string): Style {
415
+ const style = Style.create({
416
+ styleId,
417
+ name,
418
+ type: 'paragraph',
419
+ basedOn,
420
+ customStyle: true,
421
+ });
422
+ this.addStyle(style);
423
+ return style;
424
+ }
425
+
426
+ /**
427
+ * Creates a new character style
428
+ * @param styleId - Unique style ID
429
+ * @param name - Display name
430
+ * @param basedOn - Parent style ID (optional)
431
+ * @returns The created style
432
+ */
433
+ createCharacterStyle(styleId: string, name: string, basedOn?: string): Style {
434
+ const style = Style.create({
435
+ styleId,
436
+ name,
437
+ type: 'character',
438
+ basedOn,
439
+ customStyle: true,
440
+ });
441
+ this.addStyle(style);
442
+ return style;
443
+ }
444
+
445
+ /**
446
+ * Sets the latent styles configuration
447
+ * @param config - Latent styles configuration
448
+ */
449
+ setLatentStyles(config: LatentStylesConfig): this {
450
+ this.latentStyles = config;
451
+ this._modified = true;
452
+ return this;
453
+ }
454
+
455
+ /**
456
+ * Gets the latent styles configuration
457
+ */
458
+ getLatentStyles(): LatentStylesConfig | undefined {
459
+ return this.latentStyles;
460
+ }
461
+
462
+ /**
463
+ * Adds a latent style exception
464
+ * @param exception - The exception to add
465
+ */
466
+ addLatentStyleException(exception: LatentStyleException): this {
467
+ // Replace existing exception for same name
468
+ const idx = this.latentStyleExceptions.findIndex((e) => e.name === exception.name);
469
+ if (idx >= 0) {
470
+ this.latentStyleExceptions[idx] = exception;
471
+ } else {
472
+ this.latentStyleExceptions.push(exception);
473
+ }
474
+ this._modified = true;
475
+ return this;
476
+ }
477
+
478
+ /**
479
+ * Gets all latent style exceptions
480
+ */
481
+ getLatentStyleExceptions(): LatentStyleException[] {
482
+ return [...this.latentStyleExceptions];
483
+ }
484
+
485
+ /**
486
+ * Generates the complete styles.xml file
487
+ * @returns XML string for word/styles.xml
488
+ */
489
+ generateStylesXml(): string {
490
+ const builder = new XMLBuilder();
491
+
492
+ // Create styles element with namespace
493
+ const stylesChildren = [];
494
+
495
+ // Add document defaults
496
+ stylesChildren.push(this.generateDocDefaults());
497
+
498
+ // Add latent styles if configured (per ECMA-376 CT_Styles order: docDefaults, latentStyles, style*)
499
+ if (this.latentStyles) {
500
+ stylesChildren.push(this.generateLatentStyles());
501
+ }
502
+
503
+ // Add all styles
504
+ for (const style of this.getAllStyles()) {
505
+ stylesChildren.push(style.toXML());
506
+ }
507
+
508
+ builder.element(
509
+ 'w:styles',
510
+ {
511
+ 'xmlns:w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main',
512
+ 'xmlns:r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships',
513
+ },
514
+ stylesChildren
515
+ );
516
+
517
+ return builder.build(true);
518
+ }
519
+
520
+ /**
521
+ * Generates document defaults
522
+ */
523
+ private generateDocDefaults() {
524
+ const rPrDefaultChildren = [
525
+ XMLBuilder.wSelf('rFonts', {
526
+ 'w:ascii': 'Calibri',
527
+ 'w:hAnsi': 'Calibri',
528
+ 'w:eastAsia': 'Calibri',
529
+ 'w:cs': 'Calibri',
530
+ }),
531
+ XMLBuilder.wSelf('sz', { 'w:val': '22' }), // 11pt
532
+ XMLBuilder.wSelf('szCs', { 'w:val': '22' }),
533
+ XMLBuilder.wSelf('lang', {
534
+ 'w:val': 'en-US',
535
+ 'w:eastAsia': 'en-US',
536
+ 'w:bidi': 'ar-SA',
537
+ }),
538
+ ];
539
+
540
+ const pPrDefaultChildren = [
541
+ XMLBuilder.wSelf('spacing', {
542
+ 'w:after': '200',
543
+ 'w:line': '276',
544
+ 'w:lineRule': 'auto',
545
+ }),
546
+ ];
547
+
548
+ return XMLBuilder.w('docDefaults', undefined, [
549
+ XMLBuilder.w('rPrDefault', undefined, [XMLBuilder.w('rPr', undefined, rPrDefaultChildren)]),
550
+ XMLBuilder.w('pPrDefault', undefined, [XMLBuilder.w('pPr', undefined, pPrDefaultChildren)]),
551
+ ]);
552
+ }
553
+
554
+ /**
555
+ * Generates the latent styles XML element
556
+ */
557
+ private generateLatentStyles() {
558
+ if (!this.latentStyles) return XMLBuilder.w('latentStyles', {}, []);
559
+
560
+ const attrs: Record<string, string> = {};
561
+ if (this.latentStyles.defaultLockedState !== undefined) {
562
+ attrs['w:defLockedState'] = this.latentStyles.defaultLockedState ? '1' : '0';
563
+ }
564
+ if (this.latentStyles.defaultUiPriority !== undefined) {
565
+ attrs['w:defUIPriority'] = this.latentStyles.defaultUiPriority.toString();
566
+ }
567
+ if (this.latentStyles.defaultSemiHidden !== undefined) {
568
+ attrs['w:defSemiHidden'] = this.latentStyles.defaultSemiHidden ? '1' : '0';
569
+ }
570
+ if (this.latentStyles.defaultUnhideWhenUsed !== undefined) {
571
+ attrs['w:defUnhideWhenUsed'] = this.latentStyles.defaultUnhideWhenUsed ? '1' : '0';
572
+ }
573
+ if (this.latentStyles.defaultQFormat !== undefined) {
574
+ attrs['w:defQFormat'] = this.latentStyles.defaultQFormat ? '1' : '0';
575
+ }
576
+ if (this.latentStyles.count !== undefined) {
577
+ attrs['w:count'] = this.latentStyles.count.toString();
578
+ }
579
+
580
+ const children = this.latentStyleExceptions.map((exc) => {
581
+ const excAttrs: Record<string, string> = { 'w:name': exc.name };
582
+ if (exc.locked !== undefined) excAttrs['w:locked'] = exc.locked ? '1' : '0';
583
+ if (exc.uiPriority !== undefined) excAttrs['w:uiPriority'] = exc.uiPriority.toString();
584
+ if (exc.semiHidden !== undefined) excAttrs['w:semiHidden'] = exc.semiHidden ? '1' : '0';
585
+ if (exc.unhideWhenUsed !== undefined)
586
+ excAttrs['w:unhideWhenUsed'] = exc.unhideWhenUsed ? '1' : '0';
587
+ if (exc.qFormat !== undefined) excAttrs['w:qFormat'] = exc.qFormat ? '1' : '0';
588
+ return XMLBuilder.wSelf('lsdException', excAttrs);
589
+ });
590
+
591
+ return XMLBuilder.w('latentStyles', attrs, children);
592
+ }
593
+
594
+ /**
595
+ * Creates a new StylesManager with built-in styles
596
+ * @returns New StylesManager instance
597
+ */
598
+ static create(): StylesManager {
599
+ return new StylesManager(true);
600
+ }
601
+
602
+ /**
603
+ * Creates an empty StylesManager (no built-in styles)
604
+ * @returns New empty StylesManager instance
605
+ */
606
+ static createEmpty(): StylesManager {
607
+ return new StylesManager(false);
608
+ }
609
+
610
+ /**
611
+ * Validates styles.xml content for structure and correctness
612
+ *
613
+ * This performs string-based validation to avoid XML parsing corruption.
614
+ * It checks for:
615
+ * - Well-formed XML structure
616
+ * - Required w:styles root element
617
+ * - Valid style definitions
618
+ * - No duplicate style IDs
619
+ * - Required attributes
620
+ *
621
+ * @param xml - The raw styles.xml content to validate
622
+ * @returns ValidationResult with details about validity
623
+ */
624
+ static validate(xml: string): ValidationResult {
625
+ const result: ValidationResult = {
626
+ isValid: true,
627
+ errors: [],
628
+ warnings: [],
629
+ styleCount: 0,
630
+ styleIds: [],
631
+ };
632
+
633
+ // Check for empty or null
634
+ if (!xml || xml.trim().length === 0) {
635
+ result.isValid = false;
636
+ result.errors.push('Styles XML is empty or null');
637
+ return result;
638
+ }
639
+
640
+ // Check for common corruption patterns FIRST (before parsing)
641
+ // This catches double-encoding issues that would break the parser
642
+ if (xml.includes('&lt;w:') || xml.includes('&gt;')) {
643
+ result.isValid = false;
644
+ result.errors.push('XML contains escaped tags - possible double-encoding corruption');
645
+ return result;
646
+ }
647
+
648
+ // Skip complex XML structure validation - focus on w:styles specific validation
649
+ // Checking balanced tags with regex is unreliable and can give false positives
650
+
651
+ // Use XMLParser to extract root element
652
+ const stylesContent = XMLParser.extractBetweenTags(xml, '<w:styles', '</w:styles>');
653
+ if (!stylesContent) {
654
+ result.isValid = false;
655
+ result.errors.push('Missing required <w:styles> root element');
656
+ return result;
657
+ }
658
+
659
+ // Check for namespace declaration
660
+ if (!xml.includes('xmlns:w=')) {
661
+ result.warnings.push('Missing WordprocessingML namespace declaration');
662
+ }
663
+
664
+ // Use XMLParser to extract all style elements
665
+ const styleElements = XMLParser.extractElements(stylesContent, 'w:style');
666
+ result.styleCount = styleElements.length;
667
+
668
+ // Check if any styles found
669
+ if (styleElements.length === 0) {
670
+ result.warnings.push('No styles found in document');
671
+ return result;
672
+ }
673
+
674
+ // Check for styles without attributes (invalid)
675
+ const styleWithoutAttrs = styleElements.filter((el) => {
676
+ // Check if element has any attributes
677
+ const openTagEnd = el.indexOf('>');
678
+ const openTag = el.substring(0, openTagEnd);
679
+ return !openTag.includes('w:type') || !openTag.includes('w:styleId');
680
+ });
681
+
682
+ if (styleWithoutAttrs.length > 0) {
683
+ result.isValid = false;
684
+ result.errors.push('Style found without any attributes - w:type and w:styleId are required');
685
+ }
686
+
687
+ // Process each style element
688
+ const foundStyleIds = new Set<string>();
689
+
690
+ for (const styleElement of styleElements) {
691
+ // Extract styleId using XMLParser
692
+ const styleId = XMLParser.extractAttribute(styleElement, 'w:styleId');
693
+ if (styleId) {
694
+ // Check for duplicates
695
+ if (foundStyleIds.has(styleId)) {
696
+ result.isValid = false;
697
+ result.errors.push(`Duplicate style ID found: "${styleId}"`);
698
+ } else {
699
+ foundStyleIds.add(styleId);
700
+ result.styleIds.push(styleId);
701
+ }
702
+ } else {
703
+ result.isValid = false;
704
+ result.errors.push('Style found without required w:styleId attribute');
705
+ }
706
+
707
+ // Extract and validate type using XMLParser
708
+ const type = XMLParser.extractAttribute(styleElement, 'w:type');
709
+ if (type) {
710
+ if (!['paragraph', 'character', 'table', 'numbering'].includes(type)) {
711
+ result.warnings.push(`Invalid style type: "${type}"`);
712
+ }
713
+ } else {
714
+ result.isValid = false;
715
+ result.errors.push('Style found without required w:type attribute');
716
+ }
717
+
718
+ // Check for circular references - extract basedOn value
719
+ const basedOnElement = XMLParser.extractElements(styleElement, 'w:basedOn')[0];
720
+ if (basedOnElement && styleId) {
721
+ const basedOn = XMLParser.extractAttribute(basedOnElement, 'w:val');
722
+ if (basedOn && styleId === basedOn) {
723
+ result.isValid = false;
724
+ result.errors.push(`Circular reference detected: style "${styleId}" based on itself`);
725
+ }
726
+ }
727
+ }
728
+
729
+ // Check for required Normal style
730
+ if (!foundStyleIds.has('Normal')) {
731
+ result.warnings.push('Missing "Normal" style - document may not render correctly');
732
+ }
733
+
734
+ // Check for BOM or invalid characters
735
+ if (xml.charCodeAt(0) === 0xfeff) {
736
+ result.warnings.push('XML contains BOM (Byte Order Mark) - may cause parsing issues');
737
+ }
738
+
739
+ // Summary
740
+ if (result.styleCount === 0) {
741
+ result.warnings.push('No styles found in document');
742
+ }
743
+
744
+ return result;
745
+ }
746
+
747
+ /**
748
+ * Searches styles by name (case-insensitive)
749
+ * @param searchTerm - Text to search for in style names
750
+ * @returns Array of styles whose names contain the search term
751
+ * @example
752
+ * ```typescript
753
+ * const headings = stylesManager.searchByName('heading');
754
+ * console.log(`Found ${headings.length} heading styles`);
755
+ * ```
756
+ */
757
+ searchByName(searchTerm: string): Style[] {
758
+ const term = searchTerm.toLowerCase();
759
+ return this.getAllStyles().filter((style) => style.getName().toLowerCase().includes(term));
760
+ }
761
+
762
+ /**
763
+ * Finds styles using a specific font
764
+ * @param fontName - Font family name to search for
765
+ * @returns Array of styles that use the specified font
766
+ * @example
767
+ * ```typescript
768
+ * const arialStyles = stylesManager.findByFont('Arial');
769
+ * console.log(`Found ${arialStyles.length} styles using Arial font`);
770
+ * ```
771
+ */
772
+ findByFont(fontName: string): Style[] {
773
+ return this.getAllStyles().filter((style) => {
774
+ const runFormatting = style.getRunFormatting();
775
+ return runFormatting?.font === fontName;
776
+ });
777
+ }
778
+
779
+ /**
780
+ * Finds styles with specific properties using a predicate function
781
+ * @param predicate - Filter function that returns true for styles to include
782
+ * @returns Array of styles matching the predicate
783
+ * @example
784
+ * ```typescript
785
+ * // Find all paragraph styles
786
+ * const paraStyles = stylesManager.findStyles(s => s.getType() === 'paragraph');
787
+ *
788
+ * // Find styles with custom formatting
789
+ * const customStyles = stylesManager.findStyles(s => s.getProperties().customStyle);
790
+ *
791
+ * // Find styles with specific formatting
792
+ * const boldStyles = stylesManager.findStyles(s => s.getRunFormatting()?.bold);
793
+ * ```
794
+ */
795
+ findStyles(predicate: (style: Style) => boolean): Style[] {
796
+ return this.getAllStyles().filter(predicate);
797
+ }
798
+
799
+ /**
800
+ * Finds unused styles (not referenced by any paragraphs)
801
+ * @param paragraphs - All paragraphs in the document to check against
802
+ * @returns Array of unused style IDs
803
+ * @example
804
+ * ```typescript
805
+ * const doc = Document.create();
806
+ * const unused = stylesManager.findUnusedStyles(doc.getAllParagraphs());
807
+ * console.log(`Found ${unused.length} unused styles: ${unused.join(', ')}`);
808
+ * ```
809
+ */
810
+ findUnusedStyles(paragraphs: Paragraph[]): string[] {
811
+ const usedStyles = new Set<string>();
812
+
813
+ // Collect all style IDs used by paragraphs
814
+ for (const para of paragraphs) {
815
+ const styleId = para.getStyle();
816
+ if (styleId) {
817
+ usedStyles.add(styleId);
818
+ }
819
+ }
820
+
821
+ // Get all style IDs and filter out used ones
822
+ const allStyleIds = this.getAllStyles().map((style) => style.getStyleId());
823
+ return allStyleIds.filter((styleId) => !usedStyles.has(styleId));
824
+ }
825
+
826
+ /**
827
+ * Removes all unused styles from the document
828
+ * @param paragraphs - All paragraphs in the document to check against
829
+ * @returns Number of styles removed
830
+ * @example
831
+ * ```typescript
832
+ * const doc = Document.create();
833
+ * const removedCount = stylesManager.cleanupUnusedStyles(doc.getAllParagraphs());
834
+ * console.log(`Cleaned up ${removedCount} unused styles`);
835
+ * ```
836
+ */
837
+ cleanupUnusedStyles(paragraphs: Paragraph[]): number {
838
+ const unused = this.findUnusedStyles(paragraphs);
839
+ let count = 0;
840
+
841
+ for (const styleId of unused) {
842
+ // Don't remove built-in styles
843
+ if (!StylesManager.isBuiltInStyle(styleId)) {
844
+ if (this.removeStyle(styleId)) {
845
+ count++;
846
+ }
847
+ }
848
+ }
849
+
850
+ return count;
851
+ }
852
+
853
+ /**
854
+ * Validates all style references for broken or circular dependencies
855
+ * @returns Validation result with broken references and circular dependencies
856
+ * @example
857
+ * ```typescript
858
+ * const validation = stylesManager.validateStyleReferences();
859
+ * if (!validation.valid) {
860
+ * console.log('Broken references:', validation.brokenReferences);
861
+ * console.log('Circular references:', validation.circularReferences);
862
+ * }
863
+ * ```
864
+ */
865
+ validateStyleReferences(): {
866
+ valid: boolean;
867
+ brokenReferences: { styleId: string; basedOn: string }[];
868
+ circularReferences: string[][];
869
+ } {
870
+ const broken: { styleId: string; basedOn: string }[] = [];
871
+ const circular: string[][] = [];
872
+ const checkedForCycles = new Set<string>();
873
+
874
+ for (const style of this.getAllStyles()) {
875
+ const props = style.getProperties();
876
+
877
+ // Check basedOn references exist
878
+ if (props.basedOn && !this.hasStyle(props.basedOn)) {
879
+ broken.push({ styleId: props.styleId, basedOn: props.basedOn });
880
+ }
881
+
882
+ // Check for circular references (only if not already checked as part of another cycle)
883
+ if (!checkedForCycles.has(props.styleId)) {
884
+ const result = this.hasCircularReference(props.styleId);
885
+ if (result.hasCircularRef && result.cyclePath) {
886
+ circular.push(result.cyclePath);
887
+ // Mark all styles in this cycle as checked
888
+ result.cyclePath.forEach((id) => checkedForCycles.add(id));
889
+ }
890
+ }
891
+ }
892
+
893
+ return {
894
+ valid: broken.length === 0 && circular.length === 0,
895
+ brokenReferences: broken,
896
+ circularReferences: circular,
897
+ };
898
+ }
899
+
900
+ /**
901
+ * Gets the complete inheritance chain for a style
902
+ * @param styleId - Style ID to analyze
903
+ * @returns Array of styles from base to derived (base style first)
904
+ * @throws Error if style doesn't exist or circular reference detected
905
+ * @example
906
+ * ```typescript
907
+ * const chain = stylesManager.getInheritanceChain('Heading1');
908
+ * console.log('Inheritance chain:', chain.map(s => s.getName()));
909
+ * // Output: ['Normal', 'Heading1'] (Normal is base, Heading1 inherits from it)
910
+ * ```
911
+ */
912
+ getInheritanceChain(styleId: string): Style[] {
913
+ const chain: Style[] = [];
914
+ const visited = new Set<string>(); // Track visited styles to detect cycles
915
+ let current = this.getStyle(styleId);
916
+
917
+ if (!current) {
918
+ throw new Error(`Style '${styleId}' not found`);
919
+ }
920
+
921
+ while (current) {
922
+ const currentId = current.getStyleId();
923
+
924
+ // Detect circular reference
925
+ if (visited.has(currentId)) {
926
+ const cycle = [...chain.map((s) => s.getStyleId()), currentId];
927
+ throw new Error(
928
+ `Circular style reference detected: ${cycle.join(' -> ')}. ` +
929
+ `Style '${currentId}' references itself through inheritance chain.`
930
+ );
931
+ }
932
+
933
+ visited.add(currentId);
934
+ chain.unshift(current); // Add to beginning to maintain base-to-derived order
935
+ const props = current.getProperties();
936
+ current = props.basedOn ? this.getStyle(props.basedOn) : undefined;
937
+ }
938
+
939
+ return chain;
940
+ }
941
+
942
+ /**
943
+ * Checks if a style has circular references in its inheritance chain
944
+ * @param styleId - Style ID to check
945
+ * @returns Object with hasCircularRef flag and the cycle path if found
946
+ * @example
947
+ * ```typescript
948
+ * const result = stylesManager.hasCircularReference('MyStyle');
949
+ * if (result.hasCircularRef) {
950
+ * console.log('Circular reference found:', result.cyclePath?.join(' -> '));
951
+ * }
952
+ * ```
953
+ */
954
+ hasCircularReference(styleId: string): {
955
+ hasCircularRef: boolean;
956
+ cyclePath?: string[];
957
+ } {
958
+ const visited = new Set<string>();
959
+ const path: string[] = [];
960
+ let current = this.getStyle(styleId);
961
+
962
+ while (current) {
963
+ const currentId = current.getStyleId();
964
+
965
+ if (visited.has(currentId)) {
966
+ path.push(currentId);
967
+ return { hasCircularRef: true, cyclePath: path };
968
+ }
969
+
970
+ visited.add(currentId);
971
+ path.push(currentId);
972
+ const props = current.getProperties();
973
+ current = props.basedOn ? this.getStyle(props.basedOn) : undefined;
974
+ }
975
+
976
+ return { hasCircularRef: false };
977
+ }
978
+
979
+ /**
980
+ * Gets all styles that inherit from a base style
981
+ * @param baseStyleId - Base style ID to find children for
982
+ * @returns Array of styles that inherit from the base style
983
+ * @example
984
+ * ```typescript
985
+ * const children = stylesManager.getDerivedStyles('Normal');
986
+ * console.log(`Styles based on Normal: ${children.map(s => s.getName()).join(', ')}`);
987
+ * ```
988
+ */
989
+ getDerivedStyles(baseStyleId: string): Style[] {
990
+ return this.getAllStyles().filter((style) => style.getProperties().basedOn === baseStyleId);
991
+ }
992
+
993
+ /**
994
+ * Exports a single style as JSON string
995
+ * @param styleId - Style ID to export
996
+ * @returns JSON representation of the style
997
+ * @throws Error if style doesn't exist
998
+ * @example
999
+ * ```typescript
1000
+ * const json = stylesManager.exportStyle('Heading1');
1001
+ * console.log('Style JSON:', json);
1002
+ *
1003
+ * // Save to file
1004
+ * await fs.writeFile('heading1-style.json', json);
1005
+ * ```
1006
+ */
1007
+ exportStyle(styleId: string): string {
1008
+ const style = this.getStyle(styleId);
1009
+ if (!style) {
1010
+ throw new Error(`Style '${styleId}' not found`);
1011
+ }
1012
+ return JSON.stringify(style.getProperties(), null, 2);
1013
+ }
1014
+
1015
+ /**
1016
+ * Imports a style from JSON string
1017
+ * @param json - JSON string containing style properties
1018
+ * @returns The imported style
1019
+ * @throws Error if JSON is invalid or style creation fails
1020
+ * @example
1021
+ * ```typescript
1022
+ * const json = await fs.readFile('custom-style.json', 'utf8');
1023
+ * const style = stylesManager.importStyle(json);
1024
+ * console.log(`Imported style: ${style.getName()}`);
1025
+ * ```
1026
+ */
1027
+ importStyle(json: string): Style {
1028
+ try {
1029
+ const props = JSON.parse(json);
1030
+ const style = Style.create(props);
1031
+ this.addStyle(style);
1032
+ return style;
1033
+ } catch (error: unknown) {
1034
+ throw new Error(
1035
+ `Failed to import style: ${error instanceof Error ? error.message : 'Invalid JSON'}`
1036
+ );
1037
+ }
1038
+ }
1039
+
1040
+ /**
1041
+ * Exports all styles as JSON string
1042
+ * @returns JSON array of all style properties
1043
+ * @example
1044
+ * ```typescript
1045
+ * const allStylesJson = stylesManager.exportAllStyles();
1046
+ * console.log('All styles exported');
1047
+ *
1048
+ * // Save to file
1049
+ * await fs.writeFile('all-styles.json', allStylesJson);
1050
+ * ```
1051
+ */
1052
+ exportAllStyles(): string {
1053
+ const styles = this.getAllStyles().map((style) => style.getProperties());
1054
+ return JSON.stringify(styles, null, 2);
1055
+ }
1056
+
1057
+ /**
1058
+ * Imports multiple styles from JSON array string
1059
+ * @param json - JSON string containing array of style properties
1060
+ * @returns Array of imported styles
1061
+ * @throws Error if JSON is invalid or style creation fails
1062
+ * @example
1063
+ * ```typescript
1064
+ * const json = await fs.readFile('styles-collection.json', 'utf8');
1065
+ * const styles = stylesManager.importStyles(json);
1066
+ * console.log(`Imported ${styles.length} styles`);
1067
+ * ```
1068
+ */
1069
+ importStyles(json: string): Style[] {
1070
+ try {
1071
+ const propsArray = JSON.parse(json);
1072
+ if (!Array.isArray(propsArray)) {
1073
+ throw new Error('JSON must contain an array of style properties');
1074
+ }
1075
+
1076
+ const styles: Style[] = [];
1077
+ for (const props of propsArray) {
1078
+ const style = Style.create(props);
1079
+ this.addStyle(style);
1080
+ styles.push(style);
1081
+ }
1082
+
1083
+ return styles;
1084
+ } catch (error: unknown) {
1085
+ throw new Error(
1086
+ `Failed to import styles: ${error instanceof Error ? error.message : 'Invalid JSON'}`
1087
+ );
1088
+ }
1089
+ }
1090
+ }