@jasy/vue 1.0.0-alpha.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.
- package/README.md +133 -0
- package/dist/components.d.ts +376 -0
- package/dist/components.js +240 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +12 -0
- package/dist/node.d.ts +1 -0
- package/dist/node.js +3 -0
- package/dist/renderer.d.ts +8 -0
- package/dist/renderer.js +69 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/jasy-pdf/jasy/main/docs/logo.png" width="120" alt="jasy">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">@jasy/vue</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<b>Author PDFs as Vue components - and render them right in the browser.</b>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
React has [`@react-pdf/renderer`](https://react-pdf.org). Vue had nothing. This is it: a thin Vue custom
|
|
12
|
+
renderer over the [`@jasy/pdf`](https://www.npmjs.com/package/@jasy/pdf) engine. You write a component
|
|
13
|
+
tree, you get a real PDF - no headless browser, no server round-trip, no Java.
|
|
14
|
+
|
|
15
|
+
```vue
|
|
16
|
+
<script setup lang="ts">
|
|
17
|
+
import { Document, Page, Text, Box, renderToPdf } from "@jasy/vue";
|
|
18
|
+
import Invoice from "./Invoice.vue";
|
|
19
|
+
|
|
20
|
+
async function download() {
|
|
21
|
+
const bytes = await renderToPdf(Invoice); // 100% in the browser
|
|
22
|
+
const url = URL.createObjectURL(new Blob([bytes], { type: "application/pdf" }));
|
|
23
|
+
window.open(url);
|
|
24
|
+
}
|
|
25
|
+
</script>
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Why
|
|
29
|
+
|
|
30
|
+
- **Runs in the browser.** The engine is ESM + isomorphic, so `renderToPdf` produces the PDF bytes
|
|
31
|
+
client-side. No `/api/render`, no Node on the request path. (It works in Node too - same call.)
|
|
32
|
+
- **Real layout, real pagination.** Flexbox-style `Row`/`Column`, a fragmenting layout engine, repeating
|
|
33
|
+
table headers - not a drawing API you hand absolute coordinates.
|
|
34
|
+
- **Typed props.** `<Text :size :color bold>` autocompletes and type-checks; pass a wrong `fit` and it
|
|
35
|
+
goes red. Boolean shorthands (`<Text bold>`, `<Box relative>`) just work.
|
|
36
|
+
- **Extensible.** A custom element type is one `registerElement(type, factory)` away - react-pdf's
|
|
37
|
+
primitive set is closed.
|
|
38
|
+
|
|
39
|
+
## Install
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
pnpm add @jasy/vue @jasy/pdf vue
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Components
|
|
46
|
+
|
|
47
|
+
`Document` · `Page` · `Column` · `Row` · `Box` · `Padding` · `Expanded` · `Spacer` · `Divider` ·
|
|
48
|
+
`Image` · `Text` · `Paragraph` · `Span` · `Table` / `TableRow` / `TableCell`.
|
|
49
|
+
|
|
50
|
+
```vue
|
|
51
|
+
<template>
|
|
52
|
+
<Document :size="12" color="#1a1a1a">
|
|
53
|
+
<Page :size="'A4'" :gap="16">
|
|
54
|
+
<Row :justify="'between'">
|
|
55
|
+
<Text :size="22" bold color="#0a2348">Acme GmbH</Text>
|
|
56
|
+
<Text :size="22" bold color="#1450aa">INVOICE</Text>
|
|
57
|
+
</Row>
|
|
58
|
+
|
|
59
|
+
<Table :columns="['1fr', 120]" cell-border="#e2e8f0" :cell-padding="9">
|
|
60
|
+
<TableRow header>
|
|
61
|
+
<TableCell><Text bold>Description</Text></TableCell>
|
|
62
|
+
<TableCell><Text bold align="right">Amount (EUR)</Text></TableCell>
|
|
63
|
+
</TableRow>
|
|
64
|
+
<TableRow v-for="item in items" :key="item.desc">
|
|
65
|
+
<TableCell>{{ item.desc }}</TableCell>
|
|
66
|
+
<TableCell><Text align="right">{{ item.amount }}</Text></TableCell>
|
|
67
|
+
</TableRow>
|
|
68
|
+
</Table>
|
|
69
|
+
</Page>
|
|
70
|
+
</Document>
|
|
71
|
+
</template>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
A `<TableCell>` holds a plain string **or** any components - a `Box` with a `Text`, an `Image`, a whole
|
|
75
|
+
subtree. `:columns` takes points (`120`), fractions (`"1fr"`) or `"auto"` (sized to the widest cell). A
|
|
76
|
+
row marked `header` repeats at the top of every page the table flows onto.
|
|
77
|
+
|
|
78
|
+
## Three ways to import (clash-safe)
|
|
79
|
+
|
|
80
|
+
Vue UI libraries often export their own `Row` / `Text` / `Box`. Pick whichever avoids a collision:
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// 1. Direct - aliasing is fine: `import { Row as PdfRow } from "@jasy/vue"`
|
|
84
|
+
import { Document, Page, Text } from "@jasy/vue";
|
|
85
|
+
|
|
86
|
+
// 2. Namespace - no collisions at all
|
|
87
|
+
import * as Pdf from "@jasy/vue"; // <Pdf.Document> <Pdf.Text>
|
|
88
|
+
|
|
89
|
+
// 3. Plugin with a prefix - global, no per-file imports
|
|
90
|
+
app.use(jasyVue, { prefix: "Pdf" }); // <PdfDocument> <PdfText>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Custom fonts and images (bytes)
|
|
94
|
+
|
|
95
|
+
Both load as `Uint8Array`, so the same code runs in the browser and in Node:
|
|
96
|
+
|
|
97
|
+
```vue
|
|
98
|
+
<script setup lang="ts">
|
|
99
|
+
const props = defineProps<{ font: Uint8Array; logo: Uint8Array }>();
|
|
100
|
+
</script>
|
|
101
|
+
|
|
102
|
+
<template>
|
|
103
|
+
<Document :fonts="{ GreatVibes: props.font }">
|
|
104
|
+
<Page>
|
|
105
|
+
<Text :font="'GreatVibes'" :size="44">Jasy Atelier</Text>
|
|
106
|
+
<Image :src="props.logo" :width="84" :height="84" :fit="'cover'" :radius="42" />
|
|
107
|
+
</Page>
|
|
108
|
+
</Document>
|
|
109
|
+
</template>
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## API
|
|
113
|
+
|
|
114
|
+
- `renderToPdf(root, props?, options?) => Promise<Uint8Array>` - the PDF bytes. Browser or Node.
|
|
115
|
+
- `renderToPdfString(root, props?, options?) => Promise<string>` - the raw PDF string.
|
|
116
|
+
- `toDocumentDescriptor(root, props?)` - the framework-agnostic descriptor (the seam a Node service can
|
|
117
|
+
receive from the browser).
|
|
118
|
+
- `jasyVue` - the global-registration plugin (`{ prefix }`).
|
|
119
|
+
|
|
120
|
+
## Try it
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
git clone https://github.com/jasy-pdf/jasy && cd jasy && pnpm install
|
|
124
|
+
cd packages/vue && pnpm play
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The playground renders four samples in the browser, including a `Showcase` with a custom `.ttf`, a JPEG,
|
|
128
|
+
`v-for` and computed totals - all client-side.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
Part of [Jasy](https://jasy.dev) - a declarative, dependency-light PDF toolkit in pure TypeScript.
|
|
133
|
+
MIT, by Florian Heuberger.
|
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
import { type Plugin, type PropType } from "vue";
|
|
2
|
+
import type { ColorInput, Insets, ImageSource, FontSource, PageSizeInput, ColumnWidth } from "@jasy/pdf";
|
|
3
|
+
export declare const Document: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
4
|
+
align: PropType<"left" | "center" | "right">;
|
|
5
|
+
lineHeight: NumberConstructor;
|
|
6
|
+
meta: PropType<{
|
|
7
|
+
title?: string;
|
|
8
|
+
author?: string;
|
|
9
|
+
}>;
|
|
10
|
+
fonts: PropType<Record<string, FontSource>>;
|
|
11
|
+
size: NumberConstructor;
|
|
12
|
+
font: StringConstructor;
|
|
13
|
+
bold: {
|
|
14
|
+
type: BooleanConstructor;
|
|
15
|
+
default: undefined;
|
|
16
|
+
};
|
|
17
|
+
italic: {
|
|
18
|
+
type: BooleanConstructor;
|
|
19
|
+
default: undefined;
|
|
20
|
+
};
|
|
21
|
+
color: PropType<ColorInput>;
|
|
22
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
25
|
+
align: PropType<"left" | "center" | "right">;
|
|
26
|
+
lineHeight: NumberConstructor;
|
|
27
|
+
meta: PropType<{
|
|
28
|
+
title?: string;
|
|
29
|
+
author?: string;
|
|
30
|
+
}>;
|
|
31
|
+
fonts: PropType<Record<string, FontSource>>;
|
|
32
|
+
size: NumberConstructor;
|
|
33
|
+
font: StringConstructor;
|
|
34
|
+
bold: {
|
|
35
|
+
type: BooleanConstructor;
|
|
36
|
+
default: undefined;
|
|
37
|
+
};
|
|
38
|
+
italic: {
|
|
39
|
+
type: BooleanConstructor;
|
|
40
|
+
default: undefined;
|
|
41
|
+
};
|
|
42
|
+
color: PropType<ColorInput>;
|
|
43
|
+
}>> & Readonly<{}>, {
|
|
44
|
+
bold: boolean;
|
|
45
|
+
italic: boolean;
|
|
46
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
47
|
+
export declare const Page: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
48
|
+
gap: NumberConstructor;
|
|
49
|
+
justify: PropType<"start" | "center" | "end" | "between" | "around">;
|
|
50
|
+
align: PropType<"start" | "center" | "end" | "stretch">;
|
|
51
|
+
size: PropType<PageSizeInput>;
|
|
52
|
+
orientation: PropType<"portrait" | "landscape">;
|
|
53
|
+
margin: PropType<Insets>;
|
|
54
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
55
|
+
[key: string]: any;
|
|
56
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
57
|
+
gap: NumberConstructor;
|
|
58
|
+
justify: PropType<"start" | "center" | "end" | "between" | "around">;
|
|
59
|
+
align: PropType<"start" | "center" | "end" | "stretch">;
|
|
60
|
+
size: PropType<PageSizeInput>;
|
|
61
|
+
orientation: PropType<"portrait" | "landscape">;
|
|
62
|
+
margin: PropType<Insets>;
|
|
63
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
64
|
+
export declare const Column: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
65
|
+
gap: NumberConstructor;
|
|
66
|
+
justify: PropType<"start" | "center" | "end" | "between" | "around">;
|
|
67
|
+
align: PropType<"start" | "center" | "end" | "stretch">;
|
|
68
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
69
|
+
[key: string]: any;
|
|
70
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
71
|
+
gap: NumberConstructor;
|
|
72
|
+
justify: PropType<"start" | "center" | "end" | "between" | "around">;
|
|
73
|
+
align: PropType<"start" | "center" | "end" | "stretch">;
|
|
74
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
75
|
+
export declare const Row: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
76
|
+
gap: NumberConstructor;
|
|
77
|
+
justify: PropType<"start" | "center" | "end" | "between" | "around">;
|
|
78
|
+
align: PropType<"start" | "center" | "end" | "stretch">;
|
|
79
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
80
|
+
[key: string]: any;
|
|
81
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
82
|
+
gap: NumberConstructor;
|
|
83
|
+
justify: PropType<"start" | "center" | "end" | "between" | "around">;
|
|
84
|
+
align: PropType<"start" | "center" | "end" | "stretch">;
|
|
85
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
86
|
+
export declare const Box: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
87
|
+
bg: PropType<ColorInput>;
|
|
88
|
+
border: PropType<ColorInput>;
|
|
89
|
+
borderTop: PropType<ColorInput>;
|
|
90
|
+
borderRight: PropType<ColorInput>;
|
|
91
|
+
borderBottom: PropType<ColorInput>;
|
|
92
|
+
borderLeft: PropType<ColorInput>;
|
|
93
|
+
borderWidth: NumberConstructor;
|
|
94
|
+
padding: PropType<Insets>;
|
|
95
|
+
width: NumberConstructor;
|
|
96
|
+
height: NumberConstructor;
|
|
97
|
+
radius: NumberConstructor;
|
|
98
|
+
relative: {
|
|
99
|
+
type: BooleanConstructor;
|
|
100
|
+
default: undefined;
|
|
101
|
+
};
|
|
102
|
+
overflow: PropType<"hidden" | "visible">;
|
|
103
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
104
|
+
[key: string]: any;
|
|
105
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
106
|
+
bg: PropType<ColorInput>;
|
|
107
|
+
border: PropType<ColorInput>;
|
|
108
|
+
borderTop: PropType<ColorInput>;
|
|
109
|
+
borderRight: PropType<ColorInput>;
|
|
110
|
+
borderBottom: PropType<ColorInput>;
|
|
111
|
+
borderLeft: PropType<ColorInput>;
|
|
112
|
+
borderWidth: NumberConstructor;
|
|
113
|
+
padding: PropType<Insets>;
|
|
114
|
+
width: NumberConstructor;
|
|
115
|
+
height: NumberConstructor;
|
|
116
|
+
radius: NumberConstructor;
|
|
117
|
+
relative: {
|
|
118
|
+
type: BooleanConstructor;
|
|
119
|
+
default: undefined;
|
|
120
|
+
};
|
|
121
|
+
overflow: PropType<"hidden" | "visible">;
|
|
122
|
+
}>> & Readonly<{}>, {
|
|
123
|
+
relative: boolean;
|
|
124
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
125
|
+
export declare const Padding: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
126
|
+
insets: PropType<Insets>;
|
|
127
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
128
|
+
[key: string]: any;
|
|
129
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
130
|
+
insets: PropType<Insets>;
|
|
131
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
132
|
+
export declare const Expanded: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
133
|
+
flex: NumberConstructor;
|
|
134
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
135
|
+
[key: string]: any;
|
|
136
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
137
|
+
flex: NumberConstructor;
|
|
138
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
139
|
+
export declare const Spacer: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
140
|
+
flex: NumberConstructor;
|
|
141
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
142
|
+
[key: string]: any;
|
|
143
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
144
|
+
flex: NumberConstructor;
|
|
145
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
146
|
+
export declare const Divider: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
147
|
+
color: PropType<ColorInput>;
|
|
148
|
+
thickness: NumberConstructor;
|
|
149
|
+
margin: PropType<Insets>;
|
|
150
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
151
|
+
[key: string]: any;
|
|
152
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
153
|
+
color: PropType<ColorInput>;
|
|
154
|
+
thickness: NumberConstructor;
|
|
155
|
+
margin: PropType<Insets>;
|
|
156
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
157
|
+
export declare const Image: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
158
|
+
src: PropType<ImageSource>;
|
|
159
|
+
width: NumberConstructor;
|
|
160
|
+
height: NumberConstructor;
|
|
161
|
+
fit: PropType<"none" | "contain" | "cover" | "fill">;
|
|
162
|
+
radius: NumberConstructor;
|
|
163
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
164
|
+
[key: string]: any;
|
|
165
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
166
|
+
src: PropType<ImageSource>;
|
|
167
|
+
width: NumberConstructor;
|
|
168
|
+
height: NumberConstructor;
|
|
169
|
+
fit: PropType<"none" | "contain" | "cover" | "fill">;
|
|
170
|
+
radius: NumberConstructor;
|
|
171
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
172
|
+
export declare const Text: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
173
|
+
align: PropType<"left" | "center" | "right">;
|
|
174
|
+
lineHeight: NumberConstructor;
|
|
175
|
+
maxLines: NumberConstructor;
|
|
176
|
+
overflow: PropType<"clip" | "ellipsis">;
|
|
177
|
+
size: NumberConstructor;
|
|
178
|
+
font: StringConstructor;
|
|
179
|
+
bold: {
|
|
180
|
+
type: BooleanConstructor;
|
|
181
|
+
default: undefined;
|
|
182
|
+
};
|
|
183
|
+
italic: {
|
|
184
|
+
type: BooleanConstructor;
|
|
185
|
+
default: undefined;
|
|
186
|
+
};
|
|
187
|
+
color: PropType<ColorInput>;
|
|
188
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
189
|
+
[key: string]: any;
|
|
190
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
191
|
+
align: PropType<"left" | "center" | "right">;
|
|
192
|
+
lineHeight: NumberConstructor;
|
|
193
|
+
maxLines: NumberConstructor;
|
|
194
|
+
overflow: PropType<"clip" | "ellipsis">;
|
|
195
|
+
size: NumberConstructor;
|
|
196
|
+
font: StringConstructor;
|
|
197
|
+
bold: {
|
|
198
|
+
type: BooleanConstructor;
|
|
199
|
+
default: undefined;
|
|
200
|
+
};
|
|
201
|
+
italic: {
|
|
202
|
+
type: BooleanConstructor;
|
|
203
|
+
default: undefined;
|
|
204
|
+
};
|
|
205
|
+
color: PropType<ColorInput>;
|
|
206
|
+
}>> & Readonly<{}>, {
|
|
207
|
+
bold: boolean;
|
|
208
|
+
italic: boolean;
|
|
209
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
210
|
+
export declare const Paragraph: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
211
|
+
align: PropType<"left" | "center" | "right">;
|
|
212
|
+
lineHeight: NumberConstructor;
|
|
213
|
+
maxLines: NumberConstructor;
|
|
214
|
+
overflow: PropType<"clip" | "ellipsis">;
|
|
215
|
+
size: NumberConstructor;
|
|
216
|
+
font: StringConstructor;
|
|
217
|
+
bold: {
|
|
218
|
+
type: BooleanConstructor;
|
|
219
|
+
default: undefined;
|
|
220
|
+
};
|
|
221
|
+
italic: {
|
|
222
|
+
type: BooleanConstructor;
|
|
223
|
+
default: undefined;
|
|
224
|
+
};
|
|
225
|
+
color: PropType<ColorInput>;
|
|
226
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
227
|
+
[key: string]: any;
|
|
228
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
229
|
+
align: PropType<"left" | "center" | "right">;
|
|
230
|
+
lineHeight: NumberConstructor;
|
|
231
|
+
maxLines: NumberConstructor;
|
|
232
|
+
overflow: PropType<"clip" | "ellipsis">;
|
|
233
|
+
size: NumberConstructor;
|
|
234
|
+
font: StringConstructor;
|
|
235
|
+
bold: {
|
|
236
|
+
type: BooleanConstructor;
|
|
237
|
+
default: undefined;
|
|
238
|
+
};
|
|
239
|
+
italic: {
|
|
240
|
+
type: BooleanConstructor;
|
|
241
|
+
default: undefined;
|
|
242
|
+
};
|
|
243
|
+
color: PropType<ColorInput>;
|
|
244
|
+
}>> & Readonly<{}>, {
|
|
245
|
+
bold: boolean;
|
|
246
|
+
italic: boolean;
|
|
247
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
248
|
+
export declare const Span: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
249
|
+
size: NumberConstructor;
|
|
250
|
+
font: StringConstructor;
|
|
251
|
+
bold: {
|
|
252
|
+
type: BooleanConstructor;
|
|
253
|
+
default: undefined;
|
|
254
|
+
};
|
|
255
|
+
italic: {
|
|
256
|
+
type: BooleanConstructor;
|
|
257
|
+
default: undefined;
|
|
258
|
+
};
|
|
259
|
+
color: PropType<ColorInput>;
|
|
260
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
261
|
+
[key: string]: any;
|
|
262
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
263
|
+
size: NumberConstructor;
|
|
264
|
+
font: StringConstructor;
|
|
265
|
+
bold: {
|
|
266
|
+
type: BooleanConstructor;
|
|
267
|
+
default: undefined;
|
|
268
|
+
};
|
|
269
|
+
italic: {
|
|
270
|
+
type: BooleanConstructor;
|
|
271
|
+
default: undefined;
|
|
272
|
+
};
|
|
273
|
+
color: PropType<ColorInput>;
|
|
274
|
+
}>> & Readonly<{}>, {
|
|
275
|
+
bold: boolean;
|
|
276
|
+
italic: boolean;
|
|
277
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
278
|
+
export declare const Table: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
279
|
+
columns: {
|
|
280
|
+
type: PropType<ColumnWidth[]>;
|
|
281
|
+
required: boolean;
|
|
282
|
+
};
|
|
283
|
+
gap: NumberConstructor;
|
|
284
|
+
rowGap: NumberConstructor;
|
|
285
|
+
colGap: NumberConstructor;
|
|
286
|
+
cellPadding: PropType<Insets>;
|
|
287
|
+
cellBorder: PropType<ColorInput>;
|
|
288
|
+
rule: PropType<ColorInput>;
|
|
289
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
290
|
+
[key: string]: any;
|
|
291
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
292
|
+
columns: {
|
|
293
|
+
type: PropType<ColumnWidth[]>;
|
|
294
|
+
required: boolean;
|
|
295
|
+
};
|
|
296
|
+
gap: NumberConstructor;
|
|
297
|
+
rowGap: NumberConstructor;
|
|
298
|
+
colGap: NumberConstructor;
|
|
299
|
+
cellPadding: PropType<Insets>;
|
|
300
|
+
cellBorder: PropType<ColorInput>;
|
|
301
|
+
rule: PropType<ColorInput>;
|
|
302
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
303
|
+
export declare const TableRow: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
304
|
+
header: {
|
|
305
|
+
type: BooleanConstructor;
|
|
306
|
+
default: boolean;
|
|
307
|
+
};
|
|
308
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
309
|
+
[key: string]: any;
|
|
310
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
311
|
+
header: {
|
|
312
|
+
type: BooleanConstructor;
|
|
313
|
+
default: boolean;
|
|
314
|
+
};
|
|
315
|
+
}>> & Readonly<{}>, {
|
|
316
|
+
header: boolean;
|
|
317
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
318
|
+
export declare const TableCell: import("vue").DefineComponent<{}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
319
|
+
[key: string]: any;
|
|
320
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
321
|
+
export declare const Positioned: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
322
|
+
top: NumberConstructor;
|
|
323
|
+
right: NumberConstructor;
|
|
324
|
+
bottom: NumberConstructor;
|
|
325
|
+
left: NumberConstructor;
|
|
326
|
+
h: PropType<"start" | "center" | "end">;
|
|
327
|
+
v: PropType<"start" | "center" | "end">;
|
|
328
|
+
x: NumberConstructor;
|
|
329
|
+
y: NumberConstructor;
|
|
330
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
331
|
+
[key: string]: any;
|
|
332
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
333
|
+
top: NumberConstructor;
|
|
334
|
+
right: NumberConstructor;
|
|
335
|
+
bottom: NumberConstructor;
|
|
336
|
+
left: NumberConstructor;
|
|
337
|
+
h: PropType<"start" | "center" | "end">;
|
|
338
|
+
v: PropType<"start" | "center" | "end">;
|
|
339
|
+
x: NumberConstructor;
|
|
340
|
+
y: NumberConstructor;
|
|
341
|
+
}>> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
342
|
+
export declare const DefaultTextStyle: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
343
|
+
align: PropType<"left" | "center" | "right">;
|
|
344
|
+
lineHeight: NumberConstructor;
|
|
345
|
+
size: NumberConstructor;
|
|
346
|
+
font: StringConstructor;
|
|
347
|
+
bold: {
|
|
348
|
+
type: BooleanConstructor;
|
|
349
|
+
default: undefined;
|
|
350
|
+
};
|
|
351
|
+
italic: {
|
|
352
|
+
type: BooleanConstructor;
|
|
353
|
+
default: undefined;
|
|
354
|
+
};
|
|
355
|
+
color: PropType<ColorInput>;
|
|
356
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
357
|
+
[key: string]: any;
|
|
358
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
359
|
+
align: PropType<"left" | "center" | "right">;
|
|
360
|
+
lineHeight: NumberConstructor;
|
|
361
|
+
size: NumberConstructor;
|
|
362
|
+
font: StringConstructor;
|
|
363
|
+
bold: {
|
|
364
|
+
type: BooleanConstructor;
|
|
365
|
+
default: undefined;
|
|
366
|
+
};
|
|
367
|
+
italic: {
|
|
368
|
+
type: BooleanConstructor;
|
|
369
|
+
default: undefined;
|
|
370
|
+
};
|
|
371
|
+
color: PropType<ColorInput>;
|
|
372
|
+
}>> & Readonly<{}>, {
|
|
373
|
+
bold: boolean;
|
|
374
|
+
italic: boolean;
|
|
375
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
376
|
+
export declare const jasyVue: Plugin;
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { defineComponent, h } from "vue";
|
|
2
|
+
const colorProp = [String, Number, Object];
|
|
3
|
+
const insetsProp = [Number, Object, Array];
|
|
4
|
+
// `bold`/`italic` use `default: undefined` so an unset flag stays undefined (and inherits the
|
|
5
|
+
// DefaultTextStyle) while `<Text bold>` still coerces to true.
|
|
6
|
+
const textStyleProps = {
|
|
7
|
+
size: Number,
|
|
8
|
+
font: String,
|
|
9
|
+
bold: { type: Boolean, default: undefined },
|
|
10
|
+
italic: { type: Boolean, default: undefined },
|
|
11
|
+
color: colorProp,
|
|
12
|
+
};
|
|
13
|
+
const textProps = {
|
|
14
|
+
...textStyleProps,
|
|
15
|
+
align: String,
|
|
16
|
+
lineHeight: Number,
|
|
17
|
+
maxLines: Number,
|
|
18
|
+
overflow: String,
|
|
19
|
+
};
|
|
20
|
+
const stackProps = {
|
|
21
|
+
gap: Number,
|
|
22
|
+
justify: String,
|
|
23
|
+
align: String,
|
|
24
|
+
};
|
|
25
|
+
const boxProps = {
|
|
26
|
+
bg: colorProp,
|
|
27
|
+
border: colorProp,
|
|
28
|
+
borderTop: colorProp,
|
|
29
|
+
borderRight: colorProp,
|
|
30
|
+
borderBottom: colorProp,
|
|
31
|
+
borderLeft: colorProp,
|
|
32
|
+
borderWidth: Number,
|
|
33
|
+
padding: insetsProp,
|
|
34
|
+
width: Number,
|
|
35
|
+
height: Number,
|
|
36
|
+
radius: Number,
|
|
37
|
+
relative: { type: Boolean, default: undefined },
|
|
38
|
+
overflow: String,
|
|
39
|
+
};
|
|
40
|
+
const imageProps = {
|
|
41
|
+
src: [String, Object],
|
|
42
|
+
width: Number,
|
|
43
|
+
height: Number,
|
|
44
|
+
fit: String,
|
|
45
|
+
radius: Number,
|
|
46
|
+
};
|
|
47
|
+
const dividerProps = { color: colorProp, thickness: Number, margin: insetsProp };
|
|
48
|
+
const pageProps = {
|
|
49
|
+
size: [String, Object],
|
|
50
|
+
orientation: String,
|
|
51
|
+
margin: insetsProp,
|
|
52
|
+
...stackProps,
|
|
53
|
+
};
|
|
54
|
+
const documentProps = {
|
|
55
|
+
...textStyleProps,
|
|
56
|
+
align: String,
|
|
57
|
+
lineHeight: Number,
|
|
58
|
+
meta: Object,
|
|
59
|
+
fonts: Object,
|
|
60
|
+
};
|
|
61
|
+
const positionedProps = {
|
|
62
|
+
top: Number,
|
|
63
|
+
right: Number,
|
|
64
|
+
bottom: Number,
|
|
65
|
+
left: Number,
|
|
66
|
+
h: String,
|
|
67
|
+
v: String,
|
|
68
|
+
x: Number,
|
|
69
|
+
y: Number,
|
|
70
|
+
};
|
|
71
|
+
const defaultTextStyleProps = {
|
|
72
|
+
...textStyleProps,
|
|
73
|
+
align: String,
|
|
74
|
+
lineHeight: Number,
|
|
75
|
+
};
|
|
76
|
+
const tableProps = {
|
|
77
|
+
columns: { type: Array, required: true },
|
|
78
|
+
gap: Number,
|
|
79
|
+
rowGap: Number,
|
|
80
|
+
colGap: Number,
|
|
81
|
+
cellPadding: insetsProp,
|
|
82
|
+
cellBorder: colorProp,
|
|
83
|
+
rule: colorProp,
|
|
84
|
+
};
|
|
85
|
+
// Forward the typed props (+ any extra attrs) to the engine host tag; the default slot is the children.
|
|
86
|
+
const fwd = (tag) => (props, { slots, attrs }) => () => h(tag, { ...attrs, ...props }, slots.default?.());
|
|
87
|
+
// defineComponent is called directly (not via a helper) so the props object's type reaches the
|
|
88
|
+
// component, giving template type-check + autocomplete. The devtools name stays `Jasy`-prefixed.
|
|
89
|
+
export const Document = defineComponent({
|
|
90
|
+
name: "JasyDocument",
|
|
91
|
+
inheritAttrs: false,
|
|
92
|
+
props: documentProps,
|
|
93
|
+
setup: fwd("document"),
|
|
94
|
+
});
|
|
95
|
+
// `<Page>` also takes `#header` / `#footer` named slots - laid out once, repeated on every physical page.
|
|
96
|
+
export const Page = defineComponent({
|
|
97
|
+
name: "JasyPage",
|
|
98
|
+
inheritAttrs: false,
|
|
99
|
+
props: pageProps,
|
|
100
|
+
setup(props, { slots }) {
|
|
101
|
+
return () => {
|
|
102
|
+
const kids = [];
|
|
103
|
+
if (slots.header)
|
|
104
|
+
kids.push(h("page-header", null, slots.header()));
|
|
105
|
+
if (slots.footer)
|
|
106
|
+
kids.push(h("page-footer", null, slots.footer()));
|
|
107
|
+
if (slots.default)
|
|
108
|
+
kids.push(...slots.default());
|
|
109
|
+
return h("page", { ...props }, kids);
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
export const Column = defineComponent({
|
|
114
|
+
name: "JasyColumn",
|
|
115
|
+
inheritAttrs: false,
|
|
116
|
+
props: stackProps,
|
|
117
|
+
setup: fwd("column"),
|
|
118
|
+
});
|
|
119
|
+
export const Row = defineComponent({
|
|
120
|
+
name: "JasyRow",
|
|
121
|
+
inheritAttrs: false,
|
|
122
|
+
props: stackProps,
|
|
123
|
+
setup: fwd("row"),
|
|
124
|
+
});
|
|
125
|
+
export const Box = defineComponent({
|
|
126
|
+
name: "JasyBox",
|
|
127
|
+
inheritAttrs: false,
|
|
128
|
+
props: boxProps,
|
|
129
|
+
setup: fwd("box"),
|
|
130
|
+
});
|
|
131
|
+
export const Padding = defineComponent({
|
|
132
|
+
name: "JasyPadding",
|
|
133
|
+
inheritAttrs: false,
|
|
134
|
+
props: { insets: insetsProp },
|
|
135
|
+
setup: fwd("padding"),
|
|
136
|
+
});
|
|
137
|
+
export const Expanded = defineComponent({
|
|
138
|
+
name: "JasyExpanded",
|
|
139
|
+
inheritAttrs: false,
|
|
140
|
+
props: { flex: Number },
|
|
141
|
+
setup: fwd("expanded"),
|
|
142
|
+
});
|
|
143
|
+
export const Spacer = defineComponent({
|
|
144
|
+
name: "JasySpacer",
|
|
145
|
+
inheritAttrs: false,
|
|
146
|
+
props: { flex: Number },
|
|
147
|
+
setup: fwd("spacer"),
|
|
148
|
+
});
|
|
149
|
+
export const Divider = defineComponent({
|
|
150
|
+
name: "JasyDivider",
|
|
151
|
+
inheritAttrs: false,
|
|
152
|
+
props: dividerProps,
|
|
153
|
+
setup: fwd("divider"),
|
|
154
|
+
});
|
|
155
|
+
export const Image = defineComponent({
|
|
156
|
+
name: "JasyImage",
|
|
157
|
+
inheritAttrs: false,
|
|
158
|
+
props: imageProps,
|
|
159
|
+
setup: fwd("image"),
|
|
160
|
+
});
|
|
161
|
+
export const Text = defineComponent({
|
|
162
|
+
name: "JasyText",
|
|
163
|
+
inheritAttrs: false,
|
|
164
|
+
props: textProps,
|
|
165
|
+
setup: fwd("text"),
|
|
166
|
+
});
|
|
167
|
+
export const Paragraph = defineComponent({
|
|
168
|
+
name: "JasyParagraph",
|
|
169
|
+
inheritAttrs: false,
|
|
170
|
+
props: textProps,
|
|
171
|
+
setup: fwd("paragraph"),
|
|
172
|
+
});
|
|
173
|
+
export const Span = defineComponent({
|
|
174
|
+
name: "JasySpan",
|
|
175
|
+
inheritAttrs: false,
|
|
176
|
+
props: textStyleProps,
|
|
177
|
+
setup: fwd("span"),
|
|
178
|
+
});
|
|
179
|
+
// `<Table :columns>` holds `<TableRow>`s (mark one `header` to repeat it per page) of `<TableCell>`s.
|
|
180
|
+
export const Table = defineComponent({
|
|
181
|
+
name: "JasyTable",
|
|
182
|
+
inheritAttrs: false,
|
|
183
|
+
props: tableProps,
|
|
184
|
+
setup: fwd("table"),
|
|
185
|
+
});
|
|
186
|
+
export const TableRow = defineComponent({
|
|
187
|
+
name: "JasyTableRow",
|
|
188
|
+
inheritAttrs: false,
|
|
189
|
+
props: { header: { type: Boolean, default: false } },
|
|
190
|
+
setup: fwd("table-row"),
|
|
191
|
+
});
|
|
192
|
+
export const TableCell = defineComponent({
|
|
193
|
+
name: "JasyTableCell",
|
|
194
|
+
inheritAttrs: false,
|
|
195
|
+
setup: fwd("table-cell"),
|
|
196
|
+
});
|
|
197
|
+
// Out-of-flow child, anchored to the nearest `<Box relative>` (or the page). Edges or `h`/`v` + `x`/`y`.
|
|
198
|
+
export const Positioned = defineComponent({
|
|
199
|
+
name: "JasyPositioned",
|
|
200
|
+
inheritAttrs: false,
|
|
201
|
+
props: positionedProps,
|
|
202
|
+
setup: fwd("positioned"),
|
|
203
|
+
});
|
|
204
|
+
// Re-defaults the text style for its subtree (the per-section counterpart to `<Document>` defaults).
|
|
205
|
+
export const DefaultTextStyle = defineComponent({
|
|
206
|
+
name: "JasyDefaultTextStyle",
|
|
207
|
+
inheritAttrs: false,
|
|
208
|
+
props: defaultTextStyleProps,
|
|
209
|
+
setup: fwd("default-text-style"),
|
|
210
|
+
});
|
|
211
|
+
const components = {
|
|
212
|
+
Document,
|
|
213
|
+
Page,
|
|
214
|
+
Column,
|
|
215
|
+
Row,
|
|
216
|
+
Box,
|
|
217
|
+
Padding,
|
|
218
|
+
Expanded,
|
|
219
|
+
Spacer,
|
|
220
|
+
Divider,
|
|
221
|
+
Image,
|
|
222
|
+
Text,
|
|
223
|
+
Paragraph,
|
|
224
|
+
Span,
|
|
225
|
+
Table,
|
|
226
|
+
TableRow,
|
|
227
|
+
TableCell,
|
|
228
|
+
Positioned,
|
|
229
|
+
DefaultTextStyle,
|
|
230
|
+
};
|
|
231
|
+
// Register the components globally, optionally under a `prefix` to avoid name clashes with a UI library:
|
|
232
|
+
// `app.use(jasyVue, { prefix: "Pdf" })` → `<PdfRow>`, `<PdfText>`, …
|
|
233
|
+
export const jasyVue = {
|
|
234
|
+
install(app, options = {}) {
|
|
235
|
+
const prefix = options.prefix ?? "";
|
|
236
|
+
for (const [name, comp] of Object.entries(components)) {
|
|
237
|
+
app.component(prefix + name, comp);
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Component } from "vue";
|
|
2
|
+
import { type RenderOptions } from "@jasy/pdf";
|
|
3
|
+
export * from "./components.js";
|
|
4
|
+
export { toDocumentDescriptor } from "./renderer.js";
|
|
5
|
+
/** Render a Vue component (whose root is `<Document>`) to PDF bytes - browser or Node. */
|
|
6
|
+
export declare function renderToPdf(root: Component, props?: Record<string, any>, options?: RenderOptions): Promise<Uint8Array<ArrayBuffer>>;
|
|
7
|
+
/** Render a Vue component to the raw PDF string - browser or Node. */
|
|
8
|
+
export declare function renderToPdfString(root: Component, props?: Record<string, any>, options?: RenderOptions): Promise<string>;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { buildDocument, renderToBytes, renderPdf } from "@jasy/pdf";
|
|
2
|
+
import { toDocumentDescriptor } from "./renderer.js";
|
|
3
|
+
export * from "./components.js";
|
|
4
|
+
export { toDocumentDescriptor } from "./renderer.js";
|
|
5
|
+
/** Render a Vue component (whose root is `<Document>`) to PDF bytes - browser or Node. */
|
|
6
|
+
export function renderToPdf(root, props, options) {
|
|
7
|
+
return renderToBytes(buildDocument(toDocumentDescriptor(root, props)), options);
|
|
8
|
+
}
|
|
9
|
+
/** Render a Vue component to the raw PDF string - browser or Node. */
|
|
10
|
+
export function renderToPdfString(root, props, options) {
|
|
11
|
+
return renderPdf(buildDocument(toDocumentDescriptor(root, props)), options);
|
|
12
|
+
}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { renderToPdf, renderToPdfString } from "./index.js";
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Component } from "vue";
|
|
2
|
+
import type { Descriptor } from "@jasy/pdf";
|
|
3
|
+
/**
|
|
4
|
+
* Mounts a component once (one-shot - a PDF is not interactive) and returns the engine descriptor for
|
|
5
|
+
* its `<Document>` root. The descriptor is a plain serialisable object, so it can be posted to a Node
|
|
6
|
+
* renderer from the browser, or handed straight to `renderToPdf` (from `@jasy/vue/node`) on the server.
|
|
7
|
+
*/
|
|
8
|
+
export declare function toDocumentDescriptor(root: Component, props?: Record<string, any>): Descriptor;
|
package/dist/renderer.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createRenderer } from "vue";
|
|
2
|
+
const node = (type) => ({ type, props: {}, children: [], parent: null });
|
|
3
|
+
// The custom renderer: nodeOps that build the JNode tree instead of touching a DOM.
|
|
4
|
+
const { createApp } = createRenderer({
|
|
5
|
+
createElement: (type) => node(type),
|
|
6
|
+
createText: (text) => Object.assign(node("#text"), { text }),
|
|
7
|
+
createComment: () => node("#comment"),
|
|
8
|
+
setText: (n, text) => {
|
|
9
|
+
n.text = text;
|
|
10
|
+
},
|
|
11
|
+
setElementText: (n, text) => {
|
|
12
|
+
n.children = [Object.assign(node("#text"), { text })];
|
|
13
|
+
},
|
|
14
|
+
insert: (child, parent, anchor) => {
|
|
15
|
+
child.parent = parent;
|
|
16
|
+
const i = anchor ? parent.children.indexOf(anchor) : -1;
|
|
17
|
+
if (i >= 0)
|
|
18
|
+
parent.children.splice(i, 0, child);
|
|
19
|
+
else
|
|
20
|
+
parent.children.push(child);
|
|
21
|
+
},
|
|
22
|
+
remove: (child) => {
|
|
23
|
+
const siblings = child.parent?.children;
|
|
24
|
+
const i = siblings?.indexOf(child) ?? -1;
|
|
25
|
+
if (siblings && i >= 0)
|
|
26
|
+
siblings.splice(i, 1);
|
|
27
|
+
},
|
|
28
|
+
patchProp: (n, key, _prev, next) => {
|
|
29
|
+
n.props[key] = next;
|
|
30
|
+
},
|
|
31
|
+
parentNode: (n) => n.parent,
|
|
32
|
+
nextSibling: (n) => {
|
|
33
|
+
const siblings = n.parent?.children;
|
|
34
|
+
if (!siblings)
|
|
35
|
+
return null;
|
|
36
|
+
return siblings[siblings.indexOf(n) + 1] ?? null;
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
/** Strip our tree down to the engine's descriptor: text nodes become strings, comments are dropped. */
|
|
40
|
+
function toDescriptor(n) {
|
|
41
|
+
if (n.type === "#text")
|
|
42
|
+
return n.text ?? "";
|
|
43
|
+
return {
|
|
44
|
+
type: n.type,
|
|
45
|
+
props: n.props,
|
|
46
|
+
// Drop comments AND whitespace-only text nodes: Vue inserts empty `#text` anchors around lists
|
|
47
|
+
// (`v-for`) and templates leave whitespace between tags. As layout children each would become a
|
|
48
|
+
// 0-height empty Text that still gets a stray `gap` in its parent Column/Row - which silently
|
|
49
|
+
// inflates the height and can force a spurious extra page. Real text content is never whitespace.
|
|
50
|
+
children: n.children
|
|
51
|
+
.filter((c) => c.type !== "#comment" && !(c.type === "#text" && (c.text ?? "").trim() === ""))
|
|
52
|
+
.map(toDescriptor),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Mounts a component once (one-shot - a PDF is not interactive) and returns the engine descriptor for
|
|
57
|
+
* its `<Document>` root. The descriptor is a plain serialisable object, so it can be posted to a Node
|
|
58
|
+
* renderer from the browser, or handed straight to `renderToPdf` (from `@jasy/vue/node`) on the server.
|
|
59
|
+
*/
|
|
60
|
+
export function toDocumentDescriptor(root, props) {
|
|
61
|
+
const container = node("#root");
|
|
62
|
+
const app = createApp(root, props ?? null);
|
|
63
|
+
app.mount(container);
|
|
64
|
+
const doc = container.children.find((c) => c.type === "document");
|
|
65
|
+
app.unmount();
|
|
66
|
+
if (!doc)
|
|
67
|
+
throw new Error("@jasy/vue: the root component must render a <Document>.");
|
|
68
|
+
return toDescriptor(doc);
|
|
69
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jasy/vue",
|
|
3
|
+
"version": "1.0.0-alpha.1",
|
|
4
|
+
"description": "Author PDFs as Vue components - a thin Vue custom renderer over @jasy/pdf.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"vue",
|
|
7
|
+
"vue3",
|
|
8
|
+
"pdf",
|
|
9
|
+
"pdf-generation",
|
|
10
|
+
"declarative",
|
|
11
|
+
"renderer",
|
|
12
|
+
"invoice",
|
|
13
|
+
"jasy"
|
|
14
|
+
],
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "Florian Heuberger",
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "dist/index.js",
|
|
19
|
+
"types": "dist/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"import": "./dist/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./node": {
|
|
26
|
+
"types": "./dist/node.d.ts",
|
|
27
|
+
"import": "./dist/node.js"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist"
|
|
32
|
+
],
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@jasy/pdf": "1.0.0-alpha.2"
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"vue": "^3.4.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^25.9.3",
|
|
41
|
+
"@vitejs/plugin-vue": "^5.2.1",
|
|
42
|
+
"typescript": "^5.6.0",
|
|
43
|
+
"vite": "^6.3.4",
|
|
44
|
+
"vue": "^3.5.13",
|
|
45
|
+
"vue-pdf-embed": "^2.1.5"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsc",
|
|
49
|
+
"play": "vite playground --open",
|
|
50
|
+
"test": "vitest run"
|
|
51
|
+
}
|
|
52
|
+
}
|