pretext-pdf 1.1.1 → 1.6.0

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 (342) hide show
  1. package/CHANGELOG.md +659 -0
  2. package/README.md +82 -7
  3. package/dist/allowed-props.d.ts +76 -0
  4. package/dist/allowed-props.d.ts.map +1 -1
  5. package/dist/allowed-props.js.map +1 -1
  6. package/dist/assets/generators/barcode.d.ts +9 -0
  7. package/dist/assets/generators/barcode.d.ts.map +1 -0
  8. package/dist/assets/generators/barcode.js +24 -0
  9. package/dist/assets/generators/barcode.js.map +1 -0
  10. package/dist/assets/generators/chart.d.ts +13 -0
  11. package/dist/assets/generators/chart.d.ts.map +1 -0
  12. package/dist/assets/generators/chart.js +32 -0
  13. package/dist/assets/generators/chart.js.map +1 -0
  14. package/dist/assets/generators/qr.d.ts +9 -0
  15. package/dist/assets/generators/qr.d.ts.map +1 -0
  16. package/dist/assets/generators/qr.js +25 -0
  17. package/dist/assets/generators/qr.js.map +1 -0
  18. package/dist/assets/index.d.ts +19 -0
  19. package/dist/assets/index.d.ts.map +1 -0
  20. package/dist/assets/index.js +19 -0
  21. package/dist/assets/index.js.map +1 -0
  22. package/dist/assets/loaders/images.d.ts +20 -0
  23. package/dist/assets/loaders/images.d.ts.map +1 -0
  24. package/dist/assets/loaders/images.js +69 -0
  25. package/dist/assets/loaders/images.js.map +1 -0
  26. package/dist/assets/loaders/orchestrator.d.ts +24 -0
  27. package/dist/assets/loaders/orchestrator.d.ts.map +1 -0
  28. package/dist/assets/loaders/orchestrator.js +109 -0
  29. package/dist/assets/loaders/orchestrator.js.map +1 -0
  30. package/dist/assets/loaders/vectors.d.ts +25 -0
  31. package/dist/assets/loaders/vectors.d.ts.map +1 -0
  32. package/dist/assets/loaders/vectors.js +118 -0
  33. package/dist/assets/loaders/vectors.js.map +1 -0
  34. package/dist/assets/loaders/watermark.d.ts +12 -0
  35. package/dist/assets/loaders/watermark.d.ts.map +1 -0
  36. package/dist/assets/loaders/watermark.js +40 -0
  37. package/dist/assets/loaders/watermark.js.map +1 -0
  38. package/dist/assets/security/fetch.d.ts +14 -0
  39. package/dist/assets/security/fetch.d.ts.map +1 -0
  40. package/dist/assets/security/fetch.js +112 -0
  41. package/dist/assets/security/fetch.js.map +1 -0
  42. package/dist/assets/security/ipv4-normalize.d.ts +28 -0
  43. package/dist/assets/security/ipv4-normalize.d.ts.map +1 -0
  44. package/dist/assets/security/ipv4-normalize.js +116 -0
  45. package/dist/assets/security/ipv4-normalize.js.map +1 -0
  46. package/dist/assets/security/path-allowlist.d.ts +12 -0
  47. package/dist/assets/security/path-allowlist.d.ts.map +1 -0
  48. package/dist/assets/security/path-allowlist.js +26 -0
  49. package/dist/assets/security/path-allowlist.js.map +1 -0
  50. package/dist/assets/security/url-validation.d.ts +22 -0
  51. package/dist/assets/security/url-validation.d.ts.map +1 -0
  52. package/dist/assets/security/url-validation.js +164 -0
  53. package/dist/assets/security/url-validation.js.map +1 -0
  54. package/dist/assets/svg/dimensions.d.ts +19 -0
  55. package/dist/assets/svg/dimensions.d.ts.map +1 -0
  56. package/dist/assets/svg/dimensions.js +43 -0
  57. package/dist/assets/svg/dimensions.js.map +1 -0
  58. package/dist/assets/svg/rasterize.d.ts +6 -0
  59. package/dist/assets/svg/rasterize.d.ts.map +1 -0
  60. package/dist/assets/svg/rasterize.js +38 -0
  61. package/dist/assets/svg/rasterize.js.map +1 -0
  62. package/dist/assets/svg/resolve-content.d.ts +16 -0
  63. package/dist/assets/svg/resolve-content.d.ts.map +1 -0
  64. package/dist/assets/svg/resolve-content.js +38 -0
  65. package/dist/assets/svg/resolve-content.js.map +1 -0
  66. package/dist/assets/svg/sanitize.d.ts +22 -0
  67. package/dist/assets/svg/sanitize.d.ts.map +1 -0
  68. package/dist/assets/svg/sanitize.js +46 -0
  69. package/dist/assets/svg/sanitize.js.map +1 -0
  70. package/dist/assets/util/redact-path.d.ts +14 -0
  71. package/dist/assets/util/redact-path.d.ts.map +1 -0
  72. package/dist/assets/util/redact-path.js +16 -0
  73. package/dist/assets/util/redact-path.js.map +1 -0
  74. package/dist/assets.d.ts +10 -27
  75. package/dist/assets.d.ts.map +1 -1
  76. package/dist/assets.js +10 -549
  77. package/dist/assets.js.map +1 -1
  78. package/dist/builder.d.ts.map +1 -1
  79. package/dist/builder.js +2 -1
  80. package/dist/builder.js.map +1 -1
  81. package/dist/cli.js +11 -1
  82. package/dist/cli.js.map +1 -1
  83. package/dist/compat.d.ts +63 -1
  84. package/dist/compat.d.ts.map +1 -1
  85. package/dist/compat.js +42 -5
  86. package/dist/compat.js.map +1 -1
  87. package/dist/errors.d.ts +2 -2
  88. package/dist/errors.d.ts.map +1 -1
  89. package/dist/errors.js +2 -2
  90. package/dist/errors.js.map +1 -1
  91. package/dist/fonts.d.ts.map +1 -1
  92. package/dist/fonts.js +8 -10
  93. package/dist/fonts.js.map +1 -1
  94. package/dist/index.d.ts +1 -1
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +7 -5
  97. package/dist/index.js.map +1 -1
  98. package/dist/layout-state.d.ts +1 -1
  99. package/dist/layout-state.d.ts.map +1 -1
  100. package/dist/layout-state.js +5 -0
  101. package/dist/layout-state.js.map +1 -1
  102. package/dist/measure-blocks/float-group.d.ts +9 -0
  103. package/dist/measure-blocks/float-group.d.ts.map +1 -0
  104. package/dist/measure-blocks/float-group.js +103 -0
  105. package/dist/measure-blocks/float-group.js.map +1 -0
  106. package/dist/measure-blocks/helpers.d.ts +44 -0
  107. package/dist/measure-blocks/helpers.d.ts.map +1 -0
  108. package/dist/measure-blocks/helpers.js +43 -0
  109. package/dist/measure-blocks/helpers.js.map +1 -0
  110. package/dist/measure-blocks/highlight.d.ts +26 -0
  111. package/dist/measure-blocks/highlight.d.ts.map +1 -0
  112. package/dist/measure-blocks/highlight.js +169 -0
  113. package/dist/measure-blocks/highlight.js.map +1 -0
  114. package/dist/measure-blocks/image.d.ts +9 -0
  115. package/dist/measure-blocks/image.d.ts.map +1 -0
  116. package/dist/measure-blocks/image.js +136 -0
  117. package/dist/measure-blocks/image.js.map +1 -0
  118. package/dist/measure-blocks/index.d.ts +24 -0
  119. package/dist/measure-blocks/index.d.ts.map +1 -0
  120. package/dist/measure-blocks/index.js +179 -0
  121. package/dist/measure-blocks/index.js.map +1 -0
  122. package/dist/measure-blocks/list.d.ts +8 -0
  123. package/dist/measure-blocks/list.d.ts.map +1 -0
  124. package/dist/measure-blocks/list.js +108 -0
  125. package/dist/measure-blocks/list.js.map +1 -0
  126. package/dist/measure-blocks/simple-blocks.d.ts +18 -0
  127. package/dist/measure-blocks/simple-blocks.d.ts.map +1 -0
  128. package/dist/measure-blocks/simple-blocks.js +121 -0
  129. package/dist/measure-blocks/simple-blocks.js.map +1 -0
  130. package/dist/measure-blocks/table/columns.d.ts +17 -0
  131. package/dist/measure-blocks/table/columns.d.ts.map +1 -0
  132. package/dist/measure-blocks/table/columns.js +83 -0
  133. package/dist/measure-blocks/table/columns.js.map +1 -0
  134. package/dist/measure-blocks/table/measure.d.ts +8 -0
  135. package/dist/measure-blocks/table/measure.d.ts.map +1 -0
  136. package/dist/measure-blocks/table/measure.js +231 -0
  137. package/dist/measure-blocks/table/measure.js.map +1 -0
  138. package/dist/measure-blocks/table/spans.d.ts +25 -0
  139. package/dist/measure-blocks/table/spans.d.ts.map +1 -0
  140. package/dist/measure-blocks/table/spans.js +55 -0
  141. package/dist/measure-blocks/table/spans.js.map +1 -0
  142. package/dist/measure-blocks/text-blocks.d.ts +17 -0
  143. package/dist/measure-blocks/text-blocks.d.ts.map +1 -0
  144. package/dist/measure-blocks/text-blocks.js +242 -0
  145. package/dist/measure-blocks/text-blocks.js.map +1 -0
  146. package/dist/measure-text.d.ts +21 -3
  147. package/dist/measure-text.d.ts.map +1 -1
  148. package/dist/measure-text.js +87 -36
  149. package/dist/measure-text.js.map +1 -1
  150. package/dist/measure.d.ts +1 -1
  151. package/dist/measure.d.ts.map +1 -1
  152. package/dist/measure.js +8 -6
  153. package/dist/measure.js.map +1 -1
  154. package/dist/node-polyfill.d.ts.map +1 -1
  155. package/dist/node-polyfill.js +9 -0
  156. package/dist/node-polyfill.js.map +1 -1
  157. package/dist/pipeline-footnotes.d.ts +1 -1
  158. package/dist/pipeline-footnotes.d.ts.map +1 -1
  159. package/dist/pipeline-toc.d.ts +1 -1
  160. package/dist/pipeline-toc.d.ts.map +1 -1
  161. package/dist/pipeline.d.ts +3 -3
  162. package/dist/pipeline.d.ts.map +1 -1
  163. package/dist/pipeline.js +4 -5
  164. package/dist/pipeline.js.map +1 -1
  165. package/dist/plugin-types.d.ts +1 -1
  166. package/dist/plugin-types.d.ts.map +1 -1
  167. package/dist/post-process.d.ts +2 -2
  168. package/dist/post-process.d.ts.map +1 -1
  169. package/dist/post-process.js +32 -9
  170. package/dist/post-process.js.map +1 -1
  171. package/dist/render-blocks/blockquote.d.ts +7 -0
  172. package/dist/render-blocks/blockquote.d.ts.map +1 -0
  173. package/dist/render-blocks/blockquote.js +87 -0
  174. package/dist/render-blocks/blockquote.js.map +1 -0
  175. package/dist/render-blocks/callout.d.ts +7 -0
  176. package/dist/render-blocks/callout.d.ts.map +1 -0
  177. package/dist/render-blocks/callout.js +84 -0
  178. package/dist/render-blocks/callout.js.map +1 -0
  179. package/dist/render-blocks/code.d.ts +7 -0
  180. package/dist/render-blocks/code.d.ts.map +1 -0
  181. package/dist/render-blocks/code.js +84 -0
  182. package/dist/render-blocks/code.js.map +1 -0
  183. package/dist/render-blocks/footnote.d.ts +11 -0
  184. package/dist/render-blocks/footnote.d.ts.map +1 -0
  185. package/dist/render-blocks/footnote.js +45 -0
  186. package/dist/render-blocks/footnote.js.map +1 -0
  187. package/dist/render-blocks/header-footer.d.ts +11 -0
  188. package/dist/render-blocks/header-footer.d.ts.map +1 -0
  189. package/dist/render-blocks/header-footer.js +56 -0
  190. package/dist/render-blocks/header-footer.js.map +1 -0
  191. package/dist/render-blocks/hr.d.ts +7 -0
  192. package/dist/render-blocks/hr.d.ts.map +1 -0
  193. package/dist/render-blocks/hr.js +24 -0
  194. package/dist/render-blocks/hr.js.map +1 -0
  195. package/dist/render-blocks/image.d.ts +9 -0
  196. package/dist/render-blocks/image.d.ts.map +1 -0
  197. package/dist/render-blocks/image.js +135 -0
  198. package/dist/render-blocks/image.js.map +1 -0
  199. package/dist/render-blocks/index.d.ts +17 -0
  200. package/dist/render-blocks/index.d.ts.map +1 -0
  201. package/dist/render-blocks/index.js +17 -0
  202. package/dist/render-blocks/index.js.map +1 -0
  203. package/dist/render-blocks/list-item.d.ts +7 -0
  204. package/dist/render-blocks/list-item.d.ts.map +1 -0
  205. package/dist/render-blocks/list-item.js +80 -0
  206. package/dist/render-blocks/list-item.js.map +1 -0
  207. package/dist/render-blocks/rich.d.ts +7 -0
  208. package/dist/render-blocks/rich.d.ts.map +1 -0
  209. package/dist/render-blocks/rich.js +160 -0
  210. package/dist/render-blocks/rich.js.map +1 -0
  211. package/dist/render-blocks/table.d.ts +7 -0
  212. package/dist/render-blocks/table.d.ts.map +1 -0
  213. package/dist/render-blocks/table.js +139 -0
  214. package/dist/render-blocks/table.js.map +1 -0
  215. package/dist/render-blocks/text.d.ts +7 -0
  216. package/dist/render-blocks/text.d.ts.map +1 -0
  217. package/dist/render-blocks/text.js +183 -0
  218. package/dist/render-blocks/text.js.map +1 -0
  219. package/dist/render-blocks/watermark.d.ts +8 -0
  220. package/dist/render-blocks/watermark.d.ts.map +1 -0
  221. package/dist/render-blocks/watermark.js +52 -0
  222. package/dist/render-blocks/watermark.js.map +1 -0
  223. package/dist/render-extras.d.ts.map +1 -1
  224. package/dist/render-extras.js +1 -2
  225. package/dist/render-extras.js.map +1 -1
  226. package/dist/render-utils.d.ts.map +1 -1
  227. package/dist/render-utils.js +10 -6
  228. package/dist/render-utils.js.map +1 -1
  229. package/dist/render.d.ts.map +1 -1
  230. package/dist/render.js +9 -3
  231. package/dist/render.js.map +1 -1
  232. package/dist/rich-text.d.ts +2 -1
  233. package/dist/rich-text.d.ts.map +1 -1
  234. package/dist/rich-text.js +0 -1
  235. package/dist/rich-text.js.map +1 -1
  236. package/dist/types-internal.d.ts +19 -3
  237. package/dist/types-internal.d.ts.map +1 -1
  238. package/dist/types-public/document.d.ts +261 -0
  239. package/dist/types-public/document.d.ts.map +1 -0
  240. package/dist/types-public/document.js +2 -0
  241. package/dist/types-public/document.js.map +1 -0
  242. package/dist/types-public/elements-block.d.ts +246 -0
  243. package/dist/types-public/elements-block.d.ts.map +1 -0
  244. package/dist/types-public/elements-block.js +8 -0
  245. package/dist/types-public/elements-block.js.map +1 -0
  246. package/dist/types-public/elements-media.d.ts +199 -0
  247. package/dist/types-public/elements-media.d.ts.map +1 -0
  248. package/dist/types-public/elements-media.js +2 -0
  249. package/dist/types-public/elements-media.js.map +1 -0
  250. package/dist/types-public/elements-text.d.ts +327 -0
  251. package/dist/types-public/elements-text.d.ts.map +1 -0
  252. package/dist/types-public/elements-text.js +2 -0
  253. package/dist/types-public/elements-text.js.map +1 -0
  254. package/dist/types-public/index.d.ts +14 -0
  255. package/dist/types-public/index.d.ts.map +1 -0
  256. package/dist/types-public/index.js +2 -0
  257. package/dist/types-public/index.js.map +1 -0
  258. package/dist/types-public/render-options.d.ts +38 -0
  259. package/dist/types-public/render-options.d.ts.map +1 -0
  260. package/dist/types-public/render-options.js +2 -0
  261. package/dist/types-public/render-options.js.map +1 -0
  262. package/dist/types-public/union.d.ts +13 -0
  263. package/dist/types-public/union.d.ts.map +1 -0
  264. package/dist/types-public/union.js +2 -0
  265. package/dist/types-public/union.js.map +1 -0
  266. package/dist/types-public/validation.d.ts +64 -0
  267. package/dist/types-public/validation.d.ts.map +1 -0
  268. package/dist/types-public/validation.js +2 -0
  269. package/dist/types-public/validation.js.map +1 -0
  270. package/dist/types-public.d.ts +5 -1081
  271. package/dist/types-public.d.ts.map +1 -1
  272. package/dist/types.d.ts +1 -1
  273. package/dist/types.d.ts.map +1 -1
  274. package/dist/validate/document.d.ts +28 -0
  275. package/dist/validate/document.d.ts.map +1 -0
  276. package/dist/validate/document.js +295 -0
  277. package/dist/validate/document.js.map +1 -0
  278. package/dist/validate/elements/forms-floats.d.ts +19 -0
  279. package/dist/validate/elements/forms-floats.d.ts.map +1 -0
  280. package/dist/validate/elements/forms-floats.js +96 -0
  281. package/dist/validate/elements/forms-floats.js.map +1 -0
  282. package/dist/validate/elements/list.d.ts +10 -0
  283. package/dist/validate/elements/list.d.ts.map +1 -0
  284. package/dist/validate/elements/list.js +66 -0
  285. package/dist/validate/elements/list.js.map +1 -0
  286. package/dist/validate/elements/media.d.ts +23 -0
  287. package/dist/validate/elements/media.d.ts.map +1 -0
  288. package/dist/validate/elements/media.js +179 -0
  289. package/dist/validate/elements/media.js.map +1 -0
  290. package/dist/validate/elements/structural-simple.d.ts +21 -0
  291. package/dist/validate/elements/structural-simple.d.ts.map +1 -0
  292. package/dist/validate/elements/structural-simple.js +63 -0
  293. package/dist/validate/elements/structural-simple.js.map +1 -0
  294. package/dist/validate/elements/structural.d.ts +12 -0
  295. package/dist/validate/elements/structural.d.ts.map +1 -0
  296. package/dist/validate/elements/structural.js +12 -0
  297. package/dist/validate/elements/structural.js.map +1 -0
  298. package/dist/validate/elements/table.d.ts +10 -0
  299. package/dist/validate/elements/table.d.ts.map +1 -0
  300. package/dist/validate/elements/table.js +165 -0
  301. package/dist/validate/elements/table.js.map +1 -0
  302. package/dist/validate/elements/text.d.ts +26 -0
  303. package/dist/validate/elements/text.d.ts.map +1 -0
  304. package/dist/validate/elements/text.js +331 -0
  305. package/dist/validate/elements/text.js.map +1 -0
  306. package/dist/validate/errors.d.ts +9 -0
  307. package/dist/validate/errors.d.ts.map +1 -0
  308. package/dist/validate/errors.js +43 -0
  309. package/dist/validate/errors.js.map +1 -0
  310. package/dist/validate/fonts.d.ts +11 -0
  311. package/dist/validate/fonts.d.ts.map +1 -0
  312. package/dist/validate/fonts.js +118 -0
  313. package/dist/validate/fonts.js.map +1 -0
  314. package/dist/validate/helpers.d.ts +76 -0
  315. package/dist/validate/helpers.d.ts.map +1 -0
  316. package/dist/validate/helpers.js +169 -0
  317. package/dist/validate/helpers.js.map +1 -0
  318. package/dist/validate/index.d.ts +37 -0
  319. package/dist/validate/index.d.ts.map +1 -0
  320. package/dist/validate/index.js +279 -0
  321. package/dist/validate/index.js.map +1 -0
  322. package/dist/validate.d.ts +6 -18
  323. package/dist/validate.d.ts.map +1 -1
  324. package/dist/validate.js +6 -1585
  325. package/dist/validate.js.map +1 -1
  326. package/dist/vendor/pretext/VERSION.d.ts +3 -0
  327. package/dist/vendor/pretext/VERSION.d.ts.map +1 -0
  328. package/dist/vendor/pretext/VERSION.js +12 -0
  329. package/dist/vendor/pretext/VERSION.js.map +1 -0
  330. package/dist/version-check.d.ts +47 -0
  331. package/dist/version-check.d.ts.map +1 -0
  332. package/dist/version-check.js +75 -0
  333. package/dist/version-check.js.map +1 -0
  334. package/package.json +26 -7
  335. package/dist/measure-blocks.d.ts +0 -26
  336. package/dist/measure-blocks.d.ts.map +0 -1
  337. package/dist/measure-blocks.js +0 -1317
  338. package/dist/measure-blocks.js.map +0 -1
  339. package/dist/render-blocks.d.ts +0 -28
  340. package/dist/render-blocks.d.ts.map +0 -1
  341. package/dist/render-blocks.js +0 -1059
  342. package/dist/render-blocks.js.map +0 -1
package/dist/validate.js CHANGED
@@ -1,1589 +1,10 @@
1
- import { PretextPdfError } from './errors.js';
2
- import { resolvePageDimensions } from './page-sizes.js';
3
- import { ALLOWED_PROPS, ALLOWED_PROPS_SUB } from './allowed-props.js';
4
- import { ELEMENT_TYPES } from './element-types.js';
5
- import { findPlugin, runPluginValidate } from './plugin-registry.js';
6
1
  /**
7
- * RTL strong bidi characters Bidi_Class=R or AL per UAX #9.
8
- * Block-level coverage following Unicode DerivedBidiClass defaults.
9
- * Source: https://www.unicode.org/Public/17.0.0/ucd/extracted/DerivedBidiClass.txt
2
+ * src/validate.ts legacy shim, re-exports the new validate/ module.
10
3
  *
11
- * BMP blocks:
12
- * 0590–05FF Hebrew
13
- * 0600–06FF Arabic
14
- * 0700–074F Syriac
15
- * 0750–077F Arabic Supplement
16
- * 0780–07BF Thaana (Maldivian)
17
- * 07C0–07FF N'Ko (West African)
18
- * 0800–083F Samaritan
19
- * 0840–085F Mandaic
20
- * 0860–086F Syriac Supplement
21
- * 0870–089F Arabic Extended-B
22
- * 08A0–08FF Arabic Extended-A
23
- * FB1D–FB4F Hebrew Presentation Forms
24
- * FB50–FDFF Arabic Presentation Forms-A
25
- * FE70–FEFF Arabic Presentation Forms-B
26
- * Supplementary plane blocks (requires /u flag):
27
- * 10800–10CFF Ancient Semitic (Cypriot, Imperial Aramaic, Palmyrene,
28
- * Nabataean, Hatran, Phoenician, Lydian, Old South/North
29
- * Arabian, Manichaean, Avestan, Inscriptional Parthian/
30
- * Pahlavi, Old Turkic, Old Hungarian)
31
- * 10D00–10D3F Hanifi Rohingya
32
- * 10E80–10EFF Yezidi + Arabic Extended-C
33
- * 10F30–10FFF Sogdian, Old Uyghur, Chorasmian, Elymaic
34
- * 1E800–1E95F Mende Kikakui + Adlam (Fulani)
35
- * 1EC70–1ECBF Indic Siyaq Numbers (AL)
36
- * 1EE00–1EEFF Arabic Mathematical Alphabetic Symbols
4
+ * Original (1834L) was split into validate/{index,helpers,fonts,errors}.ts +
5
+ * validate/elements/* in v1.4.0 #11a. This shim exists for one release so
6
+ * external callers importing from 'pretext-pdf/dist/validate.js' (or the
7
+ * source path) continue to work; it will be deleted in step 3 of the split.
37
8
  */
38
- const RTL_REGEX = /[\u0590-\u08FF\uFB1D-\uFB4F\uFB50-\uFDFF\uFE70-\uFEFF\u{10800}-\u{10CFF}\u{10D00}-\u{10D3F}\u{10E80}-\u{10EFF}\u{10F30}-\u{10FFF}\u{1E800}-\u{1E95F}\u{1EC70}-\u{1ECBF}\u{1EE00}-\u{1EEFF}]/u;
39
- /** Valid 6-digit hex color */
40
- const HEX_COLOR_REGEX = /^#[0-9A-Fa-f]{6}$/;
41
- /** Allowed URL schemes for hyperlinks — blocks javascript:, data:, vbscript: */
42
- const SAFE_URL_SCHEME = /^(https?|mailto|ftp|#)/i;
43
- /** BCP47 language tag pattern for hyphenation.language — prevents dynamic-import path injection */
44
- const LANGUAGE_TAG_REGEX = /^[a-zA-Z]{2,8}(-[a-zA-Z0-9]{2,8})*$/;
45
- /** Levenshtein distance, returns 999 if result would exceed 2 */
46
- function levenshteinDist(a, b) {
47
- if (a === b)
48
- return 0;
49
- if (Math.abs(a.length - b.length) > 2)
50
- return 999;
51
- const m = a.length;
52
- const n = b.length;
53
- const prev = Array(n + 1)
54
- .fill(0)
55
- .map((_, j) => j);
56
- const curr = Array(n + 1).fill(0);
57
- for (let i = 1; i <= m; i++) {
58
- curr[0] = i;
59
- for (let j = 1; j <= n; j++) {
60
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
61
- curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
62
- }
63
- for (let j = 0; j <= n; j++)
64
- prev[j] = curr[j];
65
- }
66
- return prev[n] > 2 ? 999 : prev[n];
67
- }
68
- /** Find closest match with edit distance <= 2 */
69
- function closestMatch(prop, allowed) {
70
- let best = null;
71
- let bestDist = 999;
72
- for (const candidate of allowed) {
73
- const d = levenshteinDist(prop, candidate);
74
- if (d > 0 && d <= 2 && d < bestDist) {
75
- best = candidate;
76
- bestDist = d;
77
- }
78
- }
79
- return best;
80
- }
81
- /** Accumulate unknown property errors for an object */
82
- function assertUnknownProps(obj, allowed, path, errors) {
83
- if (!obj || typeof obj !== 'object' || Array.isArray(obj))
84
- return;
85
- for (const key of Object.keys(obj)) {
86
- if (!allowed.has(key)) {
87
- const suggestion = closestMatch(key, allowed);
88
- const hint = suggestion ? `; did you mean "${suggestion}"` : '';
89
- errors.push(`${path}.${key}: unknown property${hint}`);
90
- }
91
- }
92
- }
93
- /** Format accumulated errors into a single message (cap at 20 errors) */
94
- function formatErrors(errors) {
95
- if (errors.length === 0)
96
- return '';
97
- const header = `Strict validation failed (${errors.length} issue${errors.length === 1 ? '' : 's'}):\n`;
98
- const msgs = errors.slice(0, 20);
99
- const suffix = errors.length > 20 ? `\n... and ${errors.length - 20} more error(s)` : '';
100
- return header + msgs.join('\n') + suffix;
101
- }
102
- /** Validate a hyperlink URL — throws VALIDATION_ERROR for unsafe schemes */
103
- function validateUrl(url, prefix) {
104
- if (!SAFE_URL_SCHEME.test(url)) {
105
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix}: URL scheme not allowed — only http, https, mailto, ftp, and anchor (#) links are permitted. Got: "${url.slice(0, 60)}"`);
106
- }
107
- }
108
- /** Validate a metadata string field — rejects control chars and enforces length */
109
- function validateMetadataString(value, fieldName) {
110
- if (value.length > 1000) {
111
- throw new PretextPdfError('VALIDATION_ERROR', `metadata.${fieldName} must not exceed 1000 characters`);
112
- }
113
- if (/[\x00\r\n]/.test(value)) {
114
- throw new PretextPdfError('VALIDATION_ERROR', `metadata.${fieldName} must not contain null bytes or newline characters`);
115
- }
116
- }
117
- /** Valid column width: positive number OR '2*', '*', '1.5*' format */
118
- const STAR_WIDTH_REGEX = /^(\d*\.?\d+)?\*$/;
119
- /** Families always available without explicit doc.fonts entry */
120
- const BUNDLED_FAMILIES = new Set(['Inter']);
121
- /** Font variants (family-weight-style) always available without explicit doc.fonts entry */
122
- const BUNDLED_VARIANTS = new Set(['Inter-400-normal', 'Inter-700-normal', 'Inter-400-italic', 'Inter-700-italic']);
123
- /**
124
- * Validate a PdfDocument and throw a {@link PretextPdfError} if any errors are found.
125
- * @public
126
- */
127
- export function validate(doc, options) {
128
- const strict = options?.strict ?? false;
129
- const errors = [];
130
- // Plugin pre-flight: enforce type string safety and no collision with built-ins
131
- for (const plugin of options?.plugins ?? []) {
132
- if (!/^[a-zA-Z][a-zA-Z0-9_-]*$/.test(plugin.type)) {
133
- throw new PretextPdfError('VALIDATION_ERROR', `Plugin type '${plugin.type}' is invalid. Must start with a letter and contain only letters, digits, hyphens, or underscores.`);
134
- }
135
- if (ELEMENT_TYPES.includes(plugin.type)) {
136
- throw new PretextPdfError('VALIDATION_ERROR', `Plugin type '${plugin.type}' collides with a built-in element type. Choose a different type string.`);
137
- }
138
- }
139
- // Strict: check doc-level properties
140
- if (strict) {
141
- assertUnknownProps(doc, ALLOWED_PROPS_SUB['document'], 'doc', errors);
142
- }
143
- // content must be a non-empty array
144
- if (!Array.isArray(doc.content) || doc.content.length === 0) {
145
- throw new PretextPdfError('VALIDATION_ERROR', 'document.content must be a non-empty array');
146
- }
147
- // memory guard
148
- if (doc.content.length > 50_000) {
149
- throw new PretextPdfError('VALIDATION_ERROR', `document.content has ${doc.content.length} elements (hard limit: 50,000). Split into multiple documents.`);
150
- }
151
- if (doc.content.length > 10_000) {
152
- ;
153
- (options?.logger?.warn ?? console.warn)(`[pretext-pdf] Performance advisory: document.content has ${doc.content.length} elements (recommended max: 10,000). Large documents may be slow.`);
154
- }
155
- // page size
156
- if (Array.isArray(doc.pageSize)) {
157
- const [w, h] = doc.pageSize;
158
- if (typeof w !== 'number' || typeof h !== 'number' ||
159
- !isFinite(w) || !isFinite(h) ||
160
- w <= 0 || h <= 0) {
161
- throw new PretextPdfError('VALIDATION_ERROR', 'pageSize array must be [width, height] with two positive finite numbers in pt');
162
- }
163
- if (w > 14400 || h > 14400) {
164
- throw new PretextPdfError('VALIDATION_ERROR', `pageSize [${w}, ${h}] exceeds maximum 14400pt (200 inches). Values this large cause rendering overflow.`);
165
- }
166
- }
167
- // margins must be non-negative and can't make content area zero/negative
168
- if (doc.margins) {
169
- const m = doc.margins;
170
- for (const side of ['top', 'bottom', 'left', 'right']) {
171
- if (m[side] !== undefined && (typeof m[side] !== 'number' || m[side] < 0 || !isFinite(m[side]))) {
172
- throw new PretextPdfError('VALIDATION_ERROR', `margins.${side} must be a non-negative finite number. Got: ${m[side]}`);
173
- }
174
- }
175
- const [pageW, pageH] = resolvePageDimensions(doc.pageSize);
176
- const left = m.left ?? 72;
177
- const right = m.right ?? 72;
178
- const top = m.top ?? 72;
179
- const bottom = m.bottom ?? 72;
180
- if (pageW - left - right <= 0) {
181
- throw new PretextPdfError('PAGE_TOO_SMALL', `Left+right margins (${left}+${right}) exceed page width (${pageW}pt). Content area would be zero or negative.`);
182
- }
183
- if (pageH - top - bottom <= 0) {
184
- throw new PretextPdfError('PAGE_TOO_SMALL', `Top+bottom margins (${top}+${bottom}) exceed page height (${pageH}pt). Content area would be zero or negative.`);
185
- }
186
- }
187
- // font specs
188
- if (doc.fonts) {
189
- for (const font of doc.fonts) {
190
- validateFontSpec(font);
191
- }
192
- }
193
- // header / footer
194
- for (const [spec, label] of [[doc.header, 'doc.header'], [doc.footer, 'doc.footer']]) {
195
- if (!spec)
196
- continue;
197
- if (typeof spec.text !== 'string') {
198
- throw new PretextPdfError('VALIDATION_ERROR', `${label}.text must be a string`);
199
- }
200
- if (spec.fontSize !== undefined && (typeof spec.fontSize !== 'number' || spec.fontSize <= 0 || !isFinite(spec.fontSize))) {
201
- throw new PretextPdfError('VALIDATION_ERROR', `${label}.fontSize must be a positive finite number`);
202
- }
203
- if (spec.align !== undefined && !['left', 'center', 'right'].includes(spec.align)) {
204
- throw new PretextPdfError('VALIDATION_ERROR', `${label}.align must be 'left', 'center', or 'right'`);
205
- }
206
- if (spec.fontWeight !== undefined && ![400, 700].includes(spec.fontWeight)) {
207
- throw new PretextPdfError('VALIDATION_ERROR', `${label}.fontWeight must be 400 or 700`);
208
- }
209
- if (spec.color !== undefined && !HEX_COLOR_REGEX.test(spec.color)) {
210
- throw new PretextPdfError('VALIDATION_ERROR', `${label}.color must be a 6-digit hex string like '#666666'. Got: '${spec.color}'`);
211
- }
212
- }
213
- // defaultParagraphStyle
214
- if (doc.defaultParagraphStyle !== undefined) {
215
- const dps = doc.defaultParagraphStyle;
216
- if (typeof dps !== 'object' || dps === null || Array.isArray(dps)) {
217
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.defaultParagraphStyle must be an object');
218
- }
219
- if (dps.fontSize !== undefined && (typeof dps.fontSize !== 'number' || dps.fontSize <= 0 || dps.fontSize > 500 || !isFinite(dps.fontSize))) {
220
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.defaultParagraphStyle.fontSize must be a number > 0 and <= 500');
221
- }
222
- if (dps.lineHeight !== undefined && (typeof dps.lineHeight !== 'number' || dps.lineHeight <= 0 || !isFinite(dps.lineHeight))) {
223
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.defaultParagraphStyle.lineHeight must be a positive finite number');
224
- }
225
- if (dps.color !== undefined && !HEX_COLOR_REGEX.test(dps.color)) {
226
- throw new PretextPdfError('VALIDATION_ERROR', `doc.defaultParagraphStyle.color must be a 6-digit hex string like '#000000'. Got: '${dps.color}'`);
227
- }
228
- if (dps.align !== undefined && !['left', 'center', 'right', 'justify'].includes(dps.align)) {
229
- throw new PretextPdfError('VALIDATION_ERROR', "doc.defaultParagraphStyle.align must be 'left', 'center', 'right', or 'justify'");
230
- }
231
- if (dps.letterSpacing !== undefined && (typeof dps.letterSpacing !== 'number' || dps.letterSpacing < 0 || dps.letterSpacing > 200 || !isFinite(dps.letterSpacing))) {
232
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.defaultParagraphStyle.letterSpacing must be a number >= 0 and <= 200');
233
- }
234
- if (dps.fontWeight !== undefined && ![400, 700].includes(dps.fontWeight)) {
235
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.defaultParagraphStyle.fontWeight must be 400 or 700');
236
- }
237
- }
238
- // sections
239
- if (doc.sections !== undefined) {
240
- if (!Array.isArray(doc.sections)) {
241
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.sections must be an array');
242
- }
243
- for (let i = 0; i < doc.sections.length; i++) {
244
- const s = doc.sections[i];
245
- const label = `doc.sections[${i}]`;
246
- if (s.fromPage !== undefined && (typeof s.fromPage !== 'number' || !Number.isInteger(s.fromPage) || s.fromPage < 1)) {
247
- throw new PretextPdfError('VALIDATION_ERROR', `${label}.fromPage must be a positive integer`);
248
- }
249
- if (s.toPage !== undefined && (typeof s.toPage !== 'number' || !Number.isInteger(s.toPage) || s.toPage < 1)) {
250
- throw new PretextPdfError('VALIDATION_ERROR', `${label}.toPage must be a positive integer`);
251
- }
252
- if (s.fromPage !== undefined && s.toPage !== undefined && s.fromPage > s.toPage) {
253
- throw new PretextPdfError('VALIDATION_ERROR', `${label}.fromPage (${s.fromPage}) must be <= toPage (${s.toPage})`);
254
- }
255
- for (const [spec, slabel] of [[s.header, `${label}.header`], [s.footer, `${label}.footer`]]) {
256
- if (!spec)
257
- continue;
258
- if (typeof spec.text !== 'string') {
259
- throw new PretextPdfError('VALIDATION_ERROR', `${slabel}.text must be a string`);
260
- }
261
- if (spec.fontSize !== undefined && (typeof spec.fontSize !== 'number' || spec.fontSize <= 0 || !isFinite(spec.fontSize))) {
262
- throw new PretextPdfError('VALIDATION_ERROR', `${slabel}.fontSize must be a positive finite number`);
263
- }
264
- if (spec.align !== undefined && !['left', 'center', 'right'].includes(spec.align)) {
265
- throw new PretextPdfError('VALIDATION_ERROR', `${slabel}.align must be 'left', 'center', or 'right'`);
266
- }
267
- if (spec.fontWeight !== undefined && ![400, 700].includes(spec.fontWeight)) {
268
- throw new PretextPdfError('VALIDATION_ERROR', `${slabel}.fontWeight must be 400 or 700`);
269
- }
270
- if (spec.color !== undefined && !HEX_COLOR_REGEX.test(spec.color)) {
271
- throw new PretextPdfError('VALIDATION_ERROR', `${slabel}.color must be a 6-digit hex string like '#666666'. Got: '${spec.color}'`);
272
- }
273
- }
274
- }
275
- }
276
- // watermark
277
- if (doc.watermark) {
278
- const wm = doc.watermark;
279
- if (!wm.text && !wm.image) {
280
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.watermark requires either .text or .image');
281
- }
282
- if (wm.opacity !== undefined && (typeof wm.opacity !== 'number' || wm.opacity < 0 || wm.opacity > 1 || !isFinite(wm.opacity))) {
283
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.watermark.opacity must be a number 0.0–1.0');
284
- }
285
- if (wm.fontSize !== undefined && (typeof wm.fontSize !== 'number' || wm.fontSize <= 0 || wm.fontSize > 500 || !isFinite(wm.fontSize))) {
286
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.watermark.fontSize must be a positive finite number and <= 500');
287
- }
288
- if (wm.fontWeight !== undefined && ![400, 700].includes(wm.fontWeight)) {
289
- throw new PretextPdfError('VALIDATION_ERROR', "doc.watermark.fontWeight must be 400 or 700");
290
- }
291
- if (wm.color !== undefined && !HEX_COLOR_REGEX.test(wm.color)) {
292
- throw new PretextPdfError('VALIDATION_ERROR', `doc.watermark.color must be a 6-digit hex string. Got: '${wm.color}'`);
293
- }
294
- if (wm.rotation !== undefined) {
295
- if (typeof wm.rotation !== 'number' || !isFinite(wm.rotation)) {
296
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.watermark.rotation must be a finite number');
297
- }
298
- if (wm.rotation < -360 || wm.rotation > 360) {
299
- throw new PretextPdfError('WATERMARK_ROTATION_OUT_OF_RANGE', 'doc.watermark.rotation must be between -360 and 360 degrees');
300
- }
301
- }
302
- }
303
- // encryption
304
- if (doc.encryption) {
305
- const enc = doc.encryption;
306
- if (strict) {
307
- assertUnknownProps(enc, ALLOWED_PROPS_SUB['encryption'], 'doc.encryption', errors);
308
- }
309
- if (enc.userPassword !== undefined && typeof enc.userPassword !== 'string') {
310
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.encryption.userPassword must be a string if provided');
311
- }
312
- if (enc.userPassword !== undefined && enc.userPassword === '') {
313
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.encryption.userPassword must not be an empty string — an empty password provides no access control. Omit userPassword for permissions-only encryption.');
314
- }
315
- if (enc.ownerPassword !== undefined && typeof enc.ownerPassword !== 'string') {
316
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.encryption.ownerPassword must be a string if provided');
317
- }
318
- if (enc.ownerPassword !== undefined && enc.ownerPassword === '') {
319
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.encryption.ownerPassword must not be an empty string');
320
- }
321
- // permissions sub-fields are booleans — TypeScript enforces the type, no runtime check needed
322
- }
323
- if (doc.signature !== undefined) {
324
- const sig = doc.signature;
325
- if (sig.width !== undefined && (typeof sig.width !== 'number' || sig.width <= 0)) {
326
- throw new PretextPdfError('VALIDATION_ERROR', 'signature.width must be a positive number');
327
- }
328
- if (sig.height !== undefined && (typeof sig.height !== 'number' || sig.height <= 0)) {
329
- throw new PretextPdfError('VALIDATION_ERROR', 'signature.height must be a positive number');
330
- }
331
- if (sig.page !== undefined && (!Number.isInteger(sig.page) || sig.page < 0)) {
332
- throw new PretextPdfError('VALIDATION_ERROR', 'signature.page must be a non-negative integer');
333
- }
334
- if (sig.borderColor !== undefined && !/^#[0-9A-Fa-f]{6}$/.test(sig.borderColor)) {
335
- throw new PretextPdfError('VALIDATION_ERROR', 'signature.borderColor must be a 6-digit hex color e.g. "#000000"');
336
- }
337
- if (sig.fontSize !== undefined && (typeof sig.fontSize !== 'number' || sig.fontSize <= 0)) {
338
- throw new PretextPdfError('VALIDATION_ERROR', 'signature.fontSize must be a positive number');
339
- }
340
- // Crypto signature validation
341
- if (sig.p12 !== undefined) {
342
- if (typeof sig.p12 !== 'string' && !(sig.p12 instanceof Uint8Array)) {
343
- throw new PretextPdfError('VALIDATION_ERROR', 'signature.p12 must be a file path string or Uint8Array of certificate bytes');
344
- }
345
- if (typeof sig.p12 === 'string' && sig.p12.trim() === '') {
346
- throw new PretextPdfError('VALIDATION_ERROR', 'signature.p12 must not be an empty string');
347
- }
348
- }
349
- if (sig.passphrase !== undefined && typeof sig.passphrase !== 'string') {
350
- throw new PretextPdfError('VALIDATION_ERROR', 'signature.passphrase must be a string');
351
- }
352
- if (sig.contactInfo !== undefined && typeof sig.contactInfo !== 'string') {
353
- throw new PretextPdfError('VALIDATION_ERROR', 'signature.contactInfo must be a string');
354
- }
355
- if (sig.invisible !== undefined && typeof sig.invisible !== 'boolean') {
356
- throw new PretextPdfError('VALIDATION_ERROR', 'signature.invisible must be a boolean');
357
- }
358
- if (sig.p12 !== undefined && doc.encryption !== undefined) {
359
- throw new PretextPdfError('SIGNATURE_CERT_AND_ENCRYPTION', 'Cannot use both signature.p12 (cryptographic signing) and encryption together — the encryption step would invalidate the cryptographic signature.');
360
- }
361
- }
362
- // bookmarks
363
- if (doc.bookmarks !== undefined && doc.bookmarks !== false) {
364
- const bm = doc.bookmarks;
365
- if (bm.minLevel !== undefined && ![1, 2, 3, 4].includes(bm.minLevel)) {
366
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.bookmarks.minLevel must be 1, 2, 3, or 4');
367
- }
368
- if (bm.maxLevel !== undefined && ![1, 2, 3, 4].includes(bm.maxLevel)) {
369
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.bookmarks.maxLevel must be 1, 2, 3, or 4');
370
- }
371
- if (bm.minLevel !== undefined && bm.maxLevel !== undefined && bm.minLevel > bm.maxLevel) {
372
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.bookmarks.minLevel must be ≤ maxLevel');
373
- }
374
- }
375
- // hyphenation
376
- if (doc.hyphenation) {
377
- const h = doc.hyphenation;
378
- if (!h.language || typeof h.language !== 'string') {
379
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.hyphenation.language is required (e.g. "en-us")');
380
- }
381
- if (!LANGUAGE_TAG_REGEX.test(h.language)) {
382
- throw new PretextPdfError('VALIDATION_ERROR', `doc.hyphenation.language must be a BCP47 tag like "en-us" or "de" (letters and hyphens only). Got: "${h.language}"`);
383
- }
384
- if (h.minWordLength !== undefined && (h.minWordLength < 2 || h.minWordLength > 20)) {
385
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.hyphenation.minWordLength must be 2–20');
386
- }
387
- if (h.leftMin !== undefined && (h.leftMin < 1 || h.leftMin > 5)) {
388
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.hyphenation.leftMin must be 1–5');
389
- }
390
- if (h.rightMin !== undefined && (h.rightMin < 1 || h.rightMin > 5)) {
391
- throw new PretextPdfError('VALIDATION_ERROR', 'doc.hyphenation.rightMin must be 1–5');
392
- }
393
- }
394
- // metadata
395
- if (doc.metadata) {
396
- const m = doc.metadata;
397
- if (m.language !== undefined && (typeof m.language !== 'string' || m.language.trim() === '')) {
398
- throw new PretextPdfError('VALIDATION_ERROR', 'metadata.language must be a non-empty string (BCP47 tag e.g. "en-US")');
399
- }
400
- if (m.language !== undefined && typeof m.language === 'string')
401
- validateMetadataString(m.language, 'language');
402
- if (m.producer !== undefined && (typeof m.producer !== 'string' || m.producer.trim() === '')) {
403
- throw new PretextPdfError('VALIDATION_ERROR', 'metadata.producer must be a non-empty string');
404
- }
405
- // Validate free-text fields for injection chars and length
406
- for (const field of ['title', 'author', 'subject', 'keywords', 'creator', 'producer']) {
407
- const val = m[field];
408
- if (val !== undefined && typeof val === 'string')
409
- validateMetadataString(val, field);
410
- }
411
- }
412
- // validate each content element
413
- const loadedFamilies = new Set([
414
- ...BUNDLED_FAMILIES,
415
- ...(doc.fonts ?? []).map(f => f.family),
416
- ]);
417
- // Check for duplicate form field names
418
- const formFieldNames = new Set();
419
- for (const el of doc.content) {
420
- if (el.type === 'form-field') {
421
- if (formFieldNames.has(el.name)) {
422
- throw new PretextPdfError('FORM_FIELD_NAME_DUPLICATE', `Duplicate form field name: "${el.name}". Each form field must have a unique name.`);
423
- }
424
- formFieldNames.add(el.name);
425
- }
426
- }
427
- for (let i = 0; i < doc.content.length; i++) {
428
- validateElement(doc.content[i], i, loadedFamilies, strict, errors, options);
429
- }
430
- // ── Footnote ref/def cross-validation ─────────────────────────────────────
431
- const footnoteDefIds = new Map(); // id → content index
432
- const footnoteRefIds = new Set();
433
- // Collect all def ids
434
- for (let i = 0; i < doc.content.length; i++) {
435
- const el = doc.content[i];
436
- if (el.type === 'footnote-def') {
437
- if (footnoteDefIds.has(el.id)) {
438
- throw new PretextPdfError('FOOTNOTE_DEF_DUPLICATE', `content[${i}] (footnote-def): duplicate id "${el.id}". Each footnote must have a unique id.`);
439
- }
440
- footnoteDefIds.set(el.id, i);
441
- }
442
- }
443
- // Collect all ref ids from rich-paragraph spans
444
- for (const el of doc.content) {
445
- if (el.type === 'rich-paragraph') {
446
- for (const span of el.spans) {
447
- if (span.footnoteRef) {
448
- footnoteRefIds.add(span.footnoteRef);
449
- }
450
- }
451
- }
452
- }
453
- // Orphaned ref: ref id with no matching def
454
- for (const refId of footnoteRefIds) {
455
- if (!footnoteDefIds.has(refId)) {
456
- throw new PretextPdfError('FOOTNOTE_REF_ORPHANED', `A rich-paragraph span references footnote id "${refId}" but no footnote-def with that id exists in doc.content.`);
457
- }
458
- }
459
- // Orphaned def: def id never referenced
460
- for (const [defId] of footnoteDefIds) {
461
- if (!footnoteRefIds.has(defId)) {
462
- throw new PretextPdfError('FOOTNOTE_DEF_ORPHANED', `footnote-def "${defId}" is defined but never referenced by any rich-paragraph span.`);
463
- }
464
- }
465
- // validate all font references are loadable
466
- validateFontReferences(doc, loadedFamilies);
467
- // Throw collected strict validation errors
468
- if (errors.length > 0) {
469
- const msg = formatErrors(errors);
470
- throw new PretextPdfError('VALIDATION_ERROR', msg);
471
- }
472
- }
473
- /**
474
- * Validate a pretext-pdf document and return a structured result instead of throwing.
475
- *
476
- * Use this when you want to inspect all validation errors programmatically.
477
- * The existing {@link validate} function throws on first error and is unchanged.
478
- *
479
- * @param doc - The document to validate (typed as `unknown` to accept unverified input)
480
- * @param options - `{ strict?: boolean; logger?: Logger }` — strict defaults to false (matches render() behavior); logger routes diagnostic warnings away from console.warn
481
- * @returns {@link ValidationResult} with `valid`, `errors[]`, and `errorCount`
482
- * @public
483
- */
484
- export function validateDocument(doc, options) {
485
- try {
486
- validate(doc, { strict: options?.strict ?? false, ...(options?.logger !== undefined ? { logger: options.logger } : {}) });
487
- return { valid: true, errors: [], errorCount: 0, warningCount: 0 };
488
- }
489
- catch (err) {
490
- if (err instanceof PretextPdfError) {
491
- const errors = parseValidationErrorsStructured(err.message, err.code);
492
- const warnings = errors.filter((e) => e.severity === 'warning');
493
- const warningCount = warnings.length;
494
- const countMatch = err.message.match(/Strict validation failed \((\d+) issue/);
495
- const errorCount = countMatch?.[1] != null ? parseInt(countMatch[1], 10) : errors.length;
496
- return { valid: false, errors, errorCount, warningCount };
497
- }
498
- const msg = err instanceof Error ? err.message : String(err);
499
- return { valid: false, errors: [{ path: 'document', message: `Unexpected validation error: ${msg}`, severity: 'error', code: 'VALIDATION_ERROR' }], errorCount: 1, warningCount: 0 };
500
- }
501
- }
502
- /** Parse a PretextPdfError message into structured ValidationError entries */
503
- function parseValidationErrorsStructured(message, code) {
504
- if (message.startsWith('Strict validation failed')) {
505
- const lines = message
506
- .split('\n')
507
- .slice(1) // skip "Strict validation failed (N issues):" header
508
- .filter(l => Boolean(l.trim()) && !l.startsWith('...'));
509
- return lines.map(line => {
510
- const colonIdx = line.indexOf(':');
511
- const path = colonIdx > 0 ? line.slice(0, colonIdx).trim() : 'document';
512
- const rest = colonIdx > 0 ? line.slice(colonIdx + 1).trim() : line.trim();
513
- const suggMatch = rest.match(/did you mean "([^"]+)"/);
514
- const suggestion = suggMatch ? suggMatch[1] : undefined;
515
- const isUnknown = rest.includes('unknown property');
516
- const unknownProp = isUnknown ? (/\.([^.[]+)$/.exec(path)?.[1] ?? path) : undefined;
517
- return {
518
- path,
519
- message: rest,
520
- code: 'UNKNOWN_PROPERTY',
521
- severity: 'error',
522
- ...(unknownProp !== undefined && { unknownProp }),
523
- ...(suggestion !== undefined && { suggestion }),
524
- };
525
- });
526
- }
527
- const colonIdx = message.indexOf(':');
528
- // A valid path (e.g. "content[0] (paragraph) spans[0].href") never contains ". " (period + space).
529
- // Prose fragments like "margins.left must be a non-negative finite number. Got" do, so we reject them.
530
- const candidate = message.slice(0, colonIdx).trim();
531
- const hasPathPrefix = colonIdx > 0 && !/\. /.test(candidate);
532
- const path = hasPathPrefix ? candidate : 'document';
533
- const msgText = hasPathPrefix ? message.slice(colonIdx + 1).trim() : message;
534
- return [{
535
- path,
536
- message: msgText,
537
- code: code,
538
- severity: 'error',
539
- }];
540
- }
541
- /**
542
- * Validate that every font family referenced anywhere in the document
543
- * is either bundled (Inter) or present in doc.fonts.
544
- * Catches problems early instead of silently falling back or dropping content.
545
- */
546
- function validateFontReferences(doc, loadedFamilies) {
547
- const defaultFamily = doc.defaultFont ?? 'Inter';
548
- // Build a variant-level set for italic checks: "Family-weight-style"
549
- const loadedVariants = new Set(BUNDLED_VARIANTS);
550
- for (const f of doc.fonts ?? []) {
551
- loadedVariants.add(`${f.family}-${f.weight ?? 400}-${f.style ?? 'normal'}`);
552
- }
553
- const requireFamily = (family, context) => {
554
- if (!/^[a-zA-Z0-9 ._+\-]+$/.test(family)) {
555
- throw new PretextPdfError('VALIDATION_ERROR', `${context}: font family name "${family}" contains invalid characters. Use only letters, digits, spaces, hyphens, and underscores.`);
556
- }
557
- if (!loadedFamilies.has(family)) {
558
- throw new PretextPdfError('FONT_NOT_LOADED', `${context}: font family '${family}' is not loaded. Add { family: '${family}', src: '/path/to.ttf' } to doc.fonts, or remove the fontFamily reference to use the default ('${defaultFamily}').`);
559
- }
560
- };
561
- const requireVariant = (family, weight, style, context) => {
562
- const key = `${family}-${weight}-${style}`;
563
- if (!loadedVariants.has(key)) {
564
- if (style === 'italic') {
565
- throw new PretextPdfError('ITALIC_FONT_NOT_LOADED', `${context}: fontStyle 'italic' requires an italic font variant. Add { family: '${family}', weight: ${weight}, style: 'italic', src: '/path/to-italic.ttf' } to doc.fonts.`);
566
- }
567
- throw new PretextPdfError('FONT_NOT_LOADED', `${context}: font variant '${key}' is not loaded. Add a matching FontSpec to doc.fonts.`);
568
- }
569
- };
570
- // 1. defaultFont must be loadable
571
- requireFamily(defaultFamily, `doc.defaultFont '${defaultFamily}'`);
572
- // 2. header / footer fontFamily + fontWeight variant
573
- for (const [spec, label] of [[doc.header, 'doc.header'], [doc.footer, 'doc.footer']]) {
574
- if (!spec)
575
- continue;
576
- if (spec.fontFamily)
577
- requireFamily(spec.fontFamily, `${label}.fontFamily`);
578
- if ((spec.fontWeight ?? 400) === 700) {
579
- const family = spec.fontFamily ?? defaultFamily;
580
- requireVariant(family, 700, 'normal', `${label} fontWeight:700`);
581
- }
582
- }
583
- // 2b. watermark fontFamily (text watermark only)
584
- if (doc.watermark?.text) {
585
- if (doc.watermark.fontFamily)
586
- requireFamily(doc.watermark.fontFamily, 'doc.watermark.fontFamily');
587
- if ((doc.watermark.fontWeight ?? 400) === 700) {
588
- const family = doc.watermark.fontFamily ?? defaultFamily;
589
- requireVariant(family, 700, 'normal', 'doc.watermark fontWeight:700');
590
- }
591
- }
592
- // 3. content elements
593
- for (let i = 0; i < doc.content.length; i++) {
594
- const el = doc.content[i];
595
- const prefix = `content[${i}]`;
596
- if (el.type === 'paragraph') {
597
- if (el.fontFamily)
598
- requireFamily(el.fontFamily, `${prefix} (paragraph).fontFamily`);
599
- if ((el.fontWeight ?? 400) === 700) {
600
- const family = el.fontFamily ?? defaultFamily;
601
- requireVariant(family, 700, 'normal', `${prefix} (paragraph) fontWeight:700`);
602
- }
603
- }
604
- if (el.type === 'heading') {
605
- if (el.fontFamily)
606
- requireFamily(el.fontFamily, `${prefix} (heading).fontFamily`);
607
- const family = el.fontFamily ?? defaultFamily;
608
- const weight = el.fontWeight ?? 700;
609
- requireVariant(family, weight, 'normal', `${prefix} (heading) fontWeight:${weight}`);
610
- }
611
- if (el.type === 'list') {
612
- for (let ii = 0; ii < el.items.length; ii++) {
613
- const item = el.items[ii];
614
- if ((item.fontWeight ?? 400) === 700) {
615
- requireVariant(defaultFamily, 700, 'normal', `${prefix} (list) items[${ii}] fontWeight:700`);
616
- }
617
- }
618
- }
619
- if (el.type === 'table') {
620
- for (let ri = 0; ri < el.rows.length; ri++) {
621
- for (let ci = 0; ci < el.rows[ri].cells.length; ci++) {
622
- const cell = el.rows[ri].cells[ci];
623
- if (cell.fontFamily)
624
- requireFamily(cell.fontFamily, `${prefix} (table) rows[${ri}].cells[${ci}].fontFamily`);
625
- if ((cell.fontWeight ?? 400) === 700) {
626
- const family = cell.fontFamily ?? defaultFamily;
627
- requireVariant(family, 700, 'normal', `${prefix} (table) rows[${ri}].cells[${ci}] fontWeight:700`);
628
- }
629
- }
630
- }
631
- }
632
- if (el.type === 'rich-paragraph') {
633
- for (let si = 0; si < el.spans.length; si++) {
634
- const span = el.spans[si];
635
- const spanFamily = span.fontFamily ?? defaultFamily;
636
- const spanWeight = span.fontWeight ?? 400;
637
- const spanStyle = span.fontStyle ?? 'normal';
638
- if (span.fontFamily)
639
- requireFamily(span.fontFamily, `${prefix} (rich-paragraph) spans[${si}].fontFamily`);
640
- if (spanStyle === 'italic') {
641
- requireVariant(spanFamily, spanWeight, 'italic', `${prefix} (rich-paragraph) spans[${si}]`);
642
- }
643
- }
644
- }
645
- if (el.type === 'blockquote') {
646
- if (el.fontFamily)
647
- requireFamily(el.fontFamily, `${prefix} (blockquote).fontFamily`);
648
- const family = el.fontFamily ?? defaultFamily;
649
- const weight = el.fontWeight ?? 400;
650
- const style = el.fontStyle ?? 'normal';
651
- if (style === 'italic') {
652
- requireVariant(family, weight, 'italic', `${prefix} (blockquote) fontStyle:italic`);
653
- }
654
- else if (weight === 700) {
655
- requireVariant(family, 700, 'normal', `${prefix} (blockquote) fontWeight:700`);
656
- }
657
- }
658
- // code.fontFamily already validated against loadedFamilies in validateElement
659
- }
660
- }
661
- function validateElement(el, index, loadedFamilies, strict, errors, options) {
662
- const prefix = `content[${index}]`;
663
- if (!el || typeof el !== 'object' || !('type' in el)) {
664
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix}: each element must have a 'type' field`);
665
- }
666
- // Strict: check element properties match allowed set for type
667
- if (strict) {
668
- const allowed = ALLOWED_PROPS[el.type];
669
- if (allowed) {
670
- assertUnknownProps(el, allowed, prefix, errors);
671
- }
672
- }
673
- switch (el.type) {
674
- case 'paragraph': {
675
- if (typeof el.text !== 'string') {
676
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'text' must be a string`);
677
- }
678
- // NEW: Validate dir field
679
- if (el.dir !== undefined && !['ltr', 'rtl', 'auto'].includes(el.dir)) {
680
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'dir' must be 'ltr', 'rtl', or 'auto'`);
681
- }
682
- if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
683
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'color' must be a 6-digit hex string like '#ff0000'. Got: '${el.color}'`);
684
- }
685
- if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize) || el.fontSize > 500)) {
686
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'fontSize' must be a positive finite number and <= 500`);
687
- }
688
- if (el.lineHeight !== undefined && typeof el.lineHeight === 'number') {
689
- // Compare against explicit fontSize if set, or default (12pt) if not
690
- const effectiveFontSize = el.fontSize ?? 12;
691
- if (el.lineHeight < effectiveFontSize) {
692
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): lineHeight (${el.lineHeight}) is less than fontSize (${effectiveFontSize}). Lines would overlap. Set lineHeight >= fontSize.`);
693
- }
694
- }
695
- if (el.bgColor !== undefined && !HEX_COLOR_REGEX.test(el.bgColor)) {
696
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'bgColor' must be a 6-digit hex string like '#f0f0f0'. Got: '${el.bgColor}'`);
697
- }
698
- if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
699
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'spaceAfter' must be a non-negative finite number`);
700
- }
701
- if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
702
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'spaceBefore' must be a non-negative finite number`);
703
- }
704
- if (el.columns !== undefined && (typeof el.columns !== 'number' || el.columns < 1 || !Number.isInteger(el.columns) || el.columns > 6)) {
705
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'columns' must be a positive integer between 1 and 6`);
706
- }
707
- if (el.columnGap !== undefined && (typeof el.columnGap !== 'number' || el.columnGap < 0 || !isFinite(el.columnGap))) {
708
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'columnGap' must be a non-negative finite number`);
709
- }
710
- if (el.align !== undefined && !['left', 'center', 'right', 'justify'].includes(el.align)) {
711
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'align' must be 'left', 'center', 'right', or 'justify'`);
712
- }
713
- if (el.url !== undefined && (typeof el.url !== 'string' || el.url.trim() === '')) {
714
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'url' must be a non-empty string if provided`);
715
- }
716
- if (el.url !== undefined && typeof el.url === 'string')
717
- validateUrl(el.url, `${prefix} (paragraph) url`);
718
- if (el.letterSpacing !== undefined && (typeof el.letterSpacing !== 'number' || el.letterSpacing < 0 || !isFinite(el.letterSpacing) || el.letterSpacing > 200)) {
719
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): 'letterSpacing' must be a non-negative finite number and <= 200`);
720
- }
721
- if (el.annotation) {
722
- // Strict: validate annotation properties
723
- if (strict) {
724
- assertUnknownProps(el.annotation, ALLOWED_PROPS_SUB['annotation'], `${prefix}.annotation`, errors);
725
- }
726
- if (!el.annotation.contents || el.annotation.contents.trim() === '') {
727
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): annotation.contents is required and must be non-empty`);
728
- }
729
- if (el.annotation.contents.length > 5000) {
730
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): annotation.contents must be 5000 characters or fewer`);
731
- }
732
- if (el.annotation.color !== undefined && !/^#[0-9a-fA-F]{6}$/.test(el.annotation.color)) {
733
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): annotation.color must be a 6-digit hex color (e.g. "#FFFF00"). Got: "${el.annotation.color}"`);
734
- }
735
- if (el.annotation.author !== undefined && el.annotation.author.length > 100) {
736
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (paragraph): annotation.author must be 100 characters or fewer`);
737
- }
738
- }
739
- break;
740
- }
741
- case 'heading': {
742
- if (typeof el.text !== 'string') {
743
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'text' must be a string`);
744
- }
745
- if (el.text.trim() === '') {
746
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): heading text cannot be empty or whitespace-only`);
747
- }
748
- if (![1, 2, 3, 4].includes(el.level)) {
749
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'level' must be 1, 2, 3, or 4. Got: ${el.level}`);
750
- }
751
- if (el.fontWeight !== undefined && ![400, 700].includes(el.fontWeight)) {
752
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'fontWeight' must be 400 or 700`);
753
- }
754
- if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize) || el.fontSize > 500)) {
755
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'fontSize' must be a positive finite number and <= 500`);
756
- }
757
- if (el.lineHeight !== undefined && typeof el.lineHeight === 'number') {
758
- const effectiveFontSize = el.fontSize ?? 12;
759
- if (el.lineHeight < effectiveFontSize) {
760
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): lineHeight (${el.lineHeight}) is less than fontSize (${effectiveFontSize}). Lines would overlap.`);
761
- }
762
- }
763
- if (el.align !== undefined && !['left', 'center', 'right', 'justify'].includes(el.align)) {
764
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'align' must be 'left', 'center', 'right', or 'justify'`);
765
- }
766
- if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
767
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'color' must be a 6-digit hex string like '#ff0000'. Got: '${el.color}'`);
768
- }
769
- if (el.bgColor !== undefined && !HEX_COLOR_REGEX.test(el.bgColor)) {
770
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'bgColor' must be a 6-digit hex string`);
771
- }
772
- if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
773
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'spaceBefore' must be a non-negative finite number`);
774
- }
775
- if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
776
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'spaceAfter' must be a non-negative finite number`);
777
- }
778
- if (el.url !== undefined && (typeof el.url !== 'string' || el.url.trim() === '')) {
779
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'url' must be a non-empty string if provided`);
780
- }
781
- if (el.url !== undefined && typeof el.url === 'string')
782
- validateUrl(el.url, `${prefix} (heading) url`);
783
- if (el.anchor !== undefined && (typeof el.anchor !== 'string' || !/^[a-zA-Z0-9_-]+$/.test(el.anchor))) {
784
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'anchor' must be alphanumeric with hyphens/underscores only. Got: '${el.anchor}'`);
785
- }
786
- if (el.letterSpacing !== undefined && (typeof el.letterSpacing !== 'number' || el.letterSpacing < 0 || !isFinite(el.letterSpacing) || el.letterSpacing > 200)) {
787
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): 'letterSpacing' must be a non-negative finite number and <= 200`);
788
- }
789
- if (el.annotation) {
790
- if (!el.annotation.contents || el.annotation.contents.trim() === '') {
791
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): annotation.contents is required and must be non-empty`);
792
- }
793
- if (el.annotation.contents.length > 5000) {
794
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): annotation.contents must be 5000 characters or fewer`);
795
- }
796
- if (el.annotation.color !== undefined && !/^#[0-9a-fA-F]{6}$/.test(el.annotation.color)) {
797
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): annotation.color must be a 6-digit hex color (e.g. "#FFFF00"). Got: "${el.annotation.color}"`);
798
- }
799
- if (el.annotation.author !== undefined && el.annotation.author.length > 100) {
800
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (heading): annotation.author must be 100 characters or fewer`);
801
- }
802
- }
803
- break;
804
- }
805
- case 'spacer': {
806
- if (typeof el.height !== 'number' || el.height < 0 || el.height > 14400 || !isFinite(el.height)) {
807
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (spacer): 'height' must be a non-negative finite number and <= 14400pt (200 inches)`);
808
- }
809
- break;
810
- }
811
- case 'table': {
812
- if (!Array.isArray(el.columns) || el.columns.length === 0) {
813
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'columns' must be a non-empty array`);
814
- }
815
- if (!Array.isArray(el.rows) || el.rows.length === 0) {
816
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'rows' must be a non-empty array`);
817
- }
818
- const colCount = el.columns.length;
819
- for (let ci = 0; ci < el.columns.length; ci++) {
820
- const col = el.columns[ci];
821
- // Strict: validate column def properties
822
- if (strict) {
823
- assertUnknownProps(col, ALLOWED_PROPS_SUB['column-def'], `${prefix} (table).columns[${ci}]`, errors);
824
- }
825
- if (typeof col.width === 'number') {
826
- if (col.width <= 0 || !isFinite(col.width)) {
827
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): columns[${ci}].width must be a positive number. Got: ${col.width}`);
828
- }
829
- }
830
- else if (typeof col.width === 'string') {
831
- if (col.width !== 'auto' && !STAR_WIDTH_REGEX.test(col.width)) {
832
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): columns[${ci}].width must be a positive number, proportional string like '2*' or '*', or 'auto'. Got: '${col.width}'`);
833
- }
834
- if (col.width !== 'auto') {
835
- const multiplier = parseFloat(col.width);
836
- if (!isNaN(multiplier) && multiplier > 1000) {
837
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): columns[${ci}].width multiplier ${multiplier} exceeds maximum 1000`);
838
- }
839
- }
840
- }
841
- else {
842
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): columns[${ci}].width must be a number or string like '2*' or 'auto'`);
843
- }
844
- if (col.align !== undefined && !['left', 'center', 'right'].includes(col.align)) {
845
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): columns[${ci}].align must be 'left', 'center', or 'right'`);
846
- }
847
- }
848
- // Validate header row count
849
- const headerRowCount = el.headerRows !== undefined
850
- ? el.headerRows
851
- : el.rows.filter(r => r.isHeader).length;
852
- if (headerRowCount > el.rows.length) {
853
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): headerRows (${headerRowCount}) exceeds total row count (${el.rows.length})`);
854
- }
855
- // Build span occupancy grid for colspan validation (rowspan cells occupy future rows)
856
- const spanOccupied = new Set();
857
- for (let ri = 0; ri < el.rows.length; ri++) {
858
- const row = el.rows[ri];
859
- let ci = 0;
860
- for (const cell of row.cells) {
861
- while (spanOccupied.has(`${ri},${ci}`))
862
- ci++;
863
- const cs = cell.colspan ?? 1;
864
- const rs = cell.rowspan ?? 1;
865
- for (let r2 = ri + 1; r2 < ri + rs; r2++) {
866
- for (let c2 = ci; c2 < ci + cs; c2++) {
867
- spanOccupied.add(`${r2},${c2}`);
868
- }
869
- }
870
- ci += cs;
871
- }
872
- }
873
- for (let ri = 0; ri < el.rows.length; ri++) {
874
- const row = el.rows[ri];
875
- if (!Array.isArray(row.cells)) {
876
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells must be an array`);
877
- }
878
- // Count how many columns in this row are occupied by rowspan cells from above
879
- let occupiedCols = 0;
880
- for (let ci = 0; ci < colCount; ci++) {
881
- if (spanOccupied.has(`${ri},${ci}`))
882
- occupiedCols++;
883
- }
884
- // Validate colspan sum equals colCount minus occupied columns
885
- let colspanSum = 0;
886
- for (let cellI = 0; cellI < row.cells.length; cellI++) {
887
- const cell = row.cells[cellI];
888
- const cs = cell.colspan ?? 1;
889
- if (typeof cs !== 'number' || cs < 1 || !Number.isInteger(cs)) {
890
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].colspan must be a positive integer`);
891
- }
892
- colspanSum += cs;
893
- const rs = cell.rowspan ?? 1;
894
- if (typeof rs !== 'number' || rs < 1 || !Number.isInteger(rs)) {
895
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].rowspan must be a positive integer`);
896
- }
897
- }
898
- if (colspanSum !== colCount - occupiedCols) {
899
- throw new PretextPdfError('COLSPAN_OVERFLOW', `${prefix} (table): rows[${ri}] colspan sum is ${colspanSum} but expected ${colCount - occupiedCols} (${colCount} columns minus ${occupiedCols} occupied by rowspan). Sum of explicit cell colspans must cover only unoccupied columns.`);
900
- }
901
- // Strict: validate row and column defs
902
- if (strict) {
903
- assertUnknownProps(row, ALLOWED_PROPS_SUB['table-row'], `${prefix}.rows[${ri}]`, errors);
904
- }
905
- for (let cellI = 0; cellI < row.cells.length; cellI++) {
906
- const cell = row.cells[cellI];
907
- // Strict: validate cell properties
908
- if (strict) {
909
- assertUnknownProps(cell, ALLOWED_PROPS_SUB['table-cell'], `${prefix}.rows[${ri}].cells[${cellI}]`, errors);
910
- }
911
- if (typeof cell.text !== 'string') {
912
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].text must be a string`);
913
- }
914
- if (cell.fontFamily !== undefined && (typeof cell.fontFamily !== 'string' || cell.fontFamily.trim() === '')) {
915
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].fontFamily must be a non-empty string`);
916
- }
917
- if (cell.fontWeight !== undefined && ![400, 700].includes(cell.fontWeight)) {
918
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].fontWeight must be 400 or 700`);
919
- }
920
- if (cell.fontSize !== undefined && (typeof cell.fontSize !== 'number' || cell.fontSize <= 0 || !isFinite(cell.fontSize))) {
921
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].fontSize must be a positive finite number`);
922
- }
923
- if (cell.color !== undefined && !HEX_COLOR_REGEX.test(cell.color)) {
924
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].color must be a 6-digit hex string`);
925
- }
926
- if (cell.bgColor !== undefined && !HEX_COLOR_REGEX.test(cell.bgColor)) {
927
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].bgColor must be a 6-digit hex string`);
928
- }
929
- if (cell.dir !== undefined && !['ltr', 'rtl', 'auto'].includes(cell.dir)) {
930
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].dir must be 'ltr', 'rtl', or 'auto'`);
931
- }
932
- if (cell.align !== undefined && !['left', 'center', 'right'].includes(cell.align)) {
933
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): rows[${ri}].cells[${cellI}].align must be 'left', 'center', or 'right'`);
934
- }
935
- }
936
- }
937
- if (el.dir !== undefined && !['ltr', 'rtl', 'auto'].includes(el.dir)) {
938
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'dir' must be 'ltr', 'rtl', or 'auto'`);
939
- }
940
- if (el.borderColor !== undefined && !HEX_COLOR_REGEX.test(el.borderColor)) {
941
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'borderColor' must be a 6-digit hex string`);
942
- }
943
- if (el.headerBgColor !== undefined && !HEX_COLOR_REGEX.test(el.headerBgColor)) {
944
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'headerBgColor' must be a 6-digit hex string`);
945
- }
946
- if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
947
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'fontSize' must be a positive finite number`);
948
- }
949
- if (el.borderWidth !== undefined && (typeof el.borderWidth !== 'number' || el.borderWidth < 0 || el.borderWidth > 50)) {
950
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'borderWidth' must be a non-negative number <= 50`);
951
- }
952
- if (el.cellPaddingH !== undefined && (typeof el.cellPaddingH !== 'number' || el.cellPaddingH < 0 || el.cellPaddingH > 200)) {
953
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'cellPaddingH' must be a non-negative number <= 200`);
954
- }
955
- if (el.cellPaddingV !== undefined && (typeof el.cellPaddingV !== 'number' || el.cellPaddingV < 0 || el.cellPaddingV > 200)) {
956
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'cellPaddingV' must be a non-negative number <= 200`);
957
- }
958
- if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
959
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'spaceAfter' must be a non-negative finite number`);
960
- }
961
- if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
962
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (table): 'spaceBefore' must be a non-negative finite number`);
963
- }
964
- break;
965
- }
966
- case 'image': {
967
- if (!el.src || (typeof el.src !== 'string' && !(el.src instanceof Uint8Array))) {
968
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'src' must be a non-empty string path or Uint8Array`);
969
- }
970
- if (typeof el.src === 'string' && (el.src.startsWith('\\\\') || el.src.startsWith('//'))) {
971
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'src' must not be a UNC/network path`);
972
- }
973
- const fmt = el.format ?? 'auto';
974
- if (fmt !== 'png' && fmt !== 'jpg' && fmt !== 'auto') {
975
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'format' must be 'png', 'jpg', or 'auto'. Got: '${String(el.format)}'`);
976
- }
977
- if (el.width !== undefined && (typeof el.width !== 'number' || el.width <= 0 || !isFinite(el.width))) {
978
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'width' must be a positive finite number`);
979
- }
980
- if (el.height !== undefined && (typeof el.height !== 'number' || el.height <= 0 || !isFinite(el.height))) {
981
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'height' must be a positive finite number`);
982
- }
983
- if (el.align !== undefined && !['left', 'center', 'right'].includes(el.align)) {
984
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'align' must be 'left', 'center', or 'right'`);
985
- }
986
- if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
987
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'spaceAfter' must be a non-negative finite number`);
988
- }
989
- if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
990
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'spaceBefore' must be a non-negative finite number`);
991
- }
992
- // Float validation
993
- if (el.float !== undefined && el.float !== 'left' && el.float !== 'right') {
994
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'float' must be 'left' or 'right'`);
995
- }
996
- if (el.float !== undefined && (!el.floatText || el.floatText.trim() === '') && (!el.floatSpans || el.floatSpans.length === 0)) {
997
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatText' or 'floatSpans' is required when 'float' is set`);
998
- }
999
- if (el.floatText !== undefined && el.floatSpans !== undefined) {
1000
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatText' and 'floatSpans' are mutually exclusive — use one or the other`);
1001
- }
1002
- if (el.floatText !== undefined && el.float === undefined) {
1003
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatText' has no effect without 'float'`);
1004
- }
1005
- if (el.floatSpans !== undefined && el.float === undefined) {
1006
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatSpans' has no effect without 'float'`);
1007
- }
1008
- if (el.floatWidth !== undefined && (typeof el.floatWidth !== 'number' || el.floatWidth <= 0 || !isFinite(el.floatWidth))) {
1009
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatWidth' must be a positive finite number`);
1010
- }
1011
- if (el.floatGap !== undefined && (typeof el.floatGap !== 'number' || el.floatGap < 0 || !isFinite(el.floatGap))) {
1012
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatGap' must be a non-negative number`);
1013
- }
1014
- if (el.floatFontSize !== undefined && (typeof el.floatFontSize !== 'number' || el.floatFontSize <= 0)) {
1015
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (image): 'floatFontSize' must be a positive number`);
1016
- }
1017
- break;
1018
- }
1019
- case 'svg': {
1020
- const hasSvg = typeof el.svg === 'string' && el.svg.trim().length > 0;
1021
- const hasSrc = typeof el.src === 'string' && el.src.trim().length > 0;
1022
- if (!hasSvg && !hasSrc) {
1023
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): either 'svg' (inline markup) or 'src' (file path / https:// URL) is required`);
1024
- }
1025
- if (hasSvg && !el.svg.trim().startsWith('<')) {
1026
- throw new PretextPdfError('SVG_INVALID_MARKUP', `${prefix} (svg): 'svg' must be valid SVG markup (must start with '<')`);
1027
- }
1028
- if (hasSrc) {
1029
- const src = el.src;
1030
- const isUNC = src.startsWith('\\\\') || src.startsWith('//');
1031
- if (isUNC) {
1032
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'src' must not be a UNC/network path`);
1033
- }
1034
- if (!src.startsWith('/') && !src.startsWith('https://') && !/^[A-Za-z]:[/\\]/.test(src)) {
1035
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'src' must be an absolute file path or an https:// URL`);
1036
- }
1037
- }
1038
- if (el.width !== undefined && (typeof el.width !== 'number' || el.width <= 0 || !isFinite(el.width))) {
1039
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'width' must be a positive finite number`);
1040
- }
1041
- if (el.height !== undefined && (typeof el.height !== 'number' || el.height <= 0 || !isFinite(el.height))) {
1042
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'height' must be a positive finite number`);
1043
- }
1044
- if (el.align !== undefined && !['left', 'center', 'right'].includes(el.align)) {
1045
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'align' must be 'left', 'center', or 'right'`);
1046
- }
1047
- if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
1048
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'spaceAfter' must be a non-negative finite number`);
1049
- }
1050
- if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
1051
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (svg): 'spaceBefore' must be a non-negative finite number`);
1052
- }
1053
- break;
1054
- }
1055
- case 'qr-code': {
1056
- if (typeof el.data !== 'string' || el.data.trim().length === 0) {
1057
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'data' must be a non-empty string`);
1058
- }
1059
- if (el.data.length > 2953) {
1060
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'data' exceeds maximum QR capacity of 2953 characters (got ${el.data.length})`);
1061
- }
1062
- if (el.errorCorrectionLevel !== undefined && !['L', 'M', 'Q', 'H'].includes(el.errorCorrectionLevel)) {
1063
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'errorCorrectionLevel' must be 'L', 'M', 'Q', or 'H'`);
1064
- }
1065
- if (el.size !== undefined && (typeof el.size !== 'number' || el.size <= 0 || !isFinite(el.size))) {
1066
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'size' must be a positive finite number`);
1067
- }
1068
- if (el.foreground !== undefined && !HEX_COLOR_REGEX.test(el.foreground)) {
1069
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'foreground' must be a 6-digit hex string like '#000000'`);
1070
- }
1071
- if (el.background !== undefined && !HEX_COLOR_REGEX.test(el.background)) {
1072
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'background' must be a 6-digit hex string like '#ffffff'`);
1073
- }
1074
- if (el.margin !== undefined && (typeof el.margin !== 'number' || el.margin < 0 || !isFinite(el.margin))) {
1075
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'margin' must be a non-negative finite number`);
1076
- }
1077
- if (el.align !== undefined && !['left', 'center', 'right'].includes(el.align)) {
1078
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'align' must be 'left', 'center', or 'right'`);
1079
- }
1080
- if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
1081
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'spaceAfter' must be a non-negative finite number`);
1082
- }
1083
- if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
1084
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (qr-code): 'spaceBefore' must be a non-negative finite number`);
1085
- }
1086
- break;
1087
- }
1088
- case 'barcode': {
1089
- if (typeof el.symbology !== 'string' || el.symbology.trim().length === 0) {
1090
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'symbology' must be a non-empty string (e.g. 'ean13', 'code128')`);
1091
- }
1092
- if (typeof el.data !== 'string' || el.data.trim().length === 0) {
1093
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'data' must be a non-empty string`);
1094
- }
1095
- if (el.width !== undefined && (typeof el.width !== 'number' || el.width <= 0 || !isFinite(el.width))) {
1096
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'width' must be a positive finite number`);
1097
- }
1098
- if (el.height !== undefined && (typeof el.height !== 'number' || el.height <= 0 || !isFinite(el.height))) {
1099
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'height' must be a positive finite number`);
1100
- }
1101
- if (el.align !== undefined && !['left', 'center', 'right'].includes(el.align)) {
1102
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'align' must be 'left', 'center', or 'right'`);
1103
- }
1104
- if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
1105
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'spaceAfter' must be a non-negative finite number`);
1106
- }
1107
- if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
1108
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (barcode): 'spaceBefore' must be a non-negative finite number`);
1109
- }
1110
- break;
1111
- }
1112
- case 'chart': {
1113
- if (el.spec === null || typeof el.spec !== 'object' || Array.isArray(el.spec)) {
1114
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'spec' must be a plain vega-lite specification object`);
1115
- }
1116
- if (el.width !== undefined && (typeof el.width !== 'number' || el.width <= 0 || !isFinite(el.width))) {
1117
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'width' must be a positive finite number`);
1118
- }
1119
- if (el.height !== undefined && (typeof el.height !== 'number' || el.height <= 0 || !isFinite(el.height))) {
1120
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'height' must be a positive finite number`);
1121
- }
1122
- if (el.caption !== undefined && typeof el.caption !== 'string') {
1123
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'caption' must be a string`);
1124
- }
1125
- if (el.align !== undefined && !['left', 'center', 'right'].includes(el.align)) {
1126
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'align' must be 'left', 'center', or 'right'`);
1127
- }
1128
- if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
1129
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'spaceAfter' must be a non-negative finite number`);
1130
- }
1131
- if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
1132
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (chart): 'spaceBefore' must be a non-negative finite number`);
1133
- }
1134
- break;
1135
- }
1136
- case 'list': {
1137
- if (el.style !== 'ordered' && el.style !== 'unordered') {
1138
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'style' must be 'ordered' or 'unordered'`);
1139
- }
1140
- if (!Array.isArray(el.items) || el.items.length === 0) {
1141
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'items' must be a non-empty array`);
1142
- }
1143
- for (let ii = 0; ii < el.items.length; ii++) {
1144
- const item = el.items[ii];
1145
- // Strict: validate list item properties
1146
- if (strict) {
1147
- assertUnknownProps(item, ALLOWED_PROPS_SUB['list-item'], `${prefix}.items[${ii}]`, errors);
1148
- }
1149
- if (typeof item.text !== 'string' || item.text.trim() === '') {
1150
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].text must be a non-empty string`);
1151
- }
1152
- // NEW: Validate dir field
1153
- if (item.dir !== undefined && !['ltr', 'rtl', 'auto'].includes(item.dir)) {
1154
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].dir must be 'ltr', 'rtl', or 'auto'`);
1155
- }
1156
- if (item.fontWeight !== undefined && ![400, 700].includes(item.fontWeight)) {
1157
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].fontWeight must be 400 or 700`);
1158
- }
1159
- // Validate nested items (1 level deep)
1160
- if (item.items) {
1161
- if (!Array.isArray(item.items) || item.items.length === 0) {
1162
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].items must be a non-empty array if provided`);
1163
- }
1164
- for (let ni = 0; ni < item.items.length; ni++) {
1165
- const nested = item.items[ni];
1166
- // Strict: validate nested list item properties
1167
- if (strict) {
1168
- assertUnknownProps(nested, ALLOWED_PROPS_SUB['list-item'], `${prefix}.items[${ii}].items[${ni}]`, errors);
1169
- }
1170
- if (typeof nested.text !== 'string' || nested.text.trim() === '') {
1171
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].items[${ni}].text must be a non-empty string`);
1172
- }
1173
- if (nested.items !== undefined) {
1174
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): items[${ii}].items[${ni}].items is not allowed — maximum nesting depth is 2 levels`);
1175
- }
1176
- }
1177
- }
1178
- }
1179
- if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
1180
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'color' must be a 6-digit hex string like '#ff0000'. Got: '${el.color}'`);
1181
- }
1182
- if (el.nestedNumberingStyle !== undefined && !['continue', 'restart'].includes(el.nestedNumberingStyle)) {
1183
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'nestedNumberingStyle' must be 'continue' or 'restart'. Got: '${el.nestedNumberingStyle}'`);
1184
- }
1185
- if (el.indent !== undefined && (typeof el.indent !== 'number' || el.indent < 0)) {
1186
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'indent' must be a non-negative number`);
1187
- }
1188
- if (el.markerWidth !== undefined && (typeof el.markerWidth !== 'number' || el.markerWidth <= 0)) {
1189
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'markerWidth' must be a positive number`);
1190
- }
1191
- if (el.marker !== undefined && (typeof el.marker !== 'string' || el.marker.trim() === '')) {
1192
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (list): 'marker' must be a non-empty string`);
1193
- }
1194
- break;
1195
- }
1196
- case 'hr': {
1197
- if (el.thickness !== undefined && (typeof el.thickness !== 'number' || el.thickness < 0 || !isFinite(el.thickness))) {
1198
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (hr): 'thickness' must be a non-negative finite number`);
1199
- }
1200
- if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
1201
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (hr): 'color' must be a 6-digit hex string`);
1202
- }
1203
- if (el.spaceAbove !== undefined && (typeof el.spaceAbove !== 'number' || el.spaceAbove < 0)) {
1204
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (hr): 'spaceAbove' must be a non-negative number`);
1205
- }
1206
- if (el.spaceBelow !== undefined && (typeof el.spaceBelow !== 'number' || el.spaceBelow < 0)) {
1207
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (hr): 'spaceBelow' must be a non-negative number`);
1208
- }
1209
- break;
1210
- }
1211
- case 'page-break': {
1212
- // No fields to validate
1213
- break;
1214
- }
1215
- case 'rich-paragraph': {
1216
- if (!Array.isArray(el.spans) || el.spans.length === 0) {
1217
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'spans' must be a non-empty array`);
1218
- }
1219
- for (let si = 0; si < el.spans.length; si++) {
1220
- const span = el.spans[si];
1221
- // Strict: validate span properties
1222
- if (strict) {
1223
- assertUnknownProps(span, ALLOWED_PROPS_SUB['inline-span'], `${prefix}.spans[${si}]`, errors);
1224
- }
1225
- if (typeof span.text !== 'string') {
1226
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].text must be a string`);
1227
- }
1228
- if (span.text === '') {
1229
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].text cannot be an empty string. Use ' ' for a space between styled runs.`);
1230
- }
1231
- if (span.color !== undefined && !HEX_COLOR_REGEX.test(span.color)) {
1232
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].color must be a 6-digit hex string`);
1233
- }
1234
- if (span.fontWeight !== undefined && ![400, 700].includes(span.fontWeight)) {
1235
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].fontWeight must be 400 or 700`);
1236
- }
1237
- if (span.fontStyle !== undefined && !['normal', 'italic'].includes(span.fontStyle)) {
1238
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].fontStyle must be 'normal' or 'italic'`);
1239
- }
1240
- if (span.fontSize !== undefined && (typeof span.fontSize !== 'number' || span.fontSize <= 0 || !isFinite(span.fontSize))) {
1241
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].fontSize must be a positive finite number if provided`);
1242
- }
1243
- if (span.url !== undefined && (typeof span.url !== 'string' || span.url.trim() === '')) {
1244
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].url must be a non-empty string if provided`);
1245
- }
1246
- if (span.url !== undefined && typeof span.url === 'string')
1247
- validateUrl(span.url, `${prefix} (rich-paragraph) spans[${si}].url`);
1248
- if (span.href !== undefined && (typeof span.href !== 'string' || span.href.trim() === '')) {
1249
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].href must be a non-empty string if provided`);
1250
- }
1251
- if (span.href !== undefined && typeof span.href === 'string')
1252
- validateUrl(span.href, `${prefix} (rich-paragraph) spans[${si}].href`);
1253
- if (span.url !== undefined && span.href !== undefined) {
1254
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}] cannot have both 'url' and 'href' — use one or the other`);
1255
- }
1256
- if (span.verticalAlign !== undefined && span.verticalAlign !== 'superscript' && span.verticalAlign !== 'subscript') {
1257
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].verticalAlign must be "superscript" or "subscript"`);
1258
- }
1259
- if (span.letterSpacing !== undefined && (typeof span.letterSpacing !== 'number' || span.letterSpacing < 0 || !isFinite(span.letterSpacing) || span.letterSpacing > 200)) {
1260
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): spans[${si}].letterSpacing must be a non-negative finite number and <= 200`);
1261
- }
1262
- }
1263
- if (el.dir !== undefined && !['ltr', 'rtl', 'auto'].includes(el.dir)) {
1264
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'dir' must be 'ltr', 'rtl', or 'auto'`);
1265
- }
1266
- if (el.letterSpacing !== undefined && (typeof el.letterSpacing !== 'number' || el.letterSpacing < 0 || !isFinite(el.letterSpacing) || el.letterSpacing > 200)) {
1267
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'letterSpacing' must be a non-negative finite number and <= 200`);
1268
- }
1269
- if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
1270
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'fontSize' must be a positive finite number`);
1271
- }
1272
- if (el.bgColor !== undefined && !HEX_COLOR_REGEX.test(el.bgColor)) {
1273
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'bgColor' must be a 6-digit hex string`);
1274
- }
1275
- if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
1276
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'spaceAfter' must be a non-negative finite number`);
1277
- }
1278
- if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
1279
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'spaceBefore' must be a non-negative finite number`);
1280
- }
1281
- if (el.columns !== undefined && (typeof el.columns !== 'number' || el.columns < 1 || !Number.isInteger(el.columns) || el.columns > 6)) {
1282
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'columns' must be a positive integer between 1 and 6`);
1283
- }
1284
- if (el.columnGap !== undefined && (typeof el.columnGap !== 'number' || el.columnGap < 0 || !isFinite(el.columnGap))) {
1285
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'columnGap' must be a non-negative finite number`);
1286
- }
1287
- if (el.align !== undefined && !['left', 'center', 'right', 'justify'].includes(el.align)) {
1288
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (rich-paragraph): 'align' must be 'left', 'center', 'right', or 'justify'`);
1289
- }
1290
- break;
1291
- }
1292
- case 'code': {
1293
- if (typeof el.text !== 'string') {
1294
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'text' must be a string`);
1295
- }
1296
- if (el.text.trim() === '') {
1297
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'text' must be a non-empty string`);
1298
- }
1299
- if (!el.fontFamily || typeof el.fontFamily !== 'string') {
1300
- throw new PretextPdfError('MONOSPACE_FONT_REQUIRED', `${prefix} (code): 'fontFamily' is required. Provide a monospace TTF font family name that you have loaded in doc.fonts (e.g., 'JetBrains Mono', 'Fira Code', 'Courier Prime').`);
1301
- }
1302
- if (!loadedFamilies.has(el.fontFamily)) {
1303
- throw new PretextPdfError('MONOSPACE_FONT_REQUIRED', `${prefix} (code): fontFamily '${el.fontFamily}' is not loaded. Add { family: '${el.fontFamily}', src: '/path/to/font.ttf' } to doc.fonts.`);
1304
- }
1305
- if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
1306
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'fontSize' must be a positive finite number`);
1307
- }
1308
- if (el.bgColor !== undefined && !HEX_COLOR_REGEX.test(el.bgColor)) {
1309
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'bgColor' must be a 6-digit hex string`);
1310
- }
1311
- if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
1312
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'color' must be a 6-digit hex string`);
1313
- }
1314
- if (el.padding !== undefined && (typeof el.padding !== 'number' || el.padding < 0 || !isFinite(el.padding))) {
1315
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'padding' must be a non-negative finite number`);
1316
- }
1317
- if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
1318
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'spaceAfter' must be a non-negative finite number`);
1319
- }
1320
- if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
1321
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'spaceBefore' must be a non-negative finite number`);
1322
- }
1323
- if (el.language !== undefined && typeof el.language !== 'string') {
1324
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'language' must be a string`);
1325
- }
1326
- if (el.highlightTheme !== undefined) {
1327
- if (typeof el.highlightTheme !== 'object' || el.highlightTheme === null) {
1328
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): 'highlightTheme' must be an object`);
1329
- }
1330
- for (const [k, v] of Object.entries(el.highlightTheme)) {
1331
- if (v !== undefined && !HEX_COLOR_REGEX.test(v)) {
1332
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (code): highlightTheme.${k} must be a 6-digit hex string`);
1333
- }
1334
- }
1335
- }
1336
- break;
1337
- }
1338
- case 'blockquote': {
1339
- if (typeof el.text !== 'string') {
1340
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'text' must be a string`);
1341
- }
1342
- if (el.text.trim() === '') {
1343
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'text' must be a non-empty string`);
1344
- }
1345
- if (el.borderColor !== undefined && !HEX_COLOR_REGEX.test(el.borderColor)) {
1346
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'borderColor' must be a 6-digit hex string`);
1347
- }
1348
- if (el.borderWidth !== undefined && (typeof el.borderWidth !== 'number' || el.borderWidth < 0 || !isFinite(el.borderWidth))) {
1349
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'borderWidth' must be a non-negative finite number`);
1350
- }
1351
- if (el.bgColor !== undefined && !HEX_COLOR_REGEX.test(el.bgColor)) {
1352
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'bgColor' must be a 6-digit hex string`);
1353
- }
1354
- if (el.color !== undefined && !HEX_COLOR_REGEX.test(el.color)) {
1355
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'color' must be a 6-digit hex string`);
1356
- }
1357
- if (el.fontWeight !== undefined && ![400, 700].includes(el.fontWeight)) {
1358
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'fontWeight' must be 400 or 700`);
1359
- }
1360
- if (el.fontStyle !== undefined && !['normal', 'italic'].includes(el.fontStyle)) {
1361
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'fontStyle' must be 'normal' or 'italic'`);
1362
- }
1363
- if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
1364
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'fontSize' must be a positive finite number`);
1365
- }
1366
- if (el.lineHeight !== undefined && typeof el.lineHeight === 'number') {
1367
- const effectiveFontSize = el.fontSize ?? 12;
1368
- if (el.lineHeight < effectiveFontSize) {
1369
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): lineHeight (${el.lineHeight}) is less than fontSize (${effectiveFontSize}). Lines would overlap.`);
1370
- }
1371
- }
1372
- if (el.padding !== undefined && (typeof el.padding !== 'number' || el.padding < 0 || !isFinite(el.padding))) {
1373
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'padding' must be a non-negative finite number`);
1374
- }
1375
- if (el.paddingH !== undefined && (typeof el.paddingH !== 'number' || el.paddingH < 0 || !isFinite(el.paddingH))) {
1376
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'paddingH' must be a non-negative finite number`);
1377
- }
1378
- if (el.paddingV !== undefined && (typeof el.paddingV !== 'number' || el.paddingV < 0 || !isFinite(el.paddingV))) {
1379
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'paddingV' must be a non-negative finite number`);
1380
- }
1381
- if (el.align !== undefined && !['left', 'center', 'right', 'justify'].includes(el.align)) {
1382
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'align' must be 'left', 'center', 'right', or 'justify'`);
1383
- }
1384
- if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
1385
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'spaceBefore' must be a non-negative finite number`);
1386
- }
1387
- if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
1388
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (blockquote): 'spaceAfter' must be a non-negative finite number`);
1389
- }
1390
- break;
1391
- }
1392
- case 'callout': {
1393
- if (!el.content || typeof el.content !== 'string' || el.content.trim() === '') {
1394
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (callout): 'content' is required and must be a non-empty string`);
1395
- }
1396
- const validStyles = ['info', 'warning', 'tip', 'note'];
1397
- if (el.style !== undefined && !validStyles.includes(el.style)) {
1398
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (callout): 'style' must be one of: ${validStyles.join(', ')}`);
1399
- }
1400
- if (el.backgroundColor !== undefined && !/^#[0-9A-Fa-f]{6}$/.test(el.backgroundColor)) {
1401
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (callout): 'backgroundColor' must be a 6-digit hex color`);
1402
- }
1403
- if (el.borderColor !== undefined && !/^#[0-9A-Fa-f]{6}$/.test(el.borderColor)) {
1404
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (callout): 'borderColor' must be a 6-digit hex color`);
1405
- }
1406
- if (el.color !== undefined && !/^#[0-9A-Fa-f]{6}$/.test(el.color)) {
1407
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (callout): 'color' must be a 6-digit hex color`);
1408
- }
1409
- if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0)) {
1410
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (callout): 'fontSize' must be a positive number`);
1411
- }
1412
- break;
1413
- }
1414
- case 'toc': {
1415
- if (el.minLevel !== undefined && ![1, 2, 3, 4].includes(el.minLevel)) {
1416
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'minLevel' must be 1, 2, 3, or 4`);
1417
- }
1418
- if (el.maxLevel !== undefined && ![1, 2, 3, 4].includes(el.maxLevel)) {
1419
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'maxLevel' must be 1, 2, 3, or 4`);
1420
- }
1421
- if (el.minLevel !== undefined && el.maxLevel !== undefined && el.minLevel > el.maxLevel) {
1422
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'minLevel' cannot exceed 'maxLevel'`);
1423
- }
1424
- if (el.fontSize !== undefined && (typeof el.fontSize !== 'number' || el.fontSize <= 0 || !isFinite(el.fontSize))) {
1425
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'fontSize' must be a positive finite number`);
1426
- }
1427
- if (el.titleFontSize !== undefined && (typeof el.titleFontSize !== 'number' || el.titleFontSize <= 0 || !isFinite(el.titleFontSize))) {
1428
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'titleFontSize' must be a positive finite number`);
1429
- }
1430
- if (el.levelIndent !== undefined && (typeof el.levelIndent !== 'number' || el.levelIndent < 0 || !isFinite(el.levelIndent))) {
1431
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'levelIndent' must be a non-negative finite number`);
1432
- }
1433
- if (el.leader !== undefined && (typeof el.leader !== 'string' || el.leader.length === 0)) {
1434
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'leader' must be a non-empty string`);
1435
- }
1436
- if (el.entrySpacing !== undefined && (typeof el.entrySpacing !== 'number' || el.entrySpacing < 0 || !isFinite(el.entrySpacing))) {
1437
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'entrySpacing' must be a non-negative finite number`);
1438
- }
1439
- if (el.spaceBefore !== undefined && (typeof el.spaceBefore !== 'number' || el.spaceBefore < 0 || !isFinite(el.spaceBefore))) {
1440
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'spaceBefore' must be a non-negative finite number`);
1441
- }
1442
- if (el.spaceAfter !== undefined && (typeof el.spaceAfter !== 'number' || el.spaceAfter < 0 || !isFinite(el.spaceAfter))) {
1443
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (toc): 'spaceAfter' must be a non-negative finite number`);
1444
- }
1445
- break;
1446
- }
1447
- case 'comment': {
1448
- const commentEl = el;
1449
- if (!commentEl.contents || typeof commentEl.contents !== 'string' || commentEl.contents.trim() === '') {
1450
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (comment): 'contents' is required and must be a non-empty string`);
1451
- }
1452
- if (commentEl.color !== undefined && !HEX_COLOR_REGEX.test(commentEl.color)) {
1453
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (comment): 'color' must be a valid 6-digit hex color`);
1454
- }
1455
- break;
1456
- }
1457
- case 'form-field': {
1458
- const ff = el;
1459
- const fieldTypes = ['text', 'checkbox', 'radio', 'dropdown', 'button'];
1460
- if (!fieldTypes.includes(ff.fieldType)) {
1461
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (form-field): fieldType must be one of: ${fieldTypes.join(', ')}`);
1462
- }
1463
- if (!ff.name || ff.name.trim() === '') {
1464
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (form-field): name is required and must be a non-empty string`);
1465
- }
1466
- if ((ff.fieldType === 'radio' || ff.fieldType === 'dropdown') && (!ff.options || ff.options.length === 0)) {
1467
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (form-field): type "${ff.fieldType}" requires a non-empty options array`);
1468
- }
1469
- if (ff.width !== undefined && (typeof ff.width !== 'number' || ff.width <= 0)) {
1470
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (form-field): width must be a positive number`);
1471
- }
1472
- if (ff.height !== undefined && (typeof ff.height !== 'number' || ff.height <= 0)) {
1473
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (form-field): height must be a positive number`);
1474
- }
1475
- if (ff.borderColor !== undefined && !/^#[0-9A-Fa-f]{6}$/.test(ff.borderColor)) {
1476
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (form-field): borderColor must be a 6-digit hex color`);
1477
- }
1478
- if (ff.backgroundColor !== undefined && !/^#[0-9A-Fa-f]{6}$/.test(ff.backgroundColor)) {
1479
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (form-field): backgroundColor must be a 6-digit hex color`);
1480
- }
1481
- break;
1482
- }
1483
- case 'footnote-def': {
1484
- const fn = el;
1485
- if (!fn.id || typeof fn.id !== 'string' || fn.id.trim() === '') {
1486
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (footnote-def): 'id' must be a non-empty string`);
1487
- }
1488
- if (!/^[a-zA-Z0-9_-]+$/.test(fn.id)) {
1489
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (footnote-def): 'id' must contain only letters, numbers, hyphens, or underscores. Got: "${fn.id}"`);
1490
- }
1491
- if (!fn.text || typeof fn.text !== 'string' || fn.text.trim() === '') {
1492
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (footnote-def): 'text' must be a non-empty string`);
1493
- }
1494
- if (fn.fontSize !== undefined && (typeof fn.fontSize !== 'number' || fn.fontSize <= 0)) {
1495
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (footnote-def): 'fontSize' must be a positive number`);
1496
- }
1497
- break;
1498
- }
1499
- case 'toc-entry': {
1500
- // Internal type — should never appear in user input
1501
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix}: 'toc-entry' is an internal type and cannot be used in document content`);
1502
- }
1503
- case 'float-group': {
1504
- const fg = el;
1505
- // Validate image
1506
- if (!fg.image || !fg.image.src || (typeof fg.image.src !== 'string' && !(fg.image.src instanceof Uint8Array))) {
1507
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'image.src' must be a non-empty string path or Uint8Array`);
1508
- }
1509
- if (fg.image.format !== undefined && fg.image.format !== 'png' && fg.image.format !== 'jpg' && fg.image.format !== 'auto') {
1510
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'image.format' must be 'png', 'jpg', or 'auto'`);
1511
- }
1512
- if (fg.image.height !== undefined && (typeof fg.image.height !== 'number' || fg.image.height <= 0)) {
1513
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'image.height' must be a positive number`);
1514
- }
1515
- // Validate float
1516
- if (fg.float !== 'left' && fg.float !== 'right') {
1517
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'float' must be 'left' or 'right'`);
1518
- }
1519
- // Validate floatWidth
1520
- if (fg.floatWidth !== undefined && (typeof fg.floatWidth !== 'number' || fg.floatWidth < 30)) {
1521
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'floatWidth' must be a number >= 30`);
1522
- }
1523
- // Validate floatGap
1524
- if (fg.floatGap !== undefined && (typeof fg.floatGap !== 'number' || fg.floatGap < 0)) {
1525
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'floatGap' must be a non-negative number`);
1526
- }
1527
- // Validate content
1528
- if (!Array.isArray(fg.content) || fg.content.length === 0) {
1529
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'content' must be a non-empty array`);
1530
- }
1531
- for (let i = 0; i < fg.content.length; i++) {
1532
- const item = fg.content[i];
1533
- if (!['paragraph', 'heading', 'rich-paragraph'].includes(item.type)) {
1534
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group).content[${i}]: only 'paragraph', 'heading', and 'rich-paragraph' elements are allowed in float groups`);
1535
- }
1536
- // Strict: validate nested content element properties
1537
- if (strict) {
1538
- const allowed = ALLOWED_PROPS[item.type];
1539
- if (allowed) {
1540
- assertUnknownProps(item, allowed, `${prefix} (float-group).content[${i}]`, errors);
1541
- }
1542
- }
1543
- }
1544
- // Validate spacing
1545
- if (fg.spaceBefore !== undefined && (typeof fg.spaceBefore !== 'number' || fg.spaceBefore < 0)) {
1546
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'spaceBefore' must be a non-negative number`);
1547
- }
1548
- if (fg.spaceAfter !== undefined && (typeof fg.spaceAfter !== 'number' || fg.spaceAfter < 0)) {
1549
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (float-group): 'spaceAfter' must be a non-negative number`);
1550
- }
1551
- break;
1552
- }
1553
- default: {
1554
- const type = el.type;
1555
- const plugins = options?.plugins ?? [];
1556
- const plugin = findPlugin(plugins, String(type));
1557
- if (plugin) {
1558
- let rejection;
1559
- try {
1560
- rejection = runPluginValidate(plugin, el);
1561
- }
1562
- catch (err) {
1563
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (${String(type)}): plugin validate hook threw: ${err instanceof Error ? err.message : String(err)}`);
1564
- }
1565
- if (rejection) {
1566
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix} (${String(type)}): ${rejection}`);
1567
- }
1568
- break;
1569
- }
1570
- const validList = ELEMENT_TYPES.map(t => `'${t}'`).join(', ');
1571
- throw new PretextPdfError('VALIDATION_ERROR', `${prefix}: unknown element type '${String(type)}'. Valid types: ${validList}`);
1572
- }
1573
- }
1574
- }
1575
- function validateFontSpec(font) {
1576
- if (!font.family || typeof font.family !== 'string') {
1577
- throw new PretextPdfError('VALIDATION_ERROR', `FontSpec: 'family' must be a non-empty string`);
1578
- }
1579
- if (font.weight !== undefined && ![400, 700].includes(font.weight)) {
1580
- throw new PretextPdfError('VALIDATION_ERROR', `FontSpec '${font.family}': 'weight' must be 400 or 700`);
1581
- }
1582
- if (font.style !== undefined && !['normal', 'italic'].includes(font.style)) {
1583
- throw new PretextPdfError('VALIDATION_ERROR', `FontSpec '${font.family}': 'style' must be 'normal' or 'italic'`);
1584
- }
1585
- if (font.src === undefined || font.src === null) {
1586
- throw new PretextPdfError('VALIDATION_ERROR', `FontSpec '${font.family}': 'src' is required (file path or Uint8Array)`);
1587
- }
1588
- }
9
+ export * from './validate/index.js';
1589
10
  //# sourceMappingURL=validate.js.map