@jasy/pdf 1.0.0-alpha.1 → 1.0.0-alpha.10

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 (275) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +35 -21
  3. package/dist/api/args.d.ts +1 -1
  4. package/dist/api/args.js +2 -5
  5. package/dist/api/color.d.ts +4 -4
  6. package/dist/api/color.js +11 -17
  7. package/dist/api/content.d.ts +20 -13
  8. package/dist/api/content.js +44 -29
  9. package/dist/api/descriptor.d.ts +2 -2
  10. package/dist/api/descriptor.js +121 -31
  11. package/dist/api/dimension.d.ts +76 -0
  12. package/dist/api/dimension.js +43 -0
  13. package/dist/api/font-url.d.ts +28 -0
  14. package/dist/api/font-url.js +134 -0
  15. package/dist/api/forms.d.ts +347 -0
  16. package/dist/api/forms.js +189 -0
  17. package/dist/api/gradient.d.ts +53 -0
  18. package/dist/api/gradient.js +97 -0
  19. package/dist/api/index.d.ts +13 -8
  20. package/dist/api/index.js +13 -24
  21. package/dist/api/insets.d.ts +19 -12
  22. package/dist/api/insets.js +19 -12
  23. package/dist/api/layout.d.ts +149 -25
  24. package/dist/api/layout.js +182 -51
  25. package/dist/api/page-builder.d.ts +40 -0
  26. package/dist/api/page-builder.js +44 -0
  27. package/dist/api/structure.d.ts +100 -13
  28. package/dist/api/structure.js +179 -88
  29. package/dist/api/table.d.ts +5 -5
  30. package/dist/api/table.js +38 -26
  31. package/dist/api/text.d.ts +90 -4
  32. package/dist/api/text.js +116 -31
  33. package/dist/assets/font-data.d.ts +2 -0
  34. package/dist/assets/font-data.js +21 -0
  35. package/dist/assets/font-data.ts +37 -0
  36. package/dist/common/color.js +1 -5
  37. package/dist/constants/page-sizes.js +3 -6
  38. package/dist/constants/pdf-parts.js +1 -4
  39. package/dist/crypto/legacy-handler.d.ts +43 -0
  40. package/dist/crypto/legacy-handler.js +145 -0
  41. package/dist/crypto/security-handler.d.ts +62 -0
  42. package/dist/crypto/security-handler.js +155 -0
  43. package/dist/crypto/webcrypto.d.ts +11 -0
  44. package/dist/crypto/webcrypto.js +62 -0
  45. package/dist/edit/acroform-reader.d.ts +70 -0
  46. package/dist/edit/acroform-reader.js +126 -0
  47. package/dist/edit/appearance.d.ts +47 -0
  48. package/dist/edit/appearance.js +171 -0
  49. package/dist/edit/document.d.ts +145 -0
  50. package/dist/edit/document.js +642 -0
  51. package/dist/edit/fill.d.ts +65 -0
  52. package/dist/edit/fill.js +424 -0
  53. package/dist/edit/flatten.d.ts +54 -0
  54. package/dist/edit/flatten.js +462 -0
  55. package/dist/edit/index.d.ts +20 -0
  56. package/dist/edit/index.js +18 -0
  57. package/dist/edit/lexer.d.ts +30 -0
  58. package/dist/edit/lexer.js +285 -0
  59. package/dist/edit/objects.d.ts +68 -0
  60. package/dist/edit/objects.js +83 -0
  61. package/dist/edit/writer.d.ts +60 -0
  62. package/dist/edit/writer.js +283 -0
  63. package/dist/elements/container-element.d.ts +50 -5
  64. package/dist/elements/container-element.js +100 -39
  65. package/dist/elements/forms/checkbox-element.d.ts +51 -0
  66. package/dist/elements/forms/checkbox-element.js +48 -0
  67. package/dist/elements/forms/choice-element.d.ts +53 -0
  68. package/dist/elements/forms/choice-element.js +59 -0
  69. package/dist/elements/forms/push-button-element.d.ts +56 -0
  70. package/dist/elements/forms/push-button-element.js +49 -0
  71. package/dist/elements/forms/radio-element.d.ts +52 -0
  72. package/dist/elements/forms/radio-element.js +49 -0
  73. package/dist/elements/forms/signature-element.d.ts +54 -0
  74. package/dist/elements/forms/signature-element.js +47 -0
  75. package/dist/elements/forms/text-field-element.d.ts +61 -0
  76. package/dist/elements/forms/text-field-element.js +54 -0
  77. package/dist/elements/image-element.d.ts +58 -6
  78. package/dist/elements/image-element.js +145 -111
  79. package/dist/elements/index.d.ts +26 -10
  80. package/dist/elements/index.js +26 -28
  81. package/dist/elements/layout/anchor-element.d.ts +23 -0
  82. package/dist/elements/layout/anchor-element.js +27 -0
  83. package/dist/elements/layout/bookmark-element.d.ts +27 -0
  84. package/dist/elements/layout/bookmark-element.js +30 -0
  85. package/dist/elements/layout/default-text-style-element.d.ts +30 -0
  86. package/dist/elements/layout/default-text-style-element.js +47 -0
  87. package/dist/elements/layout/deferred-element.d.ts +3 -3
  88. package/dist/elements/layout/deferred-element.js +4 -8
  89. package/dist/elements/layout/expanded-element.d.ts +6 -4
  90. package/dist/elements/layout/expanded-element.js +22 -17
  91. package/dist/elements/layout/keep-together-element.d.ts +42 -0
  92. package/dist/elements/layout/keep-together-element.js +85 -0
  93. package/dist/elements/layout/link-element.d.ts +33 -0
  94. package/dist/elements/layout/link-element.js +39 -0
  95. package/dist/elements/layout/padding-element.d.ts +10 -5
  96. package/dist/elements/layout/padding-element.js +25 -16
  97. package/dist/elements/layout/page-break-element.d.ts +17 -0
  98. package/dist/elements/layout/page-break-element.js +24 -0
  99. package/dist/elements/layout/page-builder-element.d.ts +32 -0
  100. package/dist/elements/layout/page-builder-element.js +45 -0
  101. package/dist/elements/layout/positioned-element.d.ts +47 -0
  102. package/dist/elements/layout/positioned-element.js +73 -0
  103. package/dist/elements/layout/repeating-header-element.d.ts +3 -3
  104. package/dist/elements/layout/repeating-header-element.js +8 -12
  105. package/dist/elements/layout/rotated-box-element.d.ts +35 -0
  106. package/dist/elements/layout/rotated-box-element.js +54 -0
  107. package/dist/elements/layout/rotated-element.d.ts +32 -0
  108. package/dist/elements/layout/rotated-element.js +41 -0
  109. package/dist/elements/layout/sized-container-element.d.ts +2 -2
  110. package/dist/elements/layout/sized-container-element.js +6 -11
  111. package/dist/elements/layout/struct-group.d.ts +28 -0
  112. package/dist/elements/layout/struct-group.js +42 -0
  113. package/dist/elements/line-element.d.ts +3 -3
  114. package/dist/elements/line-element.js +5 -10
  115. package/dist/elements/page-element.d.ts +26 -6
  116. package/dist/elements/page-element.js +72 -27
  117. package/dist/elements/pdf-document-element.d.ts +10 -4
  118. package/dist/elements/pdf-document-element.js +11 -10
  119. package/dist/elements/pdf-element.d.ts +150 -4
  120. package/dist/elements/pdf-element.js +138 -20
  121. package/dist/elements/rectangle-element.d.ts +52 -11
  122. package/dist/elements/rectangle-element.js +129 -36
  123. package/dist/elements/row-element.d.ts +45 -4
  124. package/dist/elements/row-element.js +66 -18
  125. package/dist/elements/text-element.d.ts +132 -11
  126. package/dist/elements/text-element.js +206 -56
  127. package/dist/forms/acroform.d.ts +63 -0
  128. package/dist/forms/acroform.js +427 -0
  129. package/dist/forms/appearance.d.ts +58 -0
  130. package/dist/forms/appearance.js +204 -0
  131. package/dist/forms/field.d.ts +164 -0
  132. package/dist/forms/field.js +1 -0
  133. package/dist/forms/pdf.d.ts +19 -0
  134. package/dist/forms/pdf.js +28 -0
  135. package/dist/index.d.ts +3 -3
  136. package/dist/index.js +3 -19
  137. package/dist/ir/display-list.d.ts +197 -8
  138. package/dist/ir/display-list.js +8 -2
  139. package/dist/layout/alignment.d.ts +10 -0
  140. package/dist/layout/alignment.js +6 -0
  141. package/dist/layout/box-constraints.d.ts +61 -0
  142. package/dist/layout/box-constraints.js +88 -6
  143. package/dist/layout/collect-images.d.ts +9 -0
  144. package/dist/layout/collect-images.js +34 -0
  145. package/dist/layout/fragmentation.d.ts +20 -1
  146. package/dist/layout/fragmentation.js +74 -18
  147. package/dist/layout/insets.d.ts +28 -0
  148. package/dist/layout/insets.js +20 -0
  149. package/dist/platform/browser-fs.d.ts +2 -0
  150. package/dist/platform/browser-fs.js +9 -0
  151. package/dist/platform/browser-image.d.ts +5 -0
  152. package/dist/platform/browser-image.js +13 -0
  153. package/dist/platform/node-fs.d.ts +2 -0
  154. package/dist/platform/node-fs.js +10 -0
  155. package/dist/platform/node-image.d.ts +5 -0
  156. package/dist/platform/node-image.js +9 -0
  157. package/dist/renderer/anchor-renderer.d.ts +6 -0
  158. package/dist/renderer/anchor-renderer.js +11 -0
  159. package/dist/renderer/bookmark-renderer.d.ts +6 -0
  160. package/dist/renderer/bookmark-renderer.js +11 -0
  161. package/dist/renderer/container-renderer.d.ts +3 -3
  162. package/dist/renderer/container-renderer.js +11 -28
  163. package/dist/renderer/default-text-style-renderer.d.ts +6 -0
  164. package/dist/renderer/default-text-style-renderer.js +10 -0
  165. package/dist/renderer/deferred-renderer.d.ts +3 -3
  166. package/dist/renderer/deferred-renderer.js +8 -23
  167. package/dist/renderer/emoji-image.d.ts +9 -0
  168. package/dist/renderer/emoji-image.js +68 -0
  169. package/dist/renderer/expanded-renderer.d.ts +3 -3
  170. package/dist/renderer/expanded-renderer.js +6 -21
  171. package/dist/renderer/form-field-renderer.d.ts +20 -0
  172. package/dist/renderer/form-field-renderer.js +12 -0
  173. package/dist/renderer/image-renderer.d.ts +4 -4
  174. package/dist/renderer/image-renderer.js +86 -75
  175. package/dist/renderer/index.d.ts +10 -10
  176. package/dist/renderer/index.js +10 -26
  177. package/dist/renderer/keep-together-renderer.d.ts +10 -0
  178. package/dist/renderer/keep-together-renderer.js +12 -0
  179. package/dist/renderer/line-renderer.d.ts +3 -3
  180. package/dist/renderer/line-renderer.js +13 -28
  181. package/dist/renderer/link-renderer.d.ts +6 -0
  182. package/dist/renderer/link-renderer.js +11 -0
  183. package/dist/renderer/padding-renderer.d.ts +3 -3
  184. package/dist/renderer/padding-renderer.js +6 -21
  185. package/dist/renderer/page-break-renderer.d.ts +14 -0
  186. package/dist/renderer/page-break-renderer.js +15 -0
  187. package/dist/renderer/page-builder-renderer.d.ts +6 -0
  188. package/dist/renderer/page-builder-renderer.js +11 -0
  189. package/dist/renderer/page-renderer.d.ts +2 -2
  190. package/dist/renderer/page-renderer.js +115 -77
  191. package/dist/renderer/pdf-backend.d.ts +31 -4
  192. package/dist/renderer/pdf-backend.js +287 -49
  193. package/dist/renderer/pdf-config.js +4 -7
  194. package/dist/renderer/pdf-document-class.d.ts +5 -5
  195. package/dist/renderer/pdf-document-class.js +27 -41
  196. package/dist/renderer/pdf-document-renderer.d.ts +19 -10
  197. package/dist/renderer/pdf-document-renderer.js +135 -91
  198. package/dist/renderer/pdf-renderer.d.ts +2 -2
  199. package/dist/renderer/pdf-renderer.js +165 -90
  200. package/dist/renderer/positioned-renderer.d.ts +6 -0
  201. package/dist/renderer/positioned-renderer.js +10 -0
  202. package/dist/renderer/rectangle-renderer.d.ts +3 -3
  203. package/dist/renderer/rectangle-renderer.js +49 -44
  204. package/dist/renderer/repeating-header-renderer.d.ts +3 -3
  205. package/dist/renderer/repeating-header-renderer.js +10 -26
  206. package/dist/renderer/rotated-renderer.d.ts +7 -0
  207. package/dist/renderer/rotated-renderer.js +29 -0
  208. package/dist/renderer/row-renderer.d.ts +3 -3
  209. package/dist/renderer/row-renderer.js +11 -28
  210. package/dist/renderer/struct-group-renderer.d.ts +10 -0
  211. package/dist/renderer/struct-group-renderer.js +24 -0
  212. package/dist/renderer/text-renderer.d.ts +9 -5
  213. package/dist/renderer/text-renderer.js +437 -89
  214. package/dist/text/advance.d.ts +34 -0
  215. package/dist/text/advance.js +45 -0
  216. package/dist/text/arabic.d.ts +20 -0
  217. package/dist/text/arabic.js +101 -0
  218. package/dist/text/bidi.d.ts +33 -0
  219. package/dist/text/bidi.js +94 -0
  220. package/dist/text/emoji-codepoints.d.ts +1 -0
  221. package/dist/text/emoji-codepoints.js +29 -0
  222. package/dist/text/font-fallback.d.ts +25 -0
  223. package/dist/text/font-fallback.js +38 -0
  224. package/dist/text/line-breaker.d.ts +50 -12
  225. package/dist/text/line-breaker.js +158 -37
  226. package/dist/text/line-metrics.d.ts +49 -0
  227. package/dist/text/line-metrics.js +57 -0
  228. package/dist/text/orphans-widows.d.ts +27 -0
  229. package/dist/text/orphans-widows.js +53 -0
  230. package/dist/text/shape.d.ts +29 -0
  231. package/dist/text/shape.js +67 -0
  232. package/dist/text/text-decoration.d.ts +50 -0
  233. package/dist/text/text-decoration.js +62 -0
  234. package/dist/text/text-style.d.ts +60 -0
  235. package/dist/text/text-style.js +79 -0
  236. package/dist/utils/afm-parser.d.ts +34 -0
  237. package/dist/utils/afm-parser.js +76 -17
  238. package/dist/utils/bytes.d.ts +24 -0
  239. package/dist/utils/bytes.js +76 -0
  240. package/dist/utils/dest-registry.d.ts +22 -0
  241. package/dist/utils/dest-registry.js +32 -0
  242. package/dist/utils/flex-layout.d.ts +36 -9
  243. package/dist/utils/flex-layout.js +207 -30
  244. package/dist/utils/font-metrics.d.ts +20 -1
  245. package/dist/utils/font-metrics.js +1 -2
  246. package/dist/utils/font-path.js +3 -6
  247. package/dist/utils/gsub.d.ts +36 -0
  248. package/dist/utils/gsub.js +178 -0
  249. package/dist/utils/image-helper.d.ts +6 -5
  250. package/dist/utils/image-helper.js +101 -111
  251. package/dist/utils/inflate.d.ts +16 -0
  252. package/dist/utils/inflate.js +52 -0
  253. package/dist/utils/md5.d.ts +4 -0
  254. package/dist/utils/md5.js +79 -0
  255. package/dist/utils/outline.d.ts +21 -0
  256. package/dist/utils/outline.js +79 -0
  257. package/dist/utils/pdf-object-manager.d.ts +110 -8
  258. package/dist/utils/pdf-object-manager.js +583 -141
  259. package/dist/utils/renderer-registry.d.ts +13 -1
  260. package/dist/utils/renderer-registry.js +23 -6
  261. package/dist/utils/struct-tree.d.ts +44 -0
  262. package/dist/utils/struct-tree.js +129 -0
  263. package/dist/utils/ttf-parser.d.ts +134 -2
  264. package/dist/utils/ttf-parser.js +873 -50
  265. package/dist/utils/ttf-subsetter.d.ts +1 -1
  266. package/dist/utils/ttf-subsetter.js +40 -42
  267. package/dist/utils/ua-xmp.d.ts +9 -0
  268. package/dist/utils/ua-xmp.js +32 -0
  269. package/dist/utils/utf8-to-windows1252-encoder.d.ts +6 -0
  270. package/dist/utils/utf8-to-windows1252-encoder.js +11 -4
  271. package/dist/utils/woff.d.ts +10 -0
  272. package/dist/utils/woff.js +116 -0
  273. package/dist/validators/element-validator.d.ts +2 -2
  274. package/dist/validators/element-validator.js +31 -26
  275. package/package.json +22 -2
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Florian Heuberger
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
  </p>
11
11
 
12
12
  <p align="center">
13
- <code>npm i @jasy/zugferd</code> &nbsp;·&nbsp; <code>npx @jasy/cli</code> &nbsp;·&nbsp; MIT
13
+ <code>npm i @jasy/e-invoice</code> &nbsp;·&nbsp; <code>npx @jasy/cli</code> &nbsp;·&nbsp; MIT
14
14
  </p>
15
15
 
16
16
  ---
@@ -37,24 +37,34 @@ account, no upload, nothing leaves your machine.
37
37
  ## You bring the line items. jasy does the rest.
38
38
 
39
39
  You never compute a total, a VAT breakdown or a rounding again. You hand jasy the line items - it
40
- **derives** the document totals and the EN-16931 VAT breakdown, spec-correct. That is *why* the
40
+ **derives** the document totals and the EN-16931 VAT breakdown, spec-correct. That is _why_ the
41
41
  invoices validate: the amounts are correct **by construction**, so the single biggest class of
42
42
  EN-16931 failures (the BR-CO total checks) simply cannot happen.
43
43
 
44
44
  ```ts
45
- import { renderZugferd } from "@jasy/zugferd";
45
+ import { renderZugferd } from "@jasy/e-invoice";
46
46
 
47
47
  const { bytes, xml } = await renderZugferd({
48
48
  number: "RE-2026-001",
49
49
  issueDate: "2026-06-17",
50
50
  currency: "EUR",
51
- seller: { name: "Northwind Studio GmbH", vatId: "DE123456789",
52
- address: { city: "Berlin", postCode: "10115", country: "DE" } },
53
- buyer: { name: "Globex Corporation Ltd",
54
- address: { city: "Munich", postCode: "80331", country: "DE" } },
51
+ seller: {
52
+ name: "Northwind Studio GmbH",
53
+ vatId: "DE123456789",
54
+ address: { city: "Berlin", postCode: "10115", country: "DE" },
55
+ },
56
+ buyer: {
57
+ name: "Globex Corporation Ltd",
58
+ address: { city: "Munich", postCode: "80331", country: "DE" },
59
+ },
55
60
  lines: [
56
- { name: "Brand identity design", quantity: 2, unit: "HUR", netUnitPrice: 100,
57
- vat: { category: "S", ratePercent: 19 } },
61
+ {
62
+ name: "Brand identity design",
63
+ quantity: 2,
64
+ unit: "HUR",
65
+ netUnitPrice: 100,
66
+ vat: { category: "S", ratePercent: 19 },
67
+ },
58
68
  ],
59
69
  });
60
70
  // bytes -> a valid ZUGFeRD PDF/A-3 · xml -> the embedded EN-16931 XML
@@ -107,8 +117,8 @@ const pdf = await renderToBytes(
107
117
  ```
108
118
 
109
119
  Real text layout (Adobe AFM metrics, kerning, word-wrap), flexbox-style `gap` / `justify` / `align`,
110
- boxes with radius and alpha, images, custom TrueType fonts, and **real pagination** - content that
111
- overflows flows onto the next page, headers and footers repeat.
120
+ boxes with radius and alpha, images, custom TrueType fonts, **AES-256 password encryption**, and **real
121
+ pagination** - content that overflows flows onto the next page, headers and footers repeat.
112
122
 
113
123
  ---
114
124
 
@@ -122,12 +132,15 @@ verify all of it:
122
132
  740 KB font ships as a ~76 KB subset, and the text stays copy- and searchable.
123
133
  - **Compression.** Content streams and images are FlateDecode-compressed; the spreadsheets `jasy export`
124
134
  writes are real `.xlsx` ZIPs we deflate with our own writer and CRC32, zero dependencies.
135
+ - **AES-256 encryption, pure JS.** Lock a PDF with `renderToBytes(doc, { encrypt: { userPassword } })` - the
136
+ newest standard handler (AES-256, R6 / ISO 32000-2) via the platform WebCrypto: no native crypto, no deps,
137
+ the same code in Node **and** the browser. Owner password + permissions optional.
125
138
  - **Real font metrics.** Text is laid out with the Adobe AFM metrics of the standard-14 fonts - kerning
126
- and word-wrap are *computed*, not guessed.
139
+ and word-wrap are _computed_, not guessed.
127
140
  - **PDF/A-3, matched not approximated.** The conformance graph is hand-built and **passes veraPDF**, the
128
141
  official ISO 19005 validator.
129
142
  - **Byte-exact round-trips.** Generate and parse are inverses: `generate → parse → regenerate` reproduces
130
- the identical XML. 307 tests hold the line.
143
+ the identical XML. Over 900 tests hold the line across the workspace.
131
144
  - **Schematron, local.** The official EN-16931 + XRechnung rules run via saxon-js (the real XSLT, in pure
132
145
  JS) - no Java, no upload.
133
146
 
@@ -135,18 +148,19 @@ verify all of it:
135
148
 
136
149
  ## Packages
137
150
 
138
- | Package | What it is |
139
- | --- | --- |
140
- | **[@jasy/zugferd](https://www.npmjs.com/package/@jasy/zugferd)** | ZUGFeRD / XRechnung: your data → PDF/A-3 + EN-16931 XML, with local validation. **The prize.** |
141
- | **[@jasy/cli](https://www.npmjs.com/package/@jasy/cli)** | the `jasy` terminal: read · validate · export, headless **and** interactive |
142
- | **[@jasy/pdf](https://www.npmjs.com/package/@jasy/pdf)** | the declarative, Flutter-style PDF layout engine that powers them |
151
+ | Package | What it is |
152
+ | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
153
+ | **[@jasy/e-invoice](https://npmx.dev/@jasy/e-invoice)** | ZUGFeRD / XRechnung: your data → PDF/A-3 + EN-16931 XML, with local validation. **The prize.** |
154
+ | **[@jasy/cli](https://npmx.dev/@jasy/cli)** | the `jasy` terminal: read · validate · export, headless **and** interactive |
155
+ | **[@jasy/pdf](https://npmx.dev/@jasy/pdf)** | the declarative, Flutter-style PDF layout engine that powers them |
143
156
 
144
157
  ---
145
158
 
146
159
  ## Why you can trust it
147
160
 
148
- - **307 tests, green.** The generator and the parser are **byte-exact inverses**:
149
- `generate → parse → regenerate` reproduces the identical XML. Nothing is silently lost.
161
+ - **Over 900 tests, green** across the workspace (850 in the engine alone). The generator and the parser
162
+ are **byte-exact inverses**: `generate → parse → regenerate` reproduces the identical XML. Nothing is
163
+ silently lost.
150
164
  - **The same rules the authorities use** - the official KoSIT EN-16931 + XRechnung Schematron, and the
151
165
  official **veraPDF** for the full ISO 19005 (PDF/A) check.
152
166
  - **Proven on real third-party invoices**, not only our own.
@@ -158,7 +172,7 @@ verify all of it:
158
172
 
159
173
  jasy targets the documents that matter here: invoices, reports, quotes, datasheets. It is **not** a
160
174
  LaTeX / WeasyPrint replacement - no microtypography, hyphenation or bidi, and arbitrary multi-page flow
161
- of *any* content is still maturing. For e-invoices (a table, totals, a footer) it is complete - they
175
+ of _any_ content is still maturing. For e-invoices (a table, totals, a footer) it is complete - they
162
176
  even paginate. We would rather under-promise and over-deliver in the demo above.
163
177
 
164
178
  > **Status:** young and pre-1.0. The API can still shift between minor versions. Everything shown here
@@ -1,4 +1,4 @@
1
- import { PDFElement } from "../elements/pdf-element";
1
+ import { PDFElement } from "../elements/pdf-element.js";
2
2
  /**
3
3
  * Lets a container factory be called either `F(children)` or `F(opts, children)` - the
4
4
  * same ergonomic shape across Document/Page/Column/Row/Box. When the first argument is the
package/dist/api/args.js CHANGED
@@ -1,13 +1,10 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.splitArgs = splitArgs;
4
1
  /**
5
2
  * Lets a container factory be called either `F(children)` or `F(opts, children)` - the
6
3
  * same ergonomic shape across Document/Page/Column/Row/Box. When the first argument is the
7
4
  * child array, options default to empty.
8
5
  */
9
- function splitArgs(a, b) {
6
+ export function splitArgs(a, b) {
10
7
  if (Array.isArray(a))
11
8
  return { opts: {}, children: a };
12
- return { opts: a, children: b !== null && b !== void 0 ? b : [] };
9
+ return { opts: a, children: b ?? [] };
13
10
  }
@@ -1,11 +1,11 @@
1
- import { Color } from "../common/color";
1
+ import { Color } from "../common/color.js";
2
2
  /**
3
- * Every way to name a colour. The FORM picks the convention, so there is never a guess
3
+ * Every way to name a color. The FORM picks the convention, so there is never a guess
4
4
  * (locked design §2):
5
5
  *
6
6
  * | form | example | meaning |
7
7
  * |---------------------------|--------------------------|--------------------------------|
8
- * | named (full CSS set) | `"steelblue"`, `"transparent"` | the ~148 CSS colour names |
8
+ * | named (full CSS set) | `"steelblue"`, `"transparent"` | the ~148 CSS color names |
9
9
  * | hex 6 / 3 | `"#1450aa"` / `"#14a"` | CSS RGB |
10
10
  * | hex 8 / 4 | `"#1450aacc"` / `"#14ac"`| CSS RGBA (alpha LAST) |
11
11
  * | number | `0xff1450aa` | Flutter ARGB (alpha FIRST) |
@@ -19,6 +19,6 @@ export declare function rgb(r: number, g: number, b: number): Color;
19
19
  export declare function rgba(r: number, g: number, b: number, a: number): Color;
20
20
  /**
21
21
  * Normalizes any `ColorInput` to an engine `Color`. The single funnel every factory uses,
22
- * so a colour means the same thing no matter how it was written.
22
+ * so a color means the same thing no matter how it was written.
23
23
  */
24
24
  export declare function toColor(input: ColorInput): Color;
package/dist/api/color.js CHANGED
@@ -1,24 +1,18 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.rgb = rgb;
4
- exports.rgba = rgba;
5
- exports.toColor = toColor;
6
- const color_1 = require("../common/color");
1
+ import { Color } from "../common/color.js";
7
2
  /** Channels 0–255, fully opaque. */
8
- function rgb(r, g, b) {
9
- return new color_1.Color(r, g, b, 1);
3
+ export function rgb(r, g, b) {
4
+ return new Color(r, g, b, 1);
10
5
  }
11
6
  /** Channels 0–255, alpha 0–1. */
12
- function rgba(r, g, b, a) {
13
- return new color_1.Color(r, g, b, a);
7
+ export function rgba(r, g, b, a) {
8
+ return new Color(r, g, b, a);
14
9
  }
15
10
  /**
16
11
  * Normalizes any `ColorInput` to an engine `Color`. The single funnel every factory uses,
17
- * so a colour means the same thing no matter how it was written.
12
+ * so a color means the same thing no matter how it was written.
18
13
  */
19
- function toColor(input) {
20
- var _a;
21
- if (input instanceof color_1.Color)
14
+ export function toColor(input) {
15
+ if (input instanceof Color)
22
16
  return input;
23
17
  if (typeof input === "number")
24
18
  return fromArgbNumber(input);
@@ -27,7 +21,7 @@ function toColor(input) {
27
21
  return fromHex(s);
28
22
  const named = CSS_COLORS[s];
29
23
  if (named)
30
- return new color_1.Color(named[0], named[1], named[2], (_a = named[3]) !== null && _a !== void 0 ? _a : 1);
24
+ return new Color(named[0], named[1], named[2], named[3] ?? 1);
31
25
  throw new Error(`Unknown color: "${input}"`);
32
26
  }
33
27
  /** Flutter ARGB: 0xAARRGGBB, alpha FIRST. A 6-digit number has alpha 0x00 = transparent. */
@@ -36,7 +30,7 @@ function fromArgbNumber(n) {
36
30
  const r = (n >>> 16) & 0xff;
37
31
  const g = (n >>> 8) & 0xff;
38
32
  const b = n & 0xff;
39
- return new color_1.Color(r, g, b, a / 255);
33
+ return new Color(r, g, b, a / 255);
40
34
  }
41
35
  /** CSS hex: #RGB, #RGBA, #RRGGBB, #RRGGBBAA. Alpha is LAST. */
42
36
  function fromHex(s) {
@@ -58,7 +52,7 @@ function fromHex(s) {
58
52
  const a = full.length === 8 ? parseInt(full.slice(6, 8), 16) / 255 : 1;
59
53
  if ([r, g, b].some(Number.isNaN))
60
54
  throw new Error(`Invalid hex color: "${s}"`);
61
- return new color_1.Color(r, g, b, a);
55
+ return new Color(r, g, b, a);
62
56
  }
63
57
  // The full CSS named-color set (~148, incl. the synonyms grey/gray and `transparent`).
64
58
  // [r, g, b] or [r, g, b, alpha]. Lower-cased keys; `toColor` lower-cases the input.
@@ -1,10 +1,11 @@
1
- import { ImageElement, CustomImage } from "../elements/image-element";
2
- import { PDFElement } from "../elements/pdf-element";
3
- import { ColorInput } from "./color";
4
- import { Insets } from "./insets";
1
+ import { ImageElement, CustomImage } from "../elements/image-element.js";
2
+ import { PDFElement } from "../elements/pdf-element.js";
3
+ import { ColorInput } from "./color.js";
4
+ import { Insets } from "./insets.js";
5
+ import { BoundsInput, SizeInput, RadiusInput } from "./dimension.js";
5
6
  /** A horizontal rule (locked §4). */
6
7
  export interface DividerOptions {
7
- /** Line colour (default a light grey). */
8
+ /** Line color (default a light gray). */
8
9
  color?: ColorInput;
9
10
  /** Line thickness in points (default 1). */
10
11
  thickness?: number;
@@ -14,20 +15,26 @@ export interface DividerOptions {
14
15
  /**
15
16
  * A horizontal rule that spans the parent's width. Maps to a `LineElement` (hiding its
16
17
  * `xEnd`/`yEnd` mechanics) wrapped in a `PaddingElement` - the line has no height of its
17
- * own, so the padding gives it vertical room and centres the rule. Use inside a Column.
18
+ * own, so the padding gives it vertical room and centers the rule. Use inside a Column.
18
19
  */
19
20
  export declare function Divider(opts?: DividerOptions): PDFElement;
20
- /** An image source: a local file path, or a `CustomImage` (e.g. a browser-supplied image). */
21
- export type ImageSource = string | CustomImage;
21
+ /** An image source: a local file path (Node), raw bytes (e.g. a browser fetch/upload), or a `CustomImage`. */
22
+ export type ImageSource = string | Uint8Array | CustomImage;
22
23
  /** How the image fills its box (locked §4). Mirrors CSS `object-fit`. */
23
24
  export type ImageFit = "none" | "contain" | "cover" | "fill";
24
- export interface ImageOptions {
25
- width?: number;
26
- height?: number;
27
- /** Fit within the box (default `none`). */
25
+ export interface ImageOptions extends BoundsInput {
26
+ /** Size on each axis: points (fixed) or a percentage string like `"50%"` (a fraction of the offered
27
+ * space). Pin exactly ONE axis and the other follows the image's aspect ratio (CSS `height: auto`). */
28
+ width?: SizeInput;
29
+ height?: SizeInput;
30
+ /** Fit within the box. Default `none`, except where the box was DERIVED rather than given outright -
31
+ * exactly one axis pinned, or an `aspectRatio` - which defaults to `fill` so the image scales into it. */
28
32
  fit?: ImageFit;
29
33
  /** Corner radius in points (rounds the image box). */
30
- radius?: number;
34
+ radius?: RadiusInput;
35
+ /** Alternate text for accessibility (tagged PDF): describes the image for screen readers. With `alt`
36
+ * the image is a `Figure`; without it (and when rendered `accessible`) it counts as decoration. */
37
+ alt?: string;
31
38
  }
32
39
  /**
33
40
  * An image. `src` is a local file path (wrapped in a `CustomLocalImage`) or a ready
@@ -1,50 +1,65 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Divider = Divider;
4
- exports.Image = Image;
5
- const line_element_1 = require("../elements/line-element");
6
- const padding_element_1 = require("../elements/layout/padding-element");
7
- const image_element_1 = require("../elements/image-element");
8
- const color_1 = require("./color");
9
- const insets_1 = require("./insets");
1
+ import { LineElement } from "../elements/line-element.js";
2
+ import { PaddingElement } from "../elements/layout/padding-element.js";
3
+ import { ImageElement, CustomLocalImage, CustomBytesImage, BoxFit, } from "../elements/image-element.js";
4
+ import { toColor } from "./color.js";
5
+ import { toEdges } from "./insets.js";
6
+ import { toBounds, toDimension, toRadius, } from "./dimension.js";
10
7
  const DEFAULT_DIVIDER_COLOR = "lightgray";
11
8
  const DEFAULT_DIVIDER_MARGIN = { y: 6 };
12
9
  /**
13
10
  * A horizontal rule that spans the parent's width. Maps to a `LineElement` (hiding its
14
11
  * `xEnd`/`yEnd` mechanics) wrapped in a `PaddingElement` - the line has no height of its
15
- * own, so the padding gives it vertical room and centres the rule. Use inside a Column.
12
+ * own, so the padding gives it vertical room and centers the rule. Use inside a Column.
16
13
  */
17
- function Divider(opts = {}) {
18
- var _a, _b, _c;
19
- const line = new line_element_1.LineElement({
14
+ export function Divider(opts = {}) {
15
+ const line = new LineElement({
20
16
  x: 0,
21
17
  y: 0,
22
18
  xEnd: 0, // resolved to the parent's width at layout time
23
19
  yEnd: 0, // horizontal: no vertical span
24
- color: (0, color_1.toColor)((_a = opts.color) !== null && _a !== void 0 ? _a : DEFAULT_DIVIDER_COLOR),
25
- strokeWidth: (_b = opts.thickness) !== null && _b !== void 0 ? _b : 1,
20
+ color: toColor(opts.color ?? DEFAULT_DIVIDER_COLOR),
21
+ strokeWidth: opts.thickness ?? 1,
26
22
  });
27
- return new padding_element_1.PaddingElement({
28
- margin: (0, insets_1.toEdges)((_c = opts.margin) !== null && _c !== void 0 ? _c : DEFAULT_DIVIDER_MARGIN),
23
+ return new PaddingElement({
24
+ margin: toEdges(opts.margin ?? DEFAULT_DIVIDER_MARGIN),
29
25
  child: line,
30
26
  });
31
27
  }
32
28
  const FIT = {
33
- none: image_element_1.BoxFit.none,
34
- contain: image_element_1.BoxFit.contain,
35
- cover: image_element_1.BoxFit.cover,
36
- fill: image_element_1.BoxFit.fill,
29
+ none: BoxFit.none,
30
+ contain: BoxFit.contain,
31
+ cover: BoxFit.cover,
32
+ fill: BoxFit.fill,
37
33
  };
38
34
  /**
39
35
  * An image. `src` is a local file path (wrapped in a `CustomLocalImage`) or a ready
40
36
  * `CustomImage` for non-filesystem sources. Maps to an `ImageElement`.
41
37
  */
42
- function Image(src, opts = {}) {
43
- return new image_element_1.ImageElement({
44
- image: typeof src === "string" ? new image_element_1.CustomLocalImage(src) : src,
45
- width: opts.width,
46
- height: opts.height,
47
- fit: opts.fit ? FIT[opts.fit] : undefined,
48
- radius: opts.radius,
49
- });
38
+ export function Image(src, opts = {}) {
39
+ const w = opts.width !== undefined ? toDimension(opts.width) : undefined;
40
+ const h = opts.height !== undefined ? toDimension(opts.height) : undefined;
41
+ // ANY explicit size scales the image into the box (fit: fill), which is what `<img width height>`
42
+ // does in HTML and what react-pdf does. Two pinned axes used to keep `none` - drawing the image at
43
+ // its pixel size regardless - so a logo asked for at 48x48 spanned a third of the page.
44
+ // An image with NO size given still draws at its intrinsic size, as an unsized `<img>` does.
45
+ const sized = opts.width !== undefined || opts.height !== undefined || opts.aspectRatio !== undefined;
46
+ const fit = opts.fit ? FIT[opts.fit] : sized ? BoxFit.fill : undefined;
47
+ return new ImageElement({
48
+ image: typeof src === "string"
49
+ ? new CustomLocalImage(src)
50
+ : src instanceof Uint8Array
51
+ ? new CustomBytesImage(src)
52
+ : src,
53
+ width: w?.points,
54
+ height: h?.points,
55
+ widthFactor: w?.factor,
56
+ heightFactor: h?.factor,
57
+ ...toBounds(opts),
58
+ fit,
59
+ radius: opts.radius !== undefined ? toRadius(opts.radius) : undefined,
60
+ alt: opts.alt,
61
+ })
62
+ .withAlignSelf(opts.alignSelf)
63
+ .withOrder(opts.order)
64
+ .withFlexShrink(opts.flexShrink);
50
65
  }
@@ -1,5 +1,5 @@
1
- import { PDFElement } from "../elements/pdf-element";
2
- import { PDFDocumentElement } from "../elements/pdf-document-element";
1
+ import { PDFElement } from "../elements/pdf-element.js";
2
+ import { PDFDocumentElement } from "../elements/pdf-document-element.js";
3
3
  /**
4
4
  * The framework-agnostic contract (the firewall). A binding (Vue/React, or any tree-builder)
5
5
  * produces a tree of these plain descriptors; `build` turns each node into an engine element
@@ -1,29 +1,26 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.registerElement = registerElement;
4
- exports.build = build;
5
- exports.buildDocument = buildDocument;
6
- const text_1 = require("./text");
7
- const layout_1 = require("./layout");
8
- const content_1 = require("./content");
9
- const structure_1 = require("./structure");
1
+ import { Text, Paragraph, span } from "./text.js";
2
+ import { Column, Row, Box, Padding, Spacer, PageBreak, keepTogether, Expanded, Positioned, Link, Anchor, Bookmark, Rotated, RotatedBox, } from "./layout.js";
3
+ import { Divider, Image } from "./content.js";
4
+ import { Page, Document, DefaultTextStyle } from "./structure.js";
5
+ import { PageNumber, PageCount } from "./page-builder.js";
6
+ import { Table } from "./table.js";
7
+ import { Checkbox, Dropdown, ListBox, PushButton, RadioGroup, SignatureField, TextField, } from "./forms.js";
10
8
  // Element children (Column/Row/Box/Page/…) → built recursively. Text/Paragraph children are
11
9
  // strings or `span` descriptors and become content instead.
12
10
  function elementChildren(children) {
13
11
  return children.map(build);
14
12
  }
15
13
  function textOf(node) {
16
- var _a;
17
14
  if (typeof node === "string")
18
15
  return node;
19
- const first = (_a = node.children) === null || _a === void 0 ? void 0 : _a[0];
16
+ const first = node.children?.[0];
20
17
  return typeof first === "string" ? first : "";
21
18
  }
22
19
  function toSegment(c) {
23
20
  if (typeof c === "string")
24
- return (0, text_1.span)(c);
21
+ return span(c);
25
22
  if (c.type === "span")
26
- return (0, text_1.span)(textOf(c), c.props);
23
+ return span(textOf(c), c.props);
27
24
  throw new Error(`A Text child must be a string or a span, got "${c.type}"`);
28
25
  }
29
26
  function textContent(children) {
@@ -31,40 +28,133 @@ function textContent(children) {
31
28
  return children[0];
32
29
  return children.map(toSegment);
33
30
  }
31
+ // A `<TableCell>`'s content → a `Cell`: a lone string stays a string (the table wraps it in Text);
32
+ // otherwise its children are built (a single element, or a Column of several).
33
+ function buildCell(cell) {
34
+ if (typeof cell === "string")
35
+ return cell;
36
+ const kids = cell.children ?? [];
37
+ if (kids.length === 1 && typeof kids[0] === "string")
38
+ return kids[0];
39
+ if (kids.length === 0)
40
+ return "";
41
+ const els = kids.map(build);
42
+ return els.length === 1 ? els[0] : Column(els);
43
+ }
44
+ // A `<TableRow>`'s `<TableCell>` children → one row of `Cell`s.
45
+ function rowCells(row) {
46
+ return (row.children ?? [])
47
+ .filter((c) => typeof c !== "string" && c.type === "table-cell")
48
+ .map(buildCell);
49
+ }
50
+ // A `#header` / `#footer` slot's content → one element (a Column if it holds several; strings → Text).
51
+ function slotElement(node) {
52
+ const els = (node.children ?? []).map((c) => (typeof c === "string" ? Text(c) : build(c)));
53
+ return els.length === 1 ? els[0] : Column(els);
54
+ }
55
+ // The single child a wrapper takes (`Link`, `Anchor`, `Bookmark`, `Rotated`, ...). Several children are
56
+ // stacked in a Column, so `<JasyLink><Text/><Text/></JasyLink>` behaves like the factory would.
57
+ function wrappedChild(children) {
58
+ const els = children.map((c) => (typeof c === "string" ? Text(c) : build(c)));
59
+ if (els.length === 0)
60
+ throw new Error("A wrapper element needs exactly one child");
61
+ return els.length === 1 ? els[0] : Column(els);
62
+ }
34
63
  const REGISTRY = {
35
- document: (props, children) => (0, structure_1.Document)(props, elementChildren(children)),
36
- page: (props, children) => (0, structure_1.Page)(props, elementChildren(children)),
37
- column: (props, children) => (0, layout_1.Column)(props, elementChildren(children)),
38
- row: (props, children) => (0, layout_1.Row)(props, elementChildren(children)),
39
- box: (props, children) => (0, layout_1.Box)(props, elementChildren(children)),
40
- padding: (props, children) => { var _a; return (0, layout_1.Padding)((_a = props.insets) !== null && _a !== void 0 ? _a : 0, elementChildren(children)[0]); },
41
- expanded: (props, children) => (0, layout_1.Expanded)(props, elementChildren(children)[0]),
42
- spacer: (props) => (0, layout_1.Spacer)(props === null || props === void 0 ? void 0 : props.flex),
43
- divider: (props) => (0, content_1.Divider)(props),
44
- image: (props) => (0, content_1.Image)(props.src, props),
45
- text: (props, children) => (0, text_1.Text)(textContent(children), props),
46
- paragraph: (props, children) => (0, text_1.Paragraph)(textContent(children), props),
64
+ document: (props, children) => {
65
+ const doc = Document(props, elementChildren(children));
66
+ // A binding can register fonts declaratively: `fonts: { Name: bytes | path | family }`.
67
+ if (props.fonts) {
68
+ for (const [name, src] of Object.entries(props.fonts))
69
+ doc.addFont(name, src);
70
+ }
71
+ return doc;
72
+ },
73
+ page: (props, children) => {
74
+ // `#header` / `#footer` arrive as `page-header` / `page-footer` marker children (read raw); the rest
75
+ // is the body. Both repeat on every physical page the body paginates onto.
76
+ const slot = (type) => {
77
+ const m = children.find((c) => typeof c !== "string" && c.type === type);
78
+ return m ? slotElement(m) : undefined;
79
+ };
80
+ const body = children.filter((c) => typeof c === "string" || (c.type !== "page-header" && c.type !== "page-footer"));
81
+ return Page({ ...props, header: slot("page-header"), footer: slot("page-footer") }, elementChildren(body));
82
+ },
83
+ column: (props, children) => Column(props, elementChildren(children)),
84
+ row: (props, children) => Row(props, elementChildren(children)),
85
+ box: (props, children) => Box(props, elementChildren(children)),
86
+ padding: (props, children) => Padding(props.insets ?? 0, elementChildren(children)[0]),
87
+ expanded: (props, children) => Expanded(props, elementChildren(children)[0]),
88
+ spacer: (props) => Spacer(props?.flex),
89
+ "page-break": () => PageBreak(),
90
+ "keep-together": (_props, children) => keepTogether(elementChildren(children)),
91
+ divider: (props) => Divider(props),
92
+ image: (props) => Image(props.src, props),
93
+ text: (props, children) => Text(textContent(children), props),
94
+ paragraph: (props, children) => Paragraph(textContent(children), props),
95
+ // `<Table>` reads its `<TableRow>`/`<TableCell>` structure raw; one row may be marked `header`.
96
+ table: (props, children) => {
97
+ const rows = children.filter((c) => typeof c !== "string" && c.type === "table-row");
98
+ const header = rows.find((r) => r.props?.header);
99
+ const body = rows.filter((r) => !r.props?.header);
100
+ return Table({ ...props, header: header ? rowCells(header) : undefined }, body.map(rowCells));
101
+ },
102
+ positioned: (props, children) => Positioned(props, elementChildren(children)[0]),
103
+ "default-text-style": (props, children) => DefaultTextStyle(props, elementChildren(children)),
104
+ // Navigation. `Link` takes exactly one of `href` (a URL) or `to` (an `Anchor` name) - the factory
105
+ // enforces that, so a template typo surfaces as an error instead of a dead link.
106
+ link: (props, children) => Link(props, wrappedChild(children)),
107
+ anchor: (props, children) => Anchor(props, wrappedChild(children)),
108
+ bookmark: (props, children) => Bookmark(props, wrappedChild(children)),
109
+ // Transforms.
110
+ rotated: (props, children) => Rotated(props, wrappedChild(children)),
111
+ "rotated-box": (props, children) => RotatedBox(props, wrappedChild(children)),
112
+ // Page numbers. `PageBuilder` itself is deliberately NOT exposed: it takes a closure, which a template
113
+ // cannot express. These two cover what the closure exists for.
114
+ "page-number": (props) => PageNumber(props),
115
+ "page-count": (props) => PageCount(props),
116
+ // Form fields. The three list-like ones take their choices as a SECOND argument in TypeScript; a
117
+ // template has no second argument, so here they arrive as an `options` prop. `choiceList` accepts both
118
+ // the short spelling and the explicit one, because an export value and its label are usually the same
119
+ // string and having to write it twice is noise.
120
+ "text-field": (props) => TextField(props),
121
+ checkbox: (props) => Checkbox(props),
122
+ "radio-group": (props) => RadioGroup(props, radioChoices(props.options)),
123
+ dropdown: (props) => Dropdown(props, choiceList(props.options)),
124
+ "list-box": (props) => ListBox(props, choiceList(props.options)),
125
+ "push-button": (props) => PushButton(props),
126
+ "signature-field": (props) => SignatureField(props),
47
127
  };
128
+ /** `["S", "M"]` or `[{ value: "DE", label: "Germany" }]` - both mean the same thing to a choice field. */
129
+ function choiceList(raw) {
130
+ if (!Array.isArray(raw)) {
131
+ throw new Error("jasy: a choice field needs an `options` array (strings or {value, label}).");
132
+ }
133
+ return raw;
134
+ }
135
+ /** A radio button always shows a label, so the short spelling means "label is the value". */
136
+ function radioChoices(raw) {
137
+ return choiceList(raw).map((o) => typeof o === "string" ? { value: o, label: o } : { value: o.value, label: o.label ?? o.value });
138
+ }
48
139
  /**
49
140
  * Registers a custom element type, so a binding (or a user-defined component) can introduce
50
141
  * its own tag that resolves to an engine element through this same seam. Overwrites an
51
142
  * existing type of the same name.
52
143
  */
53
- function registerElement(type, factory) {
144
+ export function registerElement(type, factory) {
54
145
  REGISTRY[type] = factory;
55
146
  }
56
147
  /** Turns one descriptor node (or bare string → `Text`) into an engine element. */
57
- function build(node) {
58
- var _a, _b;
148
+ export function build(node) {
59
149
  if (typeof node === "string")
60
- return (0, text_1.Text)(node);
150
+ return Text(node);
61
151
  const factory = REGISTRY[node.type];
62
152
  if (!factory)
63
153
  throw new Error(`Unknown element type: "${node.type}"`);
64
- return factory((_a = node.props) !== null && _a !== void 0 ? _a : {}, (_b = node.children) !== null && _b !== void 0 ? _b : []);
154
+ return factory(node.props ?? {}, node.children ?? []);
65
155
  }
66
156
  /** Builds a descriptor tree whose root is a `document` into the renderable root element. */
67
- function buildDocument(root) {
157
+ export function buildDocument(root) {
68
158
  if (root.type !== "document")
69
159
  throw new Error(`Expected a "document" root, got "${root.type}"`);
70
160
  return build(root);