@broadpaper/forme 0.1.1

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.
@@ -0,0 +1,369 @@
1
+ import { TableRowGeometry, BlockRegistry, Theme, DocumentStyles, FormatContext, RenderMode, ResolvedNode, GeometryMap, VNode, RenderTreeOptions, ReportTemplate, AnyVersionTemplate, ReportData, DataSource, PagedDocument } from '@broadpaper/core';
2
+ import { WatermarkSpec, LicenseStatus } from '@broadpaper/license';
3
+ export { BroadPaper, BroadPaperConfig, Entitlement, LicensePayload, LicenseState, LicenseStatus, configure, parseLicense, resolveLicenseStatus, verifyLicense } from '@broadpaper/license';
4
+
5
+ /**
6
+ * Translates the inline CSS our block renderers emit into Forme's style model.
7
+ *
8
+ * BroadPaper blocks produce a deliberately small, self-generated CSS vocabulary
9
+ * (we never accept CSS from users), which is what makes a faithful mapping
10
+ * possible at all. Anything outside that vocabulary is reported rather than
11
+ * dropped silently — the caller surfaces it as a validation warning.
12
+ */
13
+ /** CSS pixels at 96 dpi → PDF points at 72 dpi. */
14
+ declare const PX_TO_PT: number;
15
+ declare function pxToPt(px: number): number;
16
+ interface FormeStyle {
17
+ [prop: string]: unknown;
18
+ }
19
+ interface StyleTranslation {
20
+ style: FormeStyle;
21
+ /** CSS declarations with no Forme equivalent, e.g. "box-shadow". */
22
+ unsupported: string[];
23
+ }
24
+ /** Parses an inline style string or object into kebab-case declarations. */
25
+ declare function parseDeclarations(style: unknown): Record<string, string>;
26
+ /**
27
+ * Maps CSS declarations to Forme style props. Returns the props plus any
28
+ * declaration we could not represent.
29
+ */
30
+ declare function translateStyle(css: Record<string, string>): StyleTranslation;
31
+
32
+ /**
33
+ * Forme as a pure measurer.
34
+ *
35
+ * Forme is page-native: given a document it paginates and draws. But given a
36
+ * page tall enough that nothing can overflow, it becomes a measurement engine —
37
+ * it returns the laid-out tree with a box for every node, a `TextLine` per line
38
+ * of wrapped text and a `TableRow` per row. That is exactly the geometry our own
39
+ * paginator consumes, so BroadPaper keeps every pagination decision (keep
40
+ * together, keep with next, orphans and widows, repeated table headers, page
41
+ * breaks, splitting multi-column rows) while no browser is involved anywhere.
42
+ *
43
+ * Correlating engine output back to BroadPaper nodes is done structurally: the
44
+ * translator records an annotation tree in lockstep with the nodes it emits, and
45
+ * the layout tree comes back with the same shape (leaf `Text` nodes merely gain
46
+ * `TextLine` children, which appends rather than reorders).
47
+ */
48
+
49
+ /** Mirrors an emitted node, carrying what we need to map geometry back. */
50
+ interface Annotation {
51
+ /** BroadPaper node key, when this node is a block wrapper. */
52
+ key?: string;
53
+ /** This node's subtree holds wrapped text whose line boxes we want. */
54
+ text?: boolean;
55
+ /** Row roles in emission order, for table nodes. */
56
+ rowKinds?: Array<TableRowGeometry["kind"]>;
57
+ children: Annotation[];
58
+ }
59
+ /** A layout element as returned by Forme. */
60
+ interface LayoutElement {
61
+ x: number;
62
+ y: number;
63
+ width: number;
64
+ height: number;
65
+ nodeType: string;
66
+ textContent?: string | null;
67
+ children?: LayoutElement[];
68
+ }
69
+ interface LayoutPage {
70
+ width: number;
71
+ height: number;
72
+ elements: LayoutElement[];
73
+ }
74
+ /** Height of the measurement page. Tall enough that nothing paginates. */
75
+ declare const UNBOUNDED_PAGE_HEIGHT = 200000;
76
+ interface MeasuredSection {
77
+ section: ResolvedNode;
78
+ node: FormeNode;
79
+ ann: Annotation;
80
+ }
81
+ /**
82
+ * Builds the geometry map our paginator expects from Forme's layout of an
83
+ * unbounded page. `pageElements` are the top-level elements of that page, in the
84
+ * same order as `sections`.
85
+ */
86
+ declare function geometryFromLayout(sections: MeasuredSection[], pageElements: LayoutElement[], width: number): GeometryMap;
87
+
88
+ interface FormeRenderer {
89
+ (doc: Record<string, unknown>): Promise<{
90
+ pdf: Uint8Array;
91
+ layout?: unknown;
92
+ warnings?: string[];
93
+ }>;
94
+ }
95
+ interface FormeMeasurerOptions {
96
+ registry: BlockRegistry;
97
+ theme: Theme;
98
+ styles: DocumentStyles;
99
+ format: FormatContext;
100
+ mode?: RenderMode;
101
+ /** Page width in CSS pixels, used for the measurement page. */
102
+ pageWidth: number;
103
+ margins: {
104
+ top: number;
105
+ right: number;
106
+ bottom: number;
107
+ left: number;
108
+ };
109
+ fonts?: unknown[];
110
+ /** Injected so the caller picks the Forme entry point (node, browser, worker). */
111
+ render: FormeRenderer;
112
+ }
113
+ /**
114
+ * Measures resolved sections with Forme, returning geometry in CSS pixels — the
115
+ * same shape `DomMeasurer` produces, so the paginator and the editor cannot tell
116
+ * which one measured.
117
+ */
118
+ declare class FormeMeasurer {
119
+ private readonly opts;
120
+ readonly warnings: Set<string>;
121
+ constructor(opts: FormeMeasurerOptions);
122
+ private renderOptions;
123
+ get contentWidth(): number;
124
+ measure(sections: ResolvedNode[], width?: number): Promise<GeometryMap>;
125
+ }
126
+
127
+ /**
128
+ * VNode → Forme document node.
129
+ *
130
+ * Block renderers (built-in and custom alike) return a VNode tree, so a single
131
+ * translator gives every block a browserless PDF path for free — no block has
132
+ * to know Forme exists.
133
+ *
134
+ * Alongside each emitted node the translator records an `Annotation` in exactly
135
+ * the same shape. That is what lets `measure.ts` map Forme's layout result back
136
+ * onto BroadPaper node keys without the engine needing to carry our identifiers.
137
+ */
138
+
139
+ interface FormeNode {
140
+ kind: Record<string, unknown>;
141
+ style: FormeStyle;
142
+ children: FormeNode[];
143
+ href?: string;
144
+ alt?: string;
145
+ }
146
+ interface TranslateContext {
147
+ warnings: Set<string>;
148
+ /**
149
+ * Keeps design-mode affordances — placeholders, empty-slot outlines, the
150
+ * page-break rule. They never belong in a PDF, but when this translation is
151
+ * measuring the editor's canvas they are part of what the canvas draws, and
152
+ * measuring them as nothing makes blocks invisible on screen.
153
+ */
154
+ design?: boolean;
155
+ /**
156
+ * Drops page-break nodes, for the passes where BroadPaper has already decided
157
+ * the pages. Measurement uses one unbounded page and reads back only its
158
+ * first, so a break there would silently discard everything after it; the
159
+ * paginated draw pass has placed its content already and must not reflow.
160
+ */
161
+ suppressPageBreaks?: boolean;
162
+ }
163
+ /** An emitted node paired with its annotation. */
164
+ interface Translated {
165
+ node: FormeNode;
166
+ ann: Annotation;
167
+ }
168
+ /** Translates one VNode, returning the emitted node and its annotation. */
169
+ declare function translate(v: VNode, ctx: TranslateContext): Translated | null;
170
+ /** Translates one VNode. Returns null for nodes that draw nothing. */
171
+ declare function vnodeToForme(v: VNode, ctx: TranslateContext): FormeNode | null;
172
+
173
+ declare function watermarkNode(spec: WatermarkSpec): FormeNode;
174
+
175
+ /**
176
+ * Forme as the editor's measurer.
177
+ *
178
+ * The canvas normally measures in the browser, which is immediate and free.
179
+ * That is the right default, but it means the editor and the PDF are laid out
180
+ * by two different engines, and where they disagree a page breaks in one place
181
+ * on screen and another in the file. Measuring the canvas with the same engine
182
+ * that renders the PDF removes the disagreement entirely: a break you see is a
183
+ * break you get.
184
+ *
185
+ * The engine is injected rather than imported so this module stays neutral
186
+ * about which Forme entry point the host uses — browser, worker or node.
187
+ */
188
+
189
+ interface FormeEditorMeasurerOptions {
190
+ /** The engine call, e.g. `renderSerializedDocWithLayout` from `@formepdf/core/browser`. */
191
+ render: FormeRenderer;
192
+ /** Font files the engine should use, matching the theme's families. */
193
+ fonts?: unknown[];
194
+ /** Called for every distinct translation warning, once. */
195
+ onWarning?(message: string): void;
196
+ }
197
+ /**
198
+ * Measures resolved sections with Forme, caching by section content.
199
+ *
200
+ * Engine measurement is far slower than reading the DOM, so an edit must not
201
+ * re-measure the whole document. Only sections whose content actually changed
202
+ * are sent, and they go in one call rather than one call each.
203
+ */
204
+ declare class FormeEditorMeasurer {
205
+ private readonly config;
206
+ private opts;
207
+ private readonly cache;
208
+ private readonly seen;
209
+ constructor(config: FormeEditorMeasurerOptions);
210
+ setOptions(opts: RenderTreeOptions): void;
211
+ clearCache(): void;
212
+ destroy(): void;
213
+ private key;
214
+ measure(sections: ResolvedNode[], width: number): Promise<GeometryMap>;
215
+ }
216
+
217
+ /**
218
+ * @broadpaper/forme — browserless PDF output.
219
+ *
220
+ * The Chromium backend (`@broadpaper/pdf`) prints a document BroadPaper has
221
+ * already paginated. This backend instead hands Forme the flowing content and
222
+ * lets its page-native Rust/WASM engine paginate and draw: no browser, no
223
+ * native dependencies, runs in Node, edge runtimes and the browser.
224
+ *
225
+ * The trade-off is explicit: pagination decisions move from BroadPaper's
226
+ * paginator to Forme's, so the editor's on-screen page breaks are an
227
+ * approximation of this output until the editor is switched to the same engine.
228
+ * `renderPdf` returns Forme's own page geometry so callers can compare.
229
+ */
230
+
231
+ interface FormeFont {
232
+ family: string;
233
+ /** TTF/OTF bytes, a data: URI, or a filesystem path (Node only). */
234
+ src: string | Uint8Array;
235
+ weight?: number;
236
+ style?: "normal" | "italic";
237
+ }
238
+ interface RenderFormePdfOptions {
239
+ template: ReportTemplate | AnyVersionTemplate;
240
+ registry: BlockRegistry;
241
+ data?: ReportData;
242
+ theme?: Partial<Theme>;
243
+ dataSources?: DataSource[];
244
+ now?: Date;
245
+ locale?: string;
246
+ currency?: string;
247
+ timeZone?: string;
248
+ metadata?: {
249
+ title?: string;
250
+ author?: string;
251
+ subject?: string;
252
+ creator?: string;
253
+ lang?: string;
254
+ };
255
+ /** Fonts to embed. Without these Forme falls back to its standard faces. */
256
+ fonts?: FormeFont[];
257
+ /** Emit a tagged (accessible) PDF. */
258
+ tagged?: boolean;
259
+ /** PDF/UA-1 conformance. Requires an embeddable font. */
260
+ pdfUa?: boolean;
261
+ /** PDF/A conformance level. */
262
+ pdfA?: "2b" | "2u" | "2a" | "3b" | "3u" | "3a";
263
+ /**
264
+ * A licence for this render only, overriding whatever `configure` was given.
265
+ *
266
+ * Almost nothing should need it: a process renders under one licence, and
267
+ * setting it once at startup is both less code and impossible to forget on
268
+ * one export path. It exists for a multi-tenant host that renders on behalf
269
+ * of several licensed organisations from one process.
270
+ */
271
+ license?: string | null;
272
+ /**
273
+ * Injected renderer, so the caller controls which Forme entry point is used
274
+ * (`@formepdf/core`, `/browser`, or `/worker`). Defaults to a dynamic import
275
+ * of `@formepdf/core`.
276
+ */
277
+ renderer?: (doc: Record<string, unknown>) => Promise<{
278
+ pdf: Uint8Array;
279
+ layout?: unknown;
280
+ warnings?: string[];
281
+ }>;
282
+ }
283
+ interface FormePageInfo {
284
+ width: number;
285
+ height: number;
286
+ contentWidth: number;
287
+ contentHeight: number;
288
+ elements: unknown[];
289
+ }
290
+ interface RenderFormePdfResult {
291
+ pdf: Uint8Array;
292
+ pages: number;
293
+ /** Forme's own layout geometry — the basis for driving an editor from this engine. */
294
+ layout: {
295
+ pages: FormePageInfo[];
296
+ } | null;
297
+ /** Everything BroadPaper could not translate, plus everything Forme flagged. */
298
+ warnings: string[];
299
+ durationMs: number;
300
+ }
301
+ /**
302
+ * Builds the Forme document JSON for a resolved BroadPaper document.
303
+ *
304
+ * Asynchronous only because it checks the licence, which it does itself rather
305
+ * than taking a "watermark this" flag from its caller. Every route to a
306
+ * BroadPaper PDF through this engine passes through here or through
307
+ * `emitPages`, and a document built without the evaluation mark is a clean
308
+ * unlicensed PDF — so the decision is made where the document is assembled and
309
+ * nowhere else.
310
+ */
311
+ declare function buildFormeDocument(opts: Omit<RenderFormePdfOptions, "renderer">): Promise<{
312
+ doc: Record<string, unknown>;
313
+ warnings: string[];
314
+ license: LicenseStatus;
315
+ }>;
316
+ /**
317
+ * How this package reaches the engine when the caller injects no `renderer`.
318
+ *
319
+ * It is set by the entry point rather than imported here, because the two
320
+ * runtimes need different Forme builds and a browser bundler must never even
321
+ * see the other one. `@formepdf/core`'s default browser build is the
322
+ * wasm-pack *bundler* target, whose `import * as wasm from "./forme_bg.wasm"`
323
+ * Angular's builder rejects outright ("WASM/ES module integration imports are
324
+ * not supported with Zone.js applications") — so a single bare import here
325
+ * obliged every Angular host to go zoneless to use us at all. That is a
326
+ * decision about the host's change detection, and a reporting SDK has no
327
+ * business making it.
328
+ *
329
+ * `src/index.ts` installs the Node loader, `src/index.browser.ts` the browser
330
+ * one, and the `browser` condition in this package's exports picks between
331
+ * them. Neither bundle contains the other's import.
332
+ */
333
+ type EngineLoader = () => Promise<(doc: Record<string, unknown>) => Promise<{
334
+ pdf: Uint8Array;
335
+ layout?: unknown;
336
+ warnings?: string[];
337
+ }>>;
338
+ /**
339
+ * Installs the engine used when no `renderer` is passed. Each entry point calls
340
+ * this for its own runtime, which is all most hosts need; it is exported
341
+ * because a host that reaches Forme some third way — a shared worker, a
342
+ * pre-instantiated module, a pooled engine — has somewhere to say so once
343
+ * rather than on every call.
344
+ */
345
+ declare function setEngineLoader(loader: EngineLoader): void;
346
+ /** Renders a BroadPaper template to PDF with no browser involved. */
347
+ declare function renderPdf(opts: RenderFormePdfOptions): Promise<RenderFormePdfResult>;
348
+
349
+ interface RenderPaginatedResult extends RenderFormePdfResult {
350
+ /** The pages BroadPaper's own paginator decided. */
351
+ paged: PagedDocument;
352
+ /** Geometry measured by the engine, keyed by section. */
353
+ geometry: GeometryMap;
354
+ }
355
+ /**
356
+ * Renders with **BroadPaper owning pagination** and Forme acting only as a
357
+ * measurer and a draw target — no browser, and every page-break rule
358
+ * (keep together, keep with next, orphans and widows, repeated table headers,
359
+ * explicit breaks, splitting multi-column rows) still comes from our paginator.
360
+ *
361
+ * Two passes through the engine: one onto a page tall enough that nothing can
362
+ * overflow, which yields a box for every node, line and table row; then one that
363
+ * draws the pages our paginator produced. Both passes lay text out with the same
364
+ * engine at the same width, so the second cannot re-break what the first
365
+ * measured.
366
+ */
367
+ declare function renderPdfPaginated(opts: RenderFormePdfOptions): Promise<RenderPaginatedResult>;
368
+
369
+ export { type Annotation, type EngineLoader, FormeEditorMeasurer, type FormeEditorMeasurerOptions, type FormeFont, FormeMeasurer, type FormeMeasurerOptions, type FormeNode, type FormePageInfo, type FormeRenderer, type LayoutElement, type LayoutPage, PX_TO_PT, type RenderFormePdfOptions, type RenderFormePdfResult, type RenderPaginatedResult, type TranslateContext, type Translated, UNBOUNDED_PAGE_HEIGHT, buildFormeDocument, geometryFromLayout, parseDeclarations, pxToPt, renderPdf, renderPdfPaginated, setEngineLoader, translate, translateStyle, vnodeToForme, watermarkNode };
package/dist/index.js ADDED
@@ -0,0 +1,54 @@
1
+ import {
2
+ BroadPaper,
3
+ FormeEditorMeasurer,
4
+ FormeMeasurer,
5
+ PX_TO_PT,
6
+ UNBOUNDED_PAGE_HEIGHT,
7
+ buildFormeDocument,
8
+ configure,
9
+ geometryFromLayout,
10
+ parseDeclarations,
11
+ parseLicense,
12
+ pxToPt,
13
+ renderPdf,
14
+ renderPdfPaginated,
15
+ resolveLicenseStatus,
16
+ setEngineLoader,
17
+ translate,
18
+ translateStyle,
19
+ verifyLicense,
20
+ vnodeToForme,
21
+ watermarkNode
22
+ } from "./chunk-KE7FFMY2.js";
23
+
24
+ // src/engine.node.ts
25
+ var loadEngine = async () => {
26
+ const mod = await import("@formepdf/core");
27
+ return (doc) => mod.renderSerializedDocWithLayout(doc);
28
+ };
29
+
30
+ // src/index.ts
31
+ setEngineLoader(loadEngine);
32
+ export {
33
+ BroadPaper,
34
+ FormeEditorMeasurer,
35
+ FormeMeasurer,
36
+ PX_TO_PT,
37
+ UNBOUNDED_PAGE_HEIGHT,
38
+ buildFormeDocument,
39
+ configure,
40
+ geometryFromLayout,
41
+ parseDeclarations,
42
+ parseLicense,
43
+ pxToPt,
44
+ renderPdf,
45
+ renderPdfPaginated,
46
+ resolveLicenseStatus,
47
+ setEngineLoader,
48
+ translate,
49
+ translateStyle,
50
+ verifyLicense,
51
+ vnodeToForme,
52
+ watermarkNode
53
+ };
54
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/engine.node.ts","../src/index.ts"],"sourcesContent":["/**\n * The engine as Node, Bun, Deno and edge runtimes get it.\n *\n * `@formepdf/core`'s own conditional exports pick the right build for the\n * runtime resolving it, so the bare specifier is the correct one here — this\n * module is only ever in a graph that is not a browser's.\n */\nimport type { EngineLoader } from \"./api.js\";\n\nexport const loadEngine: EngineLoader = async () => {\n const mod = (await import(\"@formepdf/core\")) as unknown as {\n renderSerializedDocWithLayout(d: Record<string, unknown>): Promise<{ pdf: Uint8Array; layout: unknown; warnings: string[] }>;\n };\n return (doc) => mod.renderSerializedDocWithLayout(doc);\n};\n","/**\n * @broadpaper/forme — the browserless PDF backend, for Node and every other\n * non-browser runtime. Browser bundlers resolve `./index.browser.ts` instead,\n * through the `browser` condition in this package's exports.\n */\nimport { setEngineLoader } from \"./api.js\";\nimport { loadEngine } from \"./engine.node.js\";\n\nsetEngineLoader(loadEngine);\n\nexport * from \"./api.js\";\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AASO,IAAM,aAA2B,YAAY;AAClD,QAAM,MAAO,MAAM,OAAO,gBAAgB;AAG1C,SAAO,CAAC,QAAQ,IAAI,8BAA8B,GAAG;AACvD;;;ACNA,gBAAgB,UAAU;","names":[]}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@broadpaper/forme",
3
+ "version": "0.1.1",
4
+ "description": "Browserless PDF backend for BroadPaper, built on the Forme (Rust/WASM) engine. Runs in Node, edge runtimes and the browser.",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "browser": {
13
+ "types": "./dist/index.browser.d.ts",
14
+ "import": "./dist/index.browser.js"
15
+ },
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "LICENSE",
23
+ "THIRD-PARTY-NOTICES.md"
24
+ ],
25
+ "sideEffects": [
26
+ "./dist/index.js",
27
+ "./dist/index.browser.js"
28
+ ],
29
+ "dependencies": {
30
+ "@formepdf/core": "^0.20.0",
31
+ "@formepdf/shared": "^0.20.0",
32
+ "@broadpaper/blocks": "0.1.1",
33
+ "@broadpaper/core": "0.1.1",
34
+ "@broadpaper/license": "0.1.1"
35
+ },
36
+ "browser": "./dist/index.browser.js",
37
+ "scripts": {
38
+ "build": "tsup",
39
+ "typecheck": "tsc --noEmit -p tsconfig.json"
40
+ }
41
+ }