@arslankhan/doc-forge 0.0.0-stage → 0.1.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 (246) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/LICENSE +13 -0
  3. package/LICENSING.md +80 -0
  4. package/README.md +547 -2
  5. package/RELEASE.md +50 -0
  6. package/dist/binding-path.d.ts +15 -0
  7. package/dist/binding-path.d.ts.map +1 -0
  8. package/dist/binding-path.js +37 -0
  9. package/dist/binding-path.js.map +1 -0
  10. package/dist/binding.d.ts +91 -0
  11. package/dist/binding.d.ts.map +1 -0
  12. package/dist/binding.js +229 -0
  13. package/dist/binding.js.map +1 -0
  14. package/dist/designer/Canvas.d.ts +51 -0
  15. package/dist/designer/Canvas.d.ts.map +1 -0
  16. package/dist/designer/Canvas.js +188 -0
  17. package/dist/designer/Canvas.js.map +1 -0
  18. package/dist/designer/DocumentDesigner.d.ts +14 -0
  19. package/dist/designer/DocumentDesigner.d.ts.map +1 -0
  20. package/dist/designer/DocumentDesigner.js +351 -0
  21. package/dist/designer/DocumentDesigner.js.map +1 -0
  22. package/dist/designer/HtmlPreview.d.ts +9 -0
  23. package/dist/designer/HtmlPreview.d.ts.map +1 -0
  24. package/dist/designer/HtmlPreview.js +20 -0
  25. package/dist/designer/HtmlPreview.js.map +1 -0
  26. package/dist/designer/ImageBindingFields.d.ts +11 -0
  27. package/dist/designer/ImageBindingFields.d.ts.map +1 -0
  28. package/dist/designer/ImageBindingFields.js +106 -0
  29. package/dist/designer/ImageBindingFields.js.map +1 -0
  30. package/dist/designer/InvoiceFields.d.ts +13 -0
  31. package/dist/designer/InvoiceFields.d.ts.map +1 -0
  32. package/dist/designer/InvoiceFields.js +85 -0
  33. package/dist/designer/InvoiceFields.js.map +1 -0
  34. package/dist/designer/PreviewDataDialog.d.ts +13 -0
  35. package/dist/designer/PreviewDataDialog.d.ts.map +1 -0
  36. package/dist/designer/PreviewDataDialog.js +16 -0
  37. package/dist/designer/PreviewDataDialog.js.map +1 -0
  38. package/dist/designer/PropertiesPanel.d.ts +15 -0
  39. package/dist/designer/PropertiesPanel.d.ts.map +1 -0
  40. package/dist/designer/PropertiesPanel.js +63 -0
  41. package/dist/designer/PropertiesPanel.js.map +1 -0
  42. package/dist/designer/ReportFields.d.ts +14 -0
  43. package/dist/designer/ReportFields.d.ts.map +1 -0
  44. package/dist/designer/ReportFields.js +257 -0
  45. package/dist/designer/ReportFields.js.map +1 -0
  46. package/dist/designer/StyleFields.d.ts +16 -0
  47. package/dist/designer/StyleFields.d.ts.map +1 -0
  48. package/dist/designer/StyleFields.js +138 -0
  49. package/dist/designer/StyleFields.js.map +1 -0
  50. package/dist/designer/TextBindingFields.d.ts +11 -0
  51. package/dist/designer/TextBindingFields.d.ts.map +1 -0
  52. package/dist/designer/TextBindingFields.js +105 -0
  53. package/dist/designer/TextBindingFields.js.map +1 -0
  54. package/dist/designer/Toolbar.d.ts +52 -0
  55. package/dist/designer/Toolbar.d.ts.map +1 -0
  56. package/dist/designer/Toolbar.js +62 -0
  57. package/dist/designer/Toolbar.js.map +1 -0
  58. package/dist/designer/Toolbox.d.ts +8 -0
  59. package/dist/designer/Toolbox.d.ts.map +1 -0
  60. package/dist/designer/Toolbox.js +43 -0
  61. package/dist/designer/Toolbox.js.map +1 -0
  62. package/dist/designer/border-preview.d.ts +15 -0
  63. package/dist/designer/border-preview.d.ts.map +1 -0
  64. package/dist/designer/border-preview.js +34 -0
  65. package/dist/designer/border-preview.js.map +1 -0
  66. package/dist/designer/history.d.ts +13 -0
  67. package/dist/designer/history.d.ts.map +1 -0
  68. package/dist/designer/history.js +51 -0
  69. package/dist/designer/history.js.map +1 -0
  70. package/dist/designer/index.d.ts +5 -0
  71. package/dist/designer/index.d.ts.map +1 -0
  72. package/dist/designer/index.js +3 -0
  73. package/dist/designer/index.js.map +1 -0
  74. package/dist/designer/page-geometry.d.ts +35 -0
  75. package/dist/designer/page-geometry.d.ts.map +1 -0
  76. package/dist/designer/page-geometry.js +123 -0
  77. package/dist/designer/page-geometry.js.map +1 -0
  78. package/dist/designer/styles.d.ts +3 -0
  79. package/dist/designer/styles.d.ts.map +1 -0
  80. package/dist/designer/styles.js +594 -0
  81. package/dist/designer/styles.js.map +1 -0
  82. package/dist/designer/template-edits.d.ts +26 -0
  83. package/dist/designer/template-edits.d.ts.map +1 -0
  84. package/dist/designer/template-edits.js +323 -0
  85. package/dist/designer/template-edits.js.map +1 -0
  86. package/dist/designer/theme.d.ts +31 -0
  87. package/dist/designer/theme.d.ts.map +1 -0
  88. package/dist/designer/theme.js +92 -0
  89. package/dist/designer/theme.js.map +1 -0
  90. package/dist/examples/bound-invoice.d.ts +143 -0
  91. package/dist/examples/bound-invoice.d.ts.map +1 -0
  92. package/dist/examples/bound-invoice.js +99 -0
  93. package/dist/examples/bound-invoice.js.map +1 -0
  94. package/dist/examples/full-invoice-data.d.ts +81 -0
  95. package/dist/examples/full-invoice-data.d.ts.map +1 -0
  96. package/dist/examples/full-invoice-data.js +86 -0
  97. package/dist/examples/full-invoice-data.js.map +1 -0
  98. package/dist/examples/full-invoice.d.ts +402 -0
  99. package/dist/examples/full-invoice.d.ts.map +1 -0
  100. package/dist/examples/full-invoice.js +199 -0
  101. package/dist/examples/full-invoice.js.map +1 -0
  102. package/dist/examples/invoice-data.d.ts +51 -0
  103. package/dist/examples/invoice-data.d.ts.map +1 -0
  104. package/dist/examples/invoice-data.js +51 -0
  105. package/dist/examples/invoice-data.js.map +1 -0
  106. package/dist/examples/invoice.d.ts +90 -0
  107. package/dist/examples/invoice.d.ts.map +1 -0
  108. package/dist/examples/invoice.js +62 -0
  109. package/dist/examples/invoice.js.map +1 -0
  110. package/dist/examples/jobs-report-data.d.ts +84 -0
  111. package/dist/examples/jobs-report-data.d.ts.map +1 -0
  112. package/dist/examples/jobs-report-data.js +79 -0
  113. package/dist/examples/jobs-report-data.js.map +1 -0
  114. package/dist/examples/jobs-report.d.ts +182 -0
  115. package/dist/examples/jobs-report.d.ts.map +1 -0
  116. package/dist/examples/jobs-report.js +96 -0
  117. package/dist/examples/jobs-report.js.map +1 -0
  118. package/dist/index.d.ts +31 -0
  119. package/dist/index.d.ts.map +1 -0
  120. package/dist/index.js +21 -0
  121. package/dist/index.js.map +1 -0
  122. package/dist/invoice/calculate.d.ts +59 -0
  123. package/dist/invoice/calculate.d.ts.map +1 -0
  124. package/dist/invoice/calculate.js +270 -0
  125. package/dist/invoice/calculate.js.map +1 -0
  126. package/dist/invoice/context.d.ts +15 -0
  127. package/dist/invoice/context.d.ts.map +1 -0
  128. package/dist/invoice/context.js +79 -0
  129. package/dist/invoice/context.js.map +1 -0
  130. package/dist/invoice/format.d.ts +10 -0
  131. package/dist/invoice/format.d.ts.map +1 -0
  132. package/dist/invoice/format.js +95 -0
  133. package/dist/invoice/format.js.map +1 -0
  134. package/dist/license/capabilities.d.ts +17 -0
  135. package/dist/license/capabilities.d.ts.map +1 -0
  136. package/dist/license/capabilities.js +46 -0
  137. package/dist/license/capabilities.js.map +1 -0
  138. package/dist/license/client.d.ts +109 -0
  139. package/dist/license/client.d.ts.map +1 -0
  140. package/dist/license/client.js +346 -0
  141. package/dist/license/client.js.map +1 -0
  142. package/dist/license/codec.d.ts +5 -0
  143. package/dist/license/codec.d.ts.map +1 -0
  144. package/dist/license/codec.js +26 -0
  145. package/dist/license/codec.js.map +1 -0
  146. package/dist/license/errors.d.ts +11 -0
  147. package/dist/license/errors.d.ts.map +1 -0
  148. package/dist/license/errors.js +19 -0
  149. package/dist/license/errors.js.map +1 -0
  150. package/dist/license/features.d.ts +20 -0
  151. package/dist/license/features.d.ts.map +1 -0
  152. package/dist/license/features.js +28 -0
  153. package/dist/license/features.js.map +1 -0
  154. package/dist/license/index.d.ts +16 -0
  155. package/dist/license/index.d.ts.map +1 -0
  156. package/dist/license/index.js +9 -0
  157. package/dist/license/index.js.map +1 -0
  158. package/dist/license/provider.d.ts +59 -0
  159. package/dist/license/provider.d.ts.map +1 -0
  160. package/dist/license/provider.js +150 -0
  161. package/dist/license/provider.js.map +1 -0
  162. package/dist/license/redact.d.ts +4 -0
  163. package/dist/license/redact.d.ts.map +1 -0
  164. package/dist/license/redact.js +15 -0
  165. package/dist/license/redact.js.map +1 -0
  166. package/dist/license/store.d.ts +26 -0
  167. package/dist/license/store.d.ts.map +1 -0
  168. package/dist/license/store.js +90 -0
  169. package/dist/license/store.js.map +1 -0
  170. package/dist/license/token.d.ts +45 -0
  171. package/dist/license/token.d.ts.map +1 -0
  172. package/dist/license/token.js +195 -0
  173. package/dist/license/token.js.map +1 -0
  174. package/dist/pdf/browser-blocked.d.ts +12 -0
  175. package/dist/pdf/browser-blocked.d.ts.map +1 -0
  176. package/dist/pdf/browser-blocked.js +25 -0
  177. package/dist/pdf/browser-blocked.js.map +1 -0
  178. package/dist/pdf/browser.d.ts +12 -0
  179. package/dist/pdf/browser.d.ts.map +1 -0
  180. package/dist/pdf/browser.js +69 -0
  181. package/dist/pdf/browser.js.map +1 -0
  182. package/dist/pdf/errors.d.ts +6 -0
  183. package/dist/pdf/errors.d.ts.map +1 -0
  184. package/dist/pdf/errors.js +11 -0
  185. package/dist/pdf/errors.js.map +1 -0
  186. package/dist/pdf/index.d.ts +10 -0
  187. package/dist/pdf/index.d.ts.map +1 -0
  188. package/dist/pdf/index.js +9 -0
  189. package/dist/pdf/index.js.map +1 -0
  190. package/dist/pdf/options.d.ts +143 -0
  191. package/dist/pdf/options.d.ts.map +1 -0
  192. package/dist/pdf/options.js +322 -0
  193. package/dist/pdf/options.js.map +1 -0
  194. package/dist/pdf/render-pdf.d.ts +13 -0
  195. package/dist/pdf/render-pdf.d.ts.map +1 -0
  196. package/dist/pdf/render-pdf.js +195 -0
  197. package/dist/pdf/render-pdf.js.map +1 -0
  198. package/dist/render/appearance.d.ts +25 -0
  199. package/dist/render/appearance.d.ts.map +1 -0
  200. package/dist/render/appearance.js +149 -0
  201. package/dist/render/appearance.js.map +1 -0
  202. package/dist/render/css.d.ts +17 -0
  203. package/dist/render/css.d.ts.map +1 -0
  204. package/dist/render/css.js +170 -0
  205. package/dist/render/css.js.map +1 -0
  206. package/dist/render/errors.d.ts +6 -0
  207. package/dist/render/errors.d.ts.map +1 -0
  208. package/dist/render/errors.js +11 -0
  209. package/dist/render/errors.js.map +1 -0
  210. package/dist/render/escape.d.ts +7 -0
  211. package/dist/render/escape.d.ts.map +1 -0
  212. package/dist/render/escape.js +27 -0
  213. package/dist/render/escape.js.map +1 -0
  214. package/dist/render/image-policy.d.ts +21 -0
  215. package/dist/render/image-policy.d.ts.map +1 -0
  216. package/dist/render/image-policy.js +62 -0
  217. package/dist/render/image-policy.js.map +1 -0
  218. package/dist/render/render-html.d.ts +28 -0
  219. package/dist/render/render-html.d.ts.map +1 -0
  220. package/dist/render/render-html.js +364 -0
  221. package/dist/render/render-html.js.map +1 -0
  222. package/dist/render/render-report.d.ts +23 -0
  223. package/dist/render/render-report.d.ts.map +1 -0
  224. package/dist/render/render-report.js +313 -0
  225. package/dist/render/render-report.js.map +1 -0
  226. package/dist/report/process.d.ts +48 -0
  227. package/dist/report/process.d.ts.map +1 -0
  228. package/dist/report/process.js +347 -0
  229. package/dist/report/process.js.map +1 -0
  230. package/dist/schema.d.ts +30 -0
  231. package/dist/schema.d.ts.map +1 -0
  232. package/dist/schema.js +8 -0
  233. package/dist/schema.js.map +1 -0
  234. package/dist/serialize.d.ts +13 -0
  235. package/dist/serialize.d.ts.map +1 -0
  236. package/dist/serialize.js +42 -0
  237. package/dist/serialize.js.map +1 -0
  238. package/dist/types.d.ts +335 -0
  239. package/dist/types.d.ts.map +1 -0
  240. package/dist/types.js +40 -0
  241. package/dist/types.js.map +1 -0
  242. package/dist/validate.d.ts +15 -0
  243. package/dist/validate.d.ts.map +1 -0
  244. package/dist/validate.js +977 -0
  245. package/dist/validate.js.map +1 -0
  246. package/package.json +97 -4
package/README.md CHANGED
@@ -1,3 +1,548 @@
1
- # Temporary Holding Version
1
+ # @arslankhan/doc-forge
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Reusable document template library for Node.js and React. It defines a versioned template model, checks templates before they are saved or used, converts them to and from JSON, resolves bindings against JSON data, calculates invoice totals, prepares grouped reports, renders standalone HTML, and provides a visual designer.
4
+
5
+ The host application supplies a template and decides where the saved template is stored. Binding paths stay on the template. Preview data, calculated amounts, and resolved text are separate and are not written back into the saved template. PDF export is a separate Node.js entry point, `@arslankhan/doc-forge/pdf`, and is not part of the React designer bundle. License state is also separate from template JSON. Until a license server is configured, activation stays unconfigured and the existing features keep working.
6
+
7
+ The published package name is `@arslankhan/doc-forge`. The unscoped name `doc-forge` is too similar to the existing npm package `docforge`, so npm will not accept it. The website, price, and license API URL are not part of this package.
8
+
9
+ ## Installation
10
+
11
+ The package is ESM and requires Node.js 18 or newer. CommonJS `require()` is not a supported entry point. A CommonJS caller on Node.js 18 or newer can load it with `await import("@arslankhan/doc-forge")`. TypeScript declarations are included. Source maps point at the original files and do not embed a private key.
12
+
13
+ ```bash
14
+ npm install @arslankhan/doc-forge
15
+ ```
16
+
17
+ Install the designer peers in a React application:
18
+
19
+ ```bash
20
+ npm install @arslankhan/doc-forge react react-dom
21
+ ```
22
+
23
+ Install the PDF peers in the Node.js application that prints documents:
24
+
25
+ ```bash
26
+ npm install @arslankhan/doc-forge playwright pdf-lib
27
+ npx playwright install chromium
28
+ ```
29
+
30
+ `react` and `react-dom` are optional peers because a Node.js program can render HTML and PDF without them. `playwright` is an optional peer used only by `@arslankhan/doc-forge/pdf`. `pdf-lib` is a runtime dependency and is installed with the package. The React designer entry does not import Playwright. A browser build of `@arslankhan/doc-forge/pdf` resolves to a stub that does not import Playwright either.
31
+
32
+ Public entry points:
33
+
34
+ | Import | Runtime |
35
+ | --- | --- |
36
+ | `@arslankhan/doc-forge` | Node.js and browser-safe core: templates, binding, invoices, reports, HTML, license helpers |
37
+ | `@arslankhan/doc-forge/designer` | React designer. Requires `react` and `react-dom` |
38
+ | `@arslankhan/doc-forge/license` | The same license client as the package root, for hosts that do not want the document API |
39
+ | `@arslankhan/doc-forge/pdf` | Node.js PDF export |
40
+
41
+ Deep imports of `dist/` are not part of the public API.
42
+
43
+ ## Current capabilities
44
+
45
+ - Document templates for `generic`, `invoice`, and `report`, marked with `schemaVersion`
46
+ - Page setup: paper size, orientation, dimensions, margins, and `fixed` or `flow` layout
47
+ - Elements: `text`, `image`, `line`, `rectangle`, repeating `items`, `totals`, and `reportTable`
48
+ - Data binding paths such as `company.name`, `customer.address.city`, and `items.0.name`
49
+ - Safe resolution of those paths against a JSON object, including fallback, prefix, and suffix
50
+ - Validation that returns a list of errors
51
+ - JSON serialization, deserialization, and deep cloning
52
+ - HTML rendering of one page to a standalone document, using the same text resolver
53
+ - Node.js PDF export through Playwright Chromium, from the same HTML pipeline (`@arslankhan/doc-forge/pdf`)
54
+ - `DocumentDesigner` for visual editing: toolbox, page canvas, properties, sample-data preview, HTML preview, zoom, undo, and save
55
+ - License activation and entitlement checks. Enforcement is off until a host configures it, so these features stay available
56
+
57
+ Supported schema version: `1`.
58
+
59
+ ## Usage
60
+
61
+ The package is ESM and requires Node.js 18 or newer.
62
+
63
+ ```ts
64
+ import {
65
+ cloneTemplate,
66
+ createTemplate,
67
+ deserializeTemplate,
68
+ exampleInvoiceTemplate,
69
+ serializeTemplate,
70
+ validateTemplate,
71
+ } from "@arslankhan/doc-forge";
72
+
73
+ const template = createTemplate(exampleInvoiceTemplate);
74
+ const json = serializeTemplate(template);
75
+ const copy = cloneTemplate(deserializeTemplate(json));
76
+
77
+ const result = validateTemplate(copy);
78
+ if (!result.valid) {
79
+ console.error(result.errors);
80
+ }
81
+ ```
82
+
83
+ `createTemplate`, `serializeTemplate`, and `deserializeTemplate` throw `TemplateValidationError` when the template is invalid. `validateTemplate` returns `{ valid, errors }` instead of throwing.
84
+
85
+ ## Data binding
86
+
87
+ A text element without `binding` shows its `text` value. A text element with `binding` keeps that same `text` as the design placeholder and reads a value from JSON when you resolve or preview it.
88
+
89
+ An image element without `binding` uses its static `src`. An image element with `binding` keeps that `src` on the saved template and reads the URL from JSON instead.
90
+
91
+ ```ts
92
+ {
93
+ type: "text",
94
+ text: "Customer Name",
95
+ binding: {
96
+ path: "customer.name",
97
+ fallback: "Customer Name"
98
+ }
99
+ }
100
+ ```
101
+
102
+ `prefix` and `suffix` are optional. Leave them out when they are blank. `fallback` is optional too.
103
+
104
+ ### Path syntax
105
+
106
+ A path is a list of segments separated by dots. The first segment is a name such as `company`. Later segments are names or array indexes, such as `customer.address.city` or `items.0.name`. Indexes do not use brackets and cannot have leading zeros (`items.01` is invalid).
107
+
108
+ These segments are rejected: `__proto__`, `prototype`, and `constructor`. Paths are read with property lookup. They are not expressions, and the library never calls `eval`.
109
+
110
+ ### Resolution rules
111
+
112
+ `resolveBinding(data, path)` returns `{ status: "found", value }` or `{ status: "missing", reason }`.
113
+
114
+ - `0`, `false`, and `""` are found values.
115
+ - A missing property is `missing` with reason `absent`.
116
+ - `null` is `missing` with reason `null`.
117
+ - An invalid path or a blocked segment does not read the object.
118
+
119
+ `resolveTemplateData(template, data)` copies the template layout, fills `resolvedText` on each text element, and fills `resolvedSrc` on each image element. It does not change the template or the data, and it does not request image URLs. The result is not a saved template: it contains preview fields, and `validateTemplate` rejects it.
120
+
121
+ Display conversion, with no currency or date formatting:
122
+
123
+ - Strings are shown unchanged.
124
+ - Numbers use their plain decimal form: `1250.5` stays `1250.5`, and `0` stays `0`.
125
+ - Booleans become `true` or `false`.
126
+ - Objects and arrays are not shown. They use the fallback, or an empty string when no fallback is set.
127
+
128
+ When the path is missing, null, or not a scalar:
129
+
130
+ - If `fallback` is set, the preview shows `prefix + fallback + suffix`.
131
+ - If `fallback` is omitted, the preview is an empty string and prefix and suffix are not applied.
132
+
133
+ A present scalar, including an empty string, is shown as `prefix + value + suffix` and does not use the fallback.
134
+
135
+ The same saved template can be resolved with different JSON objects. `exampleBoundInvoiceTemplate` binds `company.name`, `company.address`, `customer.name`, `customer.address.city`, `invoice.number`, `invoice.date`, and `invoice.total`.
136
+
137
+ ```ts
138
+ import {
139
+ deserializeTemplate,
140
+ exampleBoundInvoiceTemplate,
141
+ exampleInvoiceData,
142
+ exampleInvoiceDataAlternate,
143
+ resolveTemplateData,
144
+ serializeTemplate,
145
+ } from "@arslankhan/doc-forge";
146
+
147
+ const saved = serializeTemplate(exampleBoundInvoiceTemplate);
148
+ const template = deserializeTemplate(saved);
149
+
150
+ const first = resolveTemplateData(template, exampleInvoiceData);
151
+ const second = resolveTemplateData(template, exampleInvoiceDataAlternate);
152
+
153
+ const company = first.elements.find((element) => element.id === "company-name");
154
+ if (company?.type === "text") {
155
+ console.log(company.resolvedText); // Smartbooks
156
+ }
157
+ ```
158
+
159
+ `exampleInvoiceDataAlternate` supplies a different company, customer, and invoice. Its total is `0`, which is shown as `0` rather than the fallback.
160
+
161
+ ### Image sources
162
+
163
+ ```ts
164
+ {
165
+ type: "image",
166
+ src: "assets/logo.png",
167
+ binding: {
168
+ path: "company.logo",
169
+ fallback: "assets/logo.png"
170
+ }
171
+ }
172
+ ```
173
+
174
+ `company.logo`, `customer.photo`, `product.image`, and `organization.branding.logo` are normal paths. The bound value is used only when it is a non-empty string. Missing paths, `null`, blank strings, numbers, booleans, objects, and arrays use `fallback` when that fallback is a non-empty string. With no usable fallback, the image is missing. `prefix` and `suffix` are not added to an image URL.
175
+
176
+ `src` is the static URL. It is used only when the element has no binding. Removing the binding returns to that URL. The resolved URL is not written back onto the template.
177
+
178
+ `exampleInvoiceData` includes illustrative URLs such as `https://example.com/smartbooks-logo.png`. Those addresses document the binding; they are not claims that a file is hosted there. `exampleInvoiceDataAlternate` uses a different logo URL for the same `company.logo` binding.
179
+
180
+ ### Preview JSON in the designer
181
+
182
+ Open **Preview data** in the toolbar. Paste a JSON object, or choose **Load example**, then **Apply preview**. The canvas shows resolved text and keeps the current layout. Editing the JSON while preview is active updates the page when the JSON is valid. Invalid JSON shows an error and leaves the last valid preview on the page.
183
+
184
+ **Exit preview** returns to the placeholder text. Dragging, resizing, undo, and the property panel keep working during preview. **Save** stores the template and its binding configuration. It does not store the preview JSON or the resolved strings.
185
+
186
+ In the text properties, choose **Data binding**, enter a path, and set a fallback. The panel reports whether the path is valid. **Static text** removes the binding and leaves position, size, and formatting in place. A blank fallback, prefix, or suffix is omitted from the template.
187
+
188
+ In the image properties, choose **Static URL** or **Bind to JSON data**. The binding fields are the path, such as `company.logo`, and a fallback image URL. The panel reports an invalid path, a missing value, and a source the preview will not display. **Static URL** removes the binding and edits `src`. During preview, the canvas shows the resolved image. Editing mode keeps the image placeholder.
189
+
190
+ ## HTML rendering
191
+
192
+ `renderToHTML` lives on the package root, so a Node.js program can render a template without loading React. It validates the template, resolves text with `resolveTemplateData`, and returns an HTML string. The template and the data object are not modified.
193
+
194
+ ```ts
195
+ import {
196
+ exampleBoundInvoiceTemplate,
197
+ exampleInvoiceData,
198
+ renderToHTML,
199
+ } from "@arslankhan/doc-forge";
200
+
201
+ const html = renderToHTML(exampleBoundInvoiceTemplate, exampleInvoiceData, {
202
+ imageSources: { allowHttps: true, httpsHosts: ["example.com"] },
203
+ });
204
+ console.log(html);
205
+ ```
206
+
207
+ `example:html` writes that document to `examples/bound-invoice.html` after building the library:
208
+
209
+ ```bash
210
+ npm run example:html
211
+ ```
212
+
213
+ The result is a complete HTML document by default. Pass `{ document: "fragment" }` for the style element and page only. `{ title }` sets the document title. `{ additionalCss }` appends trusted CSS after the generated rules. That string is not checked as a stylesheet, and it is rejected when it contains `<`, so it cannot close the style element. Do not pass CSS from untrusted users.
214
+
215
+ ### Page size and coordinates
216
+
217
+ Positions, sizes, margins, border widths, and corner radii use the page unit (`mm`, `pt`, `in`, or `px`). A4 portrait is `210mm` by `297mm`. Landscape swaps those dimensions when the template stores the matching page size. Explicit `page.dimensions` are used when present. Font sizes are points, matching the designer. Coordinates start at the top-left of the page, which is also how the designer places elements. The margin rectangle is a guide; it does not shift those coordinates. `@page { margin: 0 }` keeps the printed page the same size as the template. The schema has one page, so the renderer emits one page. `fixed` and `flow` both place elements at their stored coordinates.
218
+
219
+ ### Elements
220
+
221
+ - `text`: static text, bound text, fallback, prefix, and suffix. Font family, size, weight, italic, color, alignment, line height, background, border, radius, opacity, width, height, and position. Text wraps inside the box and overflow is hidden. Underline, padding, and vertical alignment are not in the schema.
222
+ - `image`: static `src`, or a bound source from `binding.path`, with `alt` and the same box and border properties. A missing or rejected source becomes a placeholder that shows the alt text. Images use `object-fit: contain`. Position and size stay in the page unit.
223
+ - `line`: position, span, stroke color, and stroke width. A zero width or height is a vertical or horizontal line, and the stroke width supplies the cross-axis size.
224
+ - `rectangle`: position, size, background, border, radius, and opacity. Borders are solid.
225
+
226
+ An element type the renderer does not know throws `RenderError`. Elements are not dropped.
227
+
228
+ ### Missing values
229
+
230
+ The renderer uses the same rules as `resolveTemplateData`. For text, a missing, null, or non-scalar binding shows `fallback` when it is set, including its prefix and suffix. With no fallback, the text is empty. `0`, `false`, and `""` are shown. Numbers stay in plain decimal form.
231
+
232
+ For an image, only a non-empty string is a source. Anything else uses the image fallback when that fallback is a non-empty string. A bound URL that the image policy rejects also tries that fallback. If neither source is allowed, the placeholder is used and the rejected URL is not written into the HTML.
233
+
234
+ ### Image URLs
235
+
236
+ Relative paths such as `assets/logo.png` are emitted by default. `https` URLs and `data:image/(png|jpeg|jpg|gif|webp);base64,...` URLs are omitted unless the call opts in:
237
+
238
+ ```ts
239
+ renderToHTML(template, data, {
240
+ imageSources: {
241
+ allowHttps: true,
242
+ httpsHosts: ["cdn.example"],
243
+ allowDataImages: true,
244
+ },
245
+ });
246
+ ```
247
+
248
+ `javascript:`, `vbscript:`, `file:`, and non-image `data:` URLs are never emitted, whether they come from `src`, a bound value, or a fallback. A `javascript:` value in `src` or in an image `fallback` also fails template validation. `http` URLs are omitted. Rejected sources are replaced with an allowed fallback or the placeholder, and the rejected URL is not copied into the HTML. Bound text is escaped and is never parsed as HTML. Image sources are escaped as attributes and are not treated as HTML. The document sets `script-src 'none'`. The renderer does not add event-handler attributes.
249
+
250
+ The designer HTML preview and canvas preview allow `https`, relative paths, and image data URLs so a bound logo can be seen. They still refuse the dangerous schemes above. A Node.js caller uses the stricter default unless `imageSources` opts in.
251
+
252
+ Invalid templates throw `TemplateValidationError`, including non-finite dimensions. Unsafe renderer options throw `RenderError`.
253
+
254
+ ### HTML preview in the designer
255
+
256
+ Choose **HTML** in the toolbar to show the rendered document in a sandboxed iframe. The iframe does not allow scripts or the parent origin. **Design** returns to the canvas without discarding edits. **Preview data** supplies the JSON used by both the canvas and the HTML view. Without preview data, bindings resolve as missing and fallbacks are used.
257
+
258
+ ## Invoices
259
+
260
+ An invoice template is a normal template with `documentType: "invoice"`. It uses the same page, elements, and bindings. `generic` and `report` templates do not run invoice arithmetic, so they render as before.
261
+
262
+ `renderToHTML` prepares a copy of the JSON before resolving bindings. That copy adds calculated fields and leaves the original object unchanged. The same saved template can render different invoices by passing different JSON objects.
263
+
264
+ ```ts
265
+ import {
266
+ exampleFullInvoiceData,
267
+ exampleFullInvoiceTemplate,
268
+ renderToHTML,
269
+ } from "@arslankhan/doc-forge";
270
+
271
+ const html = renderToHTML(exampleFullInvoiceTemplate, exampleFullInvoiceData, {
272
+ imageSources: { allowHttps: true, httpsHosts: ["example.com"] },
273
+ });
274
+ ```
275
+
276
+ `exampleFullInvoiceTemplate` includes a logo bound to `company.logo`, company and customer text, invoice number and dates, an items table, notes, and totals. `exampleFullInvoiceDataAlternate` is a second dataset for that template. The sample logo URLs are illustrative and are not hosted files. Smartbooks and AED are sample data, not engine defaults.
277
+
278
+ ### JSON contract
279
+
280
+ `company`, `customer`, and `invoice` are optional objects. Missing optional details stay blank. `invoice.items` is the line-item array. Each item needs numeric `quantity` and `rate`. `description` and `unit` are strings. An item may include `taxRate` and `discount: { type: "percentage" | "fixed", value }`. The invoice may include `discount` in that same shape, `taxRate`, `currency`, `number`, `date`, `dueDate`, and `notes`.
281
+
282
+ Dates used for formatting are `YYYY-MM-DD` strings. A percentage is a number from 0 to 100, so `10` means 10 percent. Quantities, rates, discounts, and tax rates cannot be negative. A missing quantity or rate throws `InvoiceCalculationError` instead of becoming zero. An empty or omitted items array is a zero invoice.
283
+
284
+ `calculateInvoice(data, policy?)` returns line amounts and totals. `prepareInvoiceContext(data, policy?)` returns the binding copy. `policy.fractionDigits` defaults to 2 and may be 0 through 6. A template can save the same setting as `invoice.fractionDigits`. `renderToHTML(..., { invoice })` overrides that saved setting.
285
+
286
+ Calculated names on the copy, which are not written back to the input:
287
+
288
+ - `invoice.subtotal`, `invoice.discountAmount`, `invoice.taxableAmount`, `invoice.taxTotal`, `invoice.grandTotal`, `invoice.totalQuantity`
289
+ - each item's `grossAmount`, `discountAmount`, `netAmount`, `lineTotal`, `taxAmount`, `taxableAmount`
290
+
291
+ `lineTotal` is the line amount after a line discount and before the invoice discount and tax.
292
+
293
+ ### Calculation order
294
+
295
+ 1. Line gross is quantity × rate, rounded to the currency fraction digits.
296
+ 2. A line discount, when present, is applied to that gross amount.
297
+ 3. The subtotal is the sum of those net line amounts.
298
+ 4. The invoice discount is applied once to the subtotal. A fixed discount cannot exceed the subtotal, and a percentage is 0 to 100.
299
+ 5. That discount is shared across lines in proportion to their net amounts. The last line receives the rounding remainder.
300
+ 6. Tax is calculated on each line after that share. An item `taxRate` wins; otherwise `invoice.taxRate` is used; otherwise tax is 0.
301
+ 7. Grand total = subtotal − invoice discount + tax.
302
+
303
+ Rounding is half away from zero at each monetary step. This is a configurable policy, not a country tax filing rule. Amounts at or above 1,000,000,000,000 are rejected. The input JSON is not modified.
304
+
305
+ ### Items table and totals
306
+
307
+ Add **Items** or **Totals** from the toolbox. The items element stores `itemsPath` (usually `invoice.items`) and columns. A column header is static text. Its binding path is relative to the current item, such as `description`, `quantity`, `unit`, `rate`, or `lineTotal`. Column `width` is a relative weight. `align` controls horizontal alignment. Currency and decimal columns default to the right. `visible: false` hides a column.
308
+
309
+ The table uses the element font, border, and background. `padding` is cell padding in the page unit. `headerStyle` styles the header. A missing or empty array shows `emptyText`, which defaults to "No items". The table stays inside its box and does not add pages.
310
+
311
+ Totals rows bind to root paths such as `invoice.grandTotal`. `hideWhenZero` omits a row whose numeric value is 0, which is how the sample hides a zero discount or tax line.
312
+
313
+ ### Number and date display
314
+
315
+ A binding or column can set `format`:
316
+
317
+ ```ts
318
+ { style: "currency", minimumFractionDigits: 2, maximumFractionDigits: 2 }
319
+ { style: "decimal", maximumFractionDigits: 2 }
320
+ { style: "date", dateStyle: "medium" }
321
+ ```
322
+
323
+ `locale` is optional and falls back to `en` when Intl rejects it. Currency style uses `format.currency` when set. Otherwise it reads `invoice.currency` from the data. A missing or invalid currency code is shown as a decimal number, without a hardcoded symbol. Formatting is display only. An unparseable date uses the binding fallback, or the original text when no fallback is set.
324
+
325
+ ## Designer
326
+
327
+ React 18 or newer is required. The core template API stays on the package root. The designer is a separate entry so Node code can load the schema without React.
328
+
329
+ ```tsx
330
+ import { exampleInvoiceTemplate } from "@arslankhan/doc-forge";
331
+ import { DocumentDesigner } from "@arslankhan/doc-forge/designer";
332
+
333
+ export function Editor() {
334
+ return (
335
+ <DocumentDesigner
336
+ template={exampleInvoiceTemplate}
337
+ onSave={(template) => {
338
+ console.log(template);
339
+ }}
340
+ />
341
+ );
342
+ }
343
+ ```
344
+
345
+ The toolbar includes a compact theme control: Light, Dark, or System. System follows the operating system and updates when that preference changes. The choice is stored in `localStorage` under `doc-forge-theme` and defaults to System. It is interface state only. Switching themes does not edit the template, and the document page stays white unless the template sets another background. HTML preview and PDF export use the template colors.
346
+
347
+ The designer edits one page. The toolbar sets the document type to `generic`, `invoice`, or `report`. New elements are text, image placeholders, lines, rectangles, an items table, a totals block, and a report table. Save returns a validated copy of the template. Invoice preview calculates totals from the preview JSON. Report preview groups the preview JSON. Neither writes those results back to the template. With the document type set to Report, the page panel edits the report title, description, page header, page footer, report header, and report footer.
348
+
349
+ Run the local demo with `npm run demo`, then open the URL Vite prints.
350
+
351
+ ## Reports
352
+
353
+ A report is a normal template with `documentType: "report"`. Page size, orientation, and margins stay on `page`. Report title, description, and header or footer bands are optional `report` settings. A `reportTable` element repeats one row per record. Schema version remains `1`.
354
+
355
+ ```ts
356
+ import {
357
+ exampleJobsReportData,
358
+ exampleJobsReportDataAlternate,
359
+ exampleJobsReportDataLarge,
360
+ exampleJobsReportTemplate,
361
+ renderToHTML,
362
+ } from "@arslankhan/doc-forge";
363
+
364
+ const html = renderToHTML(exampleJobsReportTemplate, exampleJobsReportData, {
365
+ imageSources: { allowHttps: true, httpsHosts: ["example.com"] },
366
+ });
367
+ renderToHTML(exampleJobsReportTemplate, exampleJobsReportDataAlternate, {
368
+ imageSources: { allowHttps: true, httpsHosts: ["example.com"] },
369
+ });
370
+ renderToHTML(exampleJobsReportTemplate, exampleJobsReportDataLarge, {
371
+ imageSources: { allowHttps: true, httpsHosts: ["example.com"] },
372
+ });
373
+ ```
374
+
375
+ The sample data object has optional `report` metadata, optional `company` details, and the record collection:
376
+
377
+ ```json
378
+ {
379
+ "report": { "title": "Jobs Report", "generatedAt": "2026-10-14", "branchName": "Dubai Branch", "currency": "AED" },
380
+ "data": { "jobs": [{ "jobNumber": "JOB-001", "customer": "ABC Trading", "technician": "Ahmed", "status": "Completed", "amount": 500 }] }
381
+ }
382
+ ```
383
+
384
+ `collectionPath` selects the array, for example `data.jobs`. Column `binding.path` values are read from each record (`jobNumber`, not `data.jobs.jobNumber`). Text outside the table still uses root paths such as `report.branchName`. A missing or empty collection renders the table's empty text and omits totals. A value that is not an array, or a record that is not an object, throws `ReportProcessingError`.
385
+
386
+ Columns can set `aggregate` to `sum`, `count`, `min`, or `max`. Sum, minimum, and maximum use finite numbers. Missing and null values are skipped and are not treated as zero. A present non-number throws. Zero is included. Sums are accumulated at 8 decimal places. Values at or beyond 1e12 throw. Count counts records and is printed as an integer. A currency format uses `format.currency`, then `invoice.currency`, then `report.currency`. There is no built-in currency.
387
+
388
+ `sort` is a list of `{ path, direction }` with `asc` or `desc`. Comparison is deterministic: numbers compare numerically, text uses English numeric ordering, and the original record order breaks ties. Missing and null sort values come last when ascending and first when descending.
389
+
390
+ `groupBy` supports up to three levels. The first level is the outermost group. Each level prints a heading, the rows or nested groups, then that level's subtotal when a column has an aggregate. Inner subtotals appear before outer ones. Null and missing group values share the heading `(Blank)`. Group order follows the sorted records. Group fields must be text, numbers, or booleans. The engine does not run expressions or caller-supplied functions.
391
+
392
+ `showGrandTotal` adds one grand total after the groups when at least one column has an aggregate.
393
+
394
+ Header and footer bands contain static text, a data binding, or an image. They are separate:
395
+
396
+ - `report.title` and `report.description` print once at the start.
397
+ - `report.reportHeader` prints once before the body.
398
+ - `report.pageHeader` repeats in each report table's `thead` when printing.
399
+ - `report.pageFooter` is a footer at the end of the screen document and uses `position: fixed` when printing.
400
+ - `report.reportFooter` prints once after the body.
401
+ - `report.showPageNumber` adds a CSS `counter(page)` in the page footer.
402
+
403
+ Report HTML is one flowing document ordered from top to bottom on the page. `@page` carries the page size and margins. Table headers use `thead { display: table-header-group }`. Rows use `break-inside: avoid`. A group heading uses `break-after: avoid` so it can stay with the next row. `pageBreakBefore` on a report table adds `break-before: page`. These are hints to the browser. They do not guarantee the same page breaks in every browser, and this package does not measure rows into a page list. The screen preview does not calculate page numbers. Total page count is omitted because `counter(pages)` is not reliable. PDF export uses the same report processor and the same print CSS, then asks Chromium for the page count.
404
+
405
+ An items table on an invoice is unchanged: it stays inside its box. A report table on a generic or invoice document is also drawn inside its box. The flowing multi-page layout is used when `documentType` is `report`.
406
+
407
+ ## PDF export
408
+
409
+ PDF generation lives in `@arslankhan/doc-forge/pdf`. It calls the existing `renderToHTML` pipeline, so invoice totals and report grouping stay in their own modules. The HTML renderer does not import Playwright. The designer entry point does not either.
410
+
411
+ Install Playwright in the application that prints PDFs, then install Chromium:
412
+
413
+ ```bash
414
+ npm install playwright pdf-lib
415
+ npx playwright install chromium
416
+ ```
417
+
418
+ On a Linux server or in Docker, Chromium also needs system libraries:
419
+
420
+ ```bash
421
+ npx playwright install-deps chromium
422
+ ```
423
+
424
+ The library does not hard-code an executable path. Set `launch.executablePath` only when the browser is installed somewhere else. `pdf-lib` is used when `metadata` is set, to write the title, author, subject, and keywords after Chromium prints the file.
425
+
426
+ ```ts
427
+ import { exampleInvoiceData, exampleInvoiceTemplate } from "@arslankhan/doc-forge";
428
+ import { closePdfBrowser, renderToPDF, renderToPdfFile } from "@arslankhan/doc-forge/pdf";
429
+
430
+ const pdf = await renderToPDF(exampleInvoiceTemplate, exampleInvoiceData);
431
+ await renderToPdfFile(exampleInvoiceTemplate, exampleInvoiceData, "invoice.pdf");
432
+ await closePdfBrowser();
433
+ ```
434
+
435
+ `renderToPDF` returns a `Buffer` and does not write a file. `renderToPdfFile` writes only the path you pass. `examples/render-pdf.mjs` (`npm run example:pdf`) writes a generic document, the sample invoice, and the large jobs report under `examples/output/`.
436
+
437
+ ### Page size, margins, and CSS
438
+
439
+ Precedence for one PDF call:
440
+
441
+ 1. `options.page` fields that are set replace the template for both the HTML and the PDF. Named sizes, including A3, are written as explicit dimensions because the HTML renderer prefers `page.dimensions`. A3 is available here and is not a template paper size.
442
+ 2. Omitted page fields stay on the template.
443
+ 3. `preferCssPageSize` defaults to `false`. Chromium then uses the resolved width and height. Set it to `true` when a CSS `@page` size should win.
444
+
445
+ Margins are applied once:
446
+
447
+ - A report uses the template margins as Chromium PDF margins. The PDF stylesheet sets `@page { margin: 0 }` so the HTML margin is not added again.
448
+ - A generic document or invoice uses a zero PDF margin. Its HTML already lays the design out on the full page, and `@page { margin: 0 }` in that HTML matches the preview.
449
+ - `options.page.margins` replaces that choice. On a fixed page, the HTML page box is reduced by those margins so the design fits inside the Chromium margin. Coordinates then start at the content box, which is inset from the HTML preview.
450
+ - Headers and footers need at least 10mm on every edge. Smaller margins are rejected instead of being enlarged.
451
+
452
+ `printBackground` defaults to `true`. `scale` is `0.1` through `2`. Invalid sizes, negative margins, and unsupported paper names throw `PdfRenderError` with code `invalid_option` before Chromium starts.
453
+
454
+ ### Page numbers
455
+
456
+ When a report has `showPageNumber`, or when you set `headerTemplate` or `footerTemplate`, Chromium prints the margin header and footer. The page number is `<span class="pageNumber"></span>` and the total is `<span class="totalPages"></span>`. Those values come from the PDF, not from a browser preview estimate. The in-document print footer is hidden for that PDF so the CSS `counter(page)` is not printed twice.
457
+
458
+ The HTML preview still has no total page count. A very tall row or group can still cross a page boundary. Content is not dropped to force a fit. Repeated table headers, `break-inside: avoid`, and `break-after: avoid` are the same print CSS the HTML renderer already emits.
459
+
460
+ ### Images and network
461
+
462
+ Each PDF gets a new browser context and a new page. JavaScript is disabled in that context. Requests are aborted except `data:` and `about:`. `https` images are allowed only when `imageNetwork` is `true` and the existing image policy allows that URL. `javascript:`, `file:`, and other schemes stay rejected. The renderer does not navigate to a caller-supplied URL.
463
+
464
+ `timeout` defaults to 30 seconds and must be from 1 through 120000 milliseconds. A timeout throws `PdfRenderError` with code `timeout`.
465
+
466
+ ### Browser lifecycle
467
+
468
+ `renderToPDF` reuses one Chromium process. Call `closePdfBrowser()` when the process should exit. `pdfBrowserStatus()` reports whether that process is connected. A `launch` option starts a separate browser for that call and closes it afterward; it does not replace the shared process. A caller-supplied `browser` is not closed. Launch failures throw `PdfRenderError` with code `launch_failed` and include the Chromium install command.
469
+
470
+ Pages and contexts are closed after success and after failure. Document data is not reused across requests.
471
+
472
+ ## Appearance
473
+
474
+ Element styles stay on the existing `style` object. New fields are optional, so templates saved before this change still render. Schema version remains `1`.
475
+
476
+ Text, items, totals, and report tables can set font family, size, weight (`normal`, `medium`, `bold`), italic, alignment, vertical alignment, and decoration. Every element except a line can set colors, opacity, padding, corner radius, and a border. A border uses one color, width, and style (`solid`, `dashed`, `dotted`, `double`, or `none`), or a `borderTop`, `borderRight`, `borderBottom`, and `borderLeft` object when sides differ. Colors must be a hex color, an `rgb`/`rgba` function, or a plain color name. Font families must be a plain family list. Values that could change the CSS declaration are rejected and are not written into HTML.
477
+
478
+ Report cell precedence, from lowest to highest:
479
+
480
+ 1. Table text and border on the report table `style`.
481
+ 2. Role defaults for the header, group heading, subtotal, and grand total.
482
+ 3. `headerStyle`, `bodyStyle`, `groupStyle`, `subtotalStyle`, or `grandTotalStyle`.
483
+ 4. Odd and even row colors. `evenRowColor` falls back to `alternateRowColor`. `stripeRows: false` turns striping off.
484
+ 5. The first matching `rowRules` entry. A rule compares one record field with `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, or `blank`. It is not an expression and is never evaluated as JavaScript.
485
+ 6. The column `headerStyle` or `style`. `column.align` applies only when that style does not set `textAlign`.
486
+
487
+ `percent` formatting treats the stored number as a fraction: `0.15` is shown as 15%. The HTML preview and PDF export use this same resolution. PDF background printing stays on unless `printBackground` is set to false.
488
+
489
+ ## Licensing
490
+
491
+ License checks are optional. A host that never configures them gets status `unconfigured`, and `hasFeature()` still returns true for every registered feature. Rendering does not call the license client, so invoices, reports, HTML, the designer, and PDF export behave as they do today.
492
+
493
+ ```ts
494
+ import {
495
+ configureLicense,
496
+ createHttpLicenseProvider,
497
+ exampleInvoiceTemplate,
498
+ inspectDocumentCapabilities,
499
+ } from "@arslankhan/doc-forge";
500
+
501
+ const license = configureLicense({
502
+ productId: "doc-forge",
503
+ issuer: "<license-issuer>",
504
+ audience: "<your-application>",
505
+ keys: [{ kid: "<key-id>", spki: "<p256-public-key-spki-base64>" }],
506
+ provider: createHttpLicenseProvider({
507
+ activateUrl: "<https-license-api-url>/activate",
508
+ refreshUrl: "<https-license-api-url>/refresh",
509
+ deactivateUrl: "<https-license-api-url>/deactivate",
510
+ }),
511
+ });
512
+
513
+ console.log(await license.getStatus());
514
+ console.log(await license.hasFeature("pdf.export"));
515
+ console.log(inspectDocumentCapabilities(exampleInvoiceTemplate));
516
+ ```
517
+
518
+ `inspectDocumentCapabilities` reports which template features are in use. It does not grant or remove them. `activateLicense(key)` sends the key to the configured provider and then verifies the returned ES256 token. A provider response of `valid` is ignored unless the signature, issuer, audience, product, and expiry check out. `isPremium: true` is not an entitlement.
519
+
520
+ Leave `enforcement` at its default, `"off"`, until the feature-to-plan mapping is decided. `"enforce"` makes `hasFeature()` depend on the signed `entitlements` list. The registered names are `report.grouping`, `report.aggregates`, `designer.premium`, `styling.premium`, `pdf.export`, `tables.advanced`, and `support.commercial`.
521
+
522
+ The private signing key and any payment-provider secret stay on the license server. This package only needs the public key. A network failure is `network_unavailable`, not `invalid`. Offline, a signature-valid token can be used until it expires, and revocation is not known until the next successful refresh. Checks that run in the browser can be bypassed; they are not source-code protection. Verify PDF entitlements in the Node.js process that calls `@arslankhan/doc-forge/pdf`.
523
+
524
+ `npm run example:license` prints the unconfigured status. It does not contact a server. The software license for the package itself is still a placeholder: see `LICENSE`, `LICENSING.md`, and `RELEASE.md`. Do not publish until those terms are chosen. The package does not take card payments. A paid license should be issued only after the license server verifies a signed payment webhook.
525
+
526
+ ## Limitations
527
+
528
+ An invoice items table repeats rows inside its box and does not flow onto another page. Report page breaks follow Chromium's print layout and can differ from the designer preview. The HTML preview does not know the total page count. The package does not connect to a database, draw charts, build pivot tables, or evaluate expressions. It does not upload images, host files, convert currencies, or apply country tax rules. Binding paths cannot contain conditionals. There is no field browser: paths are typed in. Schema version remains `1`.
529
+
530
+ There is no license server in this repository. Activation does not succeed until the host supplies one. Client-side entitlement checks do not protect source code. Offline verification cannot see a revocation that happened after the token was signed. CommonJS `require()` is not supported. The package has not been published.
531
+
532
+ ## Scripts
533
+
534
+ ```bash
535
+ npm install
536
+ npm test
537
+ npm run lint
538
+ npm run typecheck
539
+ npm run build
540
+ npm run package:check
541
+ npm run package:smoke
542
+ npm run demo
543
+ npm run example:html
544
+ npm run example:pdf
545
+ npm run example:license
546
+ ```
547
+
548
+ `npm test` compiles the TypeScript tests and runs them with Node's built-in test runner. PDF tests launch Chromium and fail when it is missing. `npm run lint` is the TypeScript typecheck; this repository does not add a separate linter. `npm run build` emits the library to `dist/`, which is the package entry point. `@arslankhan/doc-forge/pdf` is the Node.js PDF entry. `@arslankhan/doc-forge` and `@arslankhan/doc-forge/designer` stay free of Playwright. `package:check` and `package:smoke` inspect the installable tarball. They do not publish it. The commands the owner runs to publish are in `RELEASE.md`.
package/RELEASE.md ADDED
@@ -0,0 +1,50 @@
1
+ # Release checklist
2
+
3
+ The package name is `@arslankhan/doc-forge`. npm rejects the unscoped name `doc-forge` because `docforge` already exists. `publishConfig.access` is `public`, so a normal `npm publish` is enough for this scope. Placeholders for the website, price, and license API belong in the host application, not in this package. `LICENSE` is still a placeholder, so replace it with the terms you intend to offer before you publish.
4
+
5
+ Nothing in this repository runs `npm publish`.
6
+
7
+ ## Before packaging
8
+
9
+ 1. Use Node.js 18 or newer. The tests for this repository were run with Node.js 22.
10
+ 2. Confirm `package.json` `license` and `LICENSE` match the terms the owner selected. Until then they say `UNLICENSED`.
11
+ 3. Confirm the package name and the npm access level (`public` or `restricted` for a scoped name).
12
+ 4. Confirm no `.env` file, private key, payment secret, or customer database is in the working tree.
13
+
14
+ ## Verify
15
+
16
+ ```bash
17
+ npm install
18
+ npm run typecheck
19
+ npm test
20
+ npm run package:check
21
+ npm run package:smoke
22
+ npm pack --dry-run
23
+ ```
24
+
25
+ `npm test` compiles the TypeScript tests and runs them with Node's test runner. PDF tests launch Chromium. `package:check` builds `dist/` and fails if the tarball would contain source, tests, secrets, or a Playwright import in the browser-safe entries. `package:smoke` installs that tarball into a temporary project and imports the core API, the React designer, and the Node PDF entry in separate processes.
26
+
27
+ Inspect the dry-run file list. It should contain `package.json`, `README.md`, `LICENSE`, `CHANGELOG.md`, `RELEASE.md`, `LICENSING.md`, and `dist/`. It should not contain `src/`, `tests/`, `demo/`, `examples/`, `.env`, or key files.
28
+
29
+ ## Publish
30
+
31
+ Run these only after the checks above and an explicit decision to publish.
32
+
33
+ ```bash
34
+ npm whoami
35
+ npm publish
36
+ ```
37
+
38
+ For a scoped package, set the name first and then choose access:
39
+
40
+ ```bash
41
+ npm publish --access public
42
+ ```
43
+
44
+ or
45
+
46
+ ```bash
47
+ npm publish --access restricted
48
+ ```
49
+
50
+ The first publish of a scoped package needs `--access`. Later publishes reuse it. Do not add a registry auth token to this repository.
@@ -0,0 +1,15 @@
1
+ export type BindingPathResult = {
2
+ valid: true;
3
+ segments: readonly string[];
4
+ } | {
5
+ valid: false;
6
+ reason: "invalid" | "unsafe";
7
+ };
8
+ /**
9
+ * Parses a stored binding path.
10
+ * Paths are dot-separated names, with array indexes after the first segment (`items.0.name`).
11
+ * `__proto__`, `prototype`, and `constructor` are rejected. This does not read data or execute code.
12
+ */
13
+ export declare function parseBindingPath(path: string): BindingPathResult;
14
+ export declare function isBindingPath(path: string): boolean;
15
+ //# sourceMappingURL=binding-path.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"binding-path.d.ts","sourceRoot":"","sources":["../src/binding-path.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,iBAAiB,GACzB;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,CAAA;CAAE,GAC5C;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,SAAS,GAAG,QAAQ,CAAA;CAAE,CAAC;AAEnD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,CAyBhE;AAED,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAEnD"}