├── .dumirc.ts
├── .editorconfig
├── .eslintrc.js
├── .fatherrc.ts
├── .gitignore
├── .husky
├── commit-msg
└── pre-commit
├── .prettierignore
├── .prettierrc.js
├── .stylelintrc
├── LICENSE
├── README.md
├── babel.config.js
├── docs
├── api.md
├── example.md
└── index.md
├── example
├── index.html
└── index.tsx
├── jest.config.js
├── package.json
├── pnpm-lock.yaml
├── src
├── __test__
│ └── jsxToPdfDocument.test.tsx
├── index.ts
├── jsxToPdfDocument.ts
├── strategy
│ ├── canvas.ts
│ ├── columns.ts
│ ├── document.ts
│ ├── img.ts
│ ├── index.ts
│ ├── link.ts
│ ├── ol.ts
│ ├── primitive.ts
│ ├── qr.ts
│ ├── stack.ts
│ ├── svg.ts
│ ├── table.tsx
│ ├── text.ts
│ ├── toc.ts
│ └── ul.ts
└── utils.ts
├── tsconfig.json
└── vite.config.js
/.dumirc.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'dumi';
2 |
3 | export default defineConfig({
4 | outputPath: 'docs-dist',
5 | themeConfig: {
6 | name: 'pdfmake-react',
7 | },
8 | });
9 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.md]
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: require.resolve('@umijs/lint/dist/config/eslint'),
3 | };
4 |
--------------------------------------------------------------------------------
/.fatherrc.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'father';
2 |
3 | export default defineConfig({
4 | // more father config: https://github.com/umijs/father/blob/master/docs/config.md
5 | esm: { output: 'dist' },
6 | umd: { output: 'dist', name: 'reactJsx2Pdf' },
7 | });
8 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /dist
3 | .dumi/tmp
4 | .dumi/tmp-test
5 | .dumi/tmp-production
6 | .DS_Store
7 | .yarn
--------------------------------------------------------------------------------
/.husky/commit-msg:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx commitlint --edit "${1}"
5 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname -- "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /dist
2 | *.yaml
3 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | pluginSearchDirs: false,
3 | plugins: [
4 | require.resolve('prettier-plugin-organize-imports'),
5 | require.resolve('prettier-plugin-packagejson'),
6 | ],
7 | printWidth: 80,
8 | proseWrap: 'never',
9 | singleQuote: true,
10 | trailingComma: 'all',
11 | overrides: [
12 | {
13 | files: '*.md',
14 | options: {
15 | proseWrap: 'preserve',
16 | },
17 | },
18 | ],
19 | };
20 |
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@umijs/lint/dist/config/stylelint"
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 271533323@qq.com
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | [](https://npmjs.org/package/react-jsx2pdf) [](https://npmjs.org/package/react-jsx2pdf)
4 |
5 | Generate modular PDFs via [pdfmake](http://pdfmake.org/) using JSX.
6 |
7 | ```jsx
8 | import React from 'react';
9 |
10 | import pdfMake from 'pdfmake/build/pdfmake';
11 | import pdfFonts from 'pdfmake/build/vfs_fonts';
12 |
13 | import { jsxToPdfDocument, html } from 'react-jsx2pdf';
14 |
15 | pdfMake.vfs = pdfFonts.pdfMake.vfs;
16 |
17 | const jsx = (
18 |
25 | this is a text
26 |
27 |
28 | name
29 | age
30 | adress
31 |
32 |
33 | Tom
34 | 18
35 | xxxxxx
36 |
37 |
38 | Bob
39 | 21
40 | xxxxxxx
41 |
42 |
43 |
44 |
45 | this is a ul 1
46 | this is a ul 1
47 | this is a ul 1
48 |
49 |
50 | this is a ol 1
51 | this is a ol 1
52 | this is a ol 1
53 |
54 | Svg
55 |
56 | {html``}
61 |
62 | go to baidu
63 |
64 | );
65 |
66 | const pdfDocument = jsxToPdfDocument(jsx);
67 |
68 | pdfMake.createPdf(pdfDocument).getBlob((blob) => {
69 | document.getElementById('iframe').src = URL.createObjectURL(blob);
70 | });
71 | ```
72 |
73 | 
74 |
75 | ## Feature
76 |
77 | - 🎉 Latest Jsx to PDF
78 | - 📦 Out-of-the-box
79 | - ❄️ Support TypeScript
80 | - 🕸 Customizable
81 |
82 | ## Quick Start
83 |
84 | ```cmd
85 | npm install react-jsx2pdf
86 | ```
87 |
88 | ```jsx
89 | import { jsxToPdfDocument } from 'react-jsx2pdf';
90 |
91 | const doc = Hello World;
92 |
93 | console.log(jsxToPdfDocument(doc));
94 | ```
95 |
96 | ## Typescript
97 |
98 | Intelligent grammar prompt without configuration
99 |
100 |
101 |
102 |
103 |
104 | ## Why not jsx-pdf
105 |
106 | ### Different
107 |
108 | [jsx-pdf](https://github.com/schibsted/jsx-pdf) is an excellent library, but it requires configuration of tsconfig and babel, while reat-jsx2pdf does not require configuration,`react-jsx2pdf` is a runtime library, and jsx-pdf is a compiletime library。
109 |
110 | ### Similarities
111 |
112 | The API of this library is similar to `jsx-pdf`
113 |
114 | All based on [pdfmake](http://pdfmake.org/) encapsulation
115 |
116 | ## Example
117 |
118 | ### text
119 |
120 | ```jsx
121 | const doc = (
122 |
123 | This is a Text
124 |
125 | This is a Bold
126 |
127 |
128 | );
129 | ```
130 |
131 | ### image
132 |
133 | ```jsx
134 | const doc = (
135 |
142 |
143 |
144 |
145 | );
146 | ```
147 |
148 | ### table
149 |
150 | ```jsx
151 | const doc =
152 |
153 |
154 | name
155 | age
156 | adress
157 |
158 |
159 | Tom
160 | 18
161 | xxxxxx
162 |
163 |
164 | Bob
165 | 21
166 | xxxxxxx
167 |
168 |
169 |
170 | ```
171 |
172 | ### ul
173 |
174 | ```jsx
175 | const doc = (
176 |
177 |
178 | this is a ul 1
179 | this is a ul 2
180 | this is a ul 3
181 |
182 |
183 | );
184 | ```
185 |
186 | ### ol
187 |
188 | ```jsx
189 | const doc = (
190 |
191 |
192 | this is a ol 1
193 | this is a ol 2
194 | this is a ol 3
195 |
196 |
197 | );
198 | ```
199 |
200 | ### canvas
201 |
202 | ```jsx
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
218 |
226 |
227 |
228 | ```
229 |
230 | ### svg
231 |
232 | ```jsx
233 | import { html } from 'react-jsx2pdf';
234 |
235 | const doc = (
236 |
237 |
238 | {html``}
279 |
280 |
281 | );
282 | ```
283 |
284 | You can use strings directly without using HTML`` syntax. HTML is for highlighting syntax and better editing of HTML
285 |
286 | ```jsx
287 | const svg = ``;
328 |
329 | const doc = (
330 |
331 | {svg}
332 |
333 | );
334 | ```
335 |
336 | ## Conponent
337 |
338 | ### base Component
339 |
340 | ```jsx
341 | const list = [1, 2, 3];
342 |
343 | const TextList = ({ list }) => {
344 | return list.map((item) => {item});
345 | };
346 |
347 | const doc = (
348 |
349 |
350 |
351 | );
352 | ```
353 |
354 | ## Regester JavaScriptXML
355 |
356 | ### regester echarts
357 |
358 | ```jsx
359 | import { registerStrategy } from 'react-jsx2pdf';
360 | import * as echarts from "echarts";
361 |
362 | const echartsRule = (element) => element.type === 'p-echarts'
363 |
364 | export const echartsHandler = (element) => {
365 | const { options, ...rest } = element.props;
366 |
367 | const domElement = document.createElement("div");
368 |
369 | domElement.style.width = `600px`;
370 | domElement.style.height = `400px`;
371 | const echarsInstance = echarts.init(domElement);
372 | echarsInstance.setOption({ ...options, animation: false });
373 | const url = echarsInstance.getDataURL({pixelRatio: devicePixelRatio});
374 | return
375 | };
376 |
377 | registerStrategy(echartsRule, echartsHandler)
378 |
379 | const options = // ..echarts options
380 |
381 | const doc =
382 |
383 |
384 | ```
385 |
386 | ## use context
387 |
388 | ```jsx
389 | import { jsxToPdfDocument } from 'react-jsx2pdf';
390 | const ctx = {name: 'test'}
391 | console.log(jsxToPdfDocument(doc, {ctx}));
392 |
393 | const Text = (_, ctx) => {
394 | return {ctx.name}
395 | }
396 |
397 | ```
398 |
399 | ## License
400 |
401 | MIT
402 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: [
3 | ['@babel/preset-env', {targets: {node: 'current'}}],
4 | '@babel/preset-typescript',
5 | ],
6 | };
--------------------------------------------------------------------------------
/docs/api.md:
--------------------------------------------------------------------------------
1 | # API
2 | ## jsxToPdfDocument
3 |
4 | ## r
--------------------------------------------------------------------------------
/docs/example.md:
--------------------------------------------------------------------------------
1 | ## OderList
2 |
3 | ```tsx
4 | import React, { useState, useRef, useEffect } from 'react'
5 | import { jsxToPdfDocument } from 'pdfmake-react'
6 | import pdfMake from "pdfmake/build/pdfmake";
7 | import pdfFonts from "pdfmake/build/vfs_fonts";
8 | pdfMake.vfs = pdfFonts.pdfMake.vfs;
9 | const S = () => {
10 | const ref= useRef(null)
11 | const [a, seta] = useState(0)
12 | useEffect(() => {
13 | const doc = jsxToPdfDocument(
14 |
15 |
16 | 1
17 | 2
18 | 3
19 |
20 |
21 | 1
22 | 2
23 | 3
24 |
25 |
26 |
27 | 123
28 | 123
29 | 123
30 |
31 |
32 | 123
33 | 123
34 | 123
35 |
36 |
37 | 123
38 | 123
39 | 123
40 |
41 |
42 |
43 |
44 | )
45 | console.log('doc', doc);
46 | pdfMake
47 | .createPdf(doc).getBlob((blob) => {
48 | const blobURL = URL.createObjectURL(blob);
49 | ref.current.src = blobURL;
50 | });
51 | }, [])
52 |
53 | return ;
54 | }
55 | export default S;
56 |
57 | ```
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | SDDDDDD
2 |
3 | # React
4 |
5 | ## React1
6 | 123
7 | ## React2
8 |
9 | 123
--------------------------------------------------------------------------------
/example/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Document
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/example/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import pdfMake from 'pdfmake/build/pdfmake';
4 | import pdfFonts from 'pdfmake/build/vfs_fonts';
5 |
6 | import { jsxToPdfDocument } from '../src';
7 |
8 | pdfMake.vfs = pdfFonts.pdfMake.vfs;
9 |
10 | const T = (_, ctx) => {
11 | debugger
12 | return {ctx.text}
13 | }
14 |
15 | const XX = () => {
16 | return (
17 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
39 |
47 |
48 |
49 |
50 | );
51 | }
52 |
53 | const pdfDocument = jsxToPdfDocument(XX(), {
54 | ctx: {
55 | text: 'hahahahahha'
56 | }
57 | });
58 |
59 | pdfMake.createPdf(pdfDocument).getBlob((blob) => {
60 | (document.getElementById('ifa') as HTMLIFrameElement).src =
61 | URL.createObjectURL(blob);
62 | });
63 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest',
3 | testEnvironment: 'node',
4 | testMatch: ['**/__tests__/**/*.ts?(x)', '**/?(*.)+(spec|test).ts?(x)'],
5 | transform: {
6 | '^.+\\.(ts|tsx)$': 'ts-jest',
7 | },
8 | };
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-jsx2pdf",
3 | "version": "0.0.7",
4 | "description": "react-jsx2pdf",
5 | "keywords": [
6 | "pdf",
7 | "jsx",
8 | "react",
9 | "pdf-generation",
10 | "pdfmake"
11 | ],
12 | "homepage": "https://github.com/pengshengjie/react-jsx2pdf",
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/pengshengjie/react-jsx2pdf.git"
16 | },
17 | "license": "MIT",
18 | "unpkg": "dist/react-jsx2pdf.min.js",
19 | "module": "dist/index.js",
20 | "types": "dist/index.d.ts",
21 | "files": [
22 | "dist"
23 | ],
24 | "scripts": {
25 | "build": "father build",
26 | "build:watch": "father dev",
27 | "dev": "dumi dev",
28 | "docs:build": "dumi build",
29 | "doctor": "father doctor",
30 | "lint": "npm run lint:es && npm run lint:css",
31 | "lint:css": "stylelint \"{src,test}/**/*.{css,less}\"",
32 | "lint:es": "eslint \"{src,test}/**/*.{js,jsx,ts,tsx}\"",
33 | "prepare": "husky install && dumi setup",
34 | "prepublishOnly": "father doctor && npm run build",
35 | "start": "npm run dev",
36 | "test": "jest",
37 | "vite": "vite example"
38 | },
39 | "commitlint": {
40 | "extends": [
41 | "@commitlint/config-conventional"
42 | ]
43 | },
44 | "lint-staged": {
45 | "*.{md,json}": [
46 | "prettier --write --no-error-on-unmatched-pattern"
47 | ],
48 | "*.{css,less}": [
49 | "stylelint --fix",
50 | "prettier --write"
51 | ],
52 | "*.{js,jsx}": [
53 | "eslint --fix",
54 | "prettier --write"
55 | ],
56 | "*.{ts,tsx}": [
57 | "eslint --fix",
58 | "prettier --parser=typescript --write"
59 | ]
60 | },
61 | "dependencies": {
62 | "@types/pdfmake": ">=0.2.2",
63 | "pdfmake": ">=0.0.0"
64 | },
65 | "devDependencies": {
66 | "@babel/preset-typescript": "^7.22.15",
67 | "@commitlint/cli": "^17.1.2",
68 | "@commitlint/config-conventional": "^17.1.0",
69 | "@types/jest": "^29.5.5",
70 | "@types/react": "^18.2.28",
71 | "@umijs/lint": "^4.0.0",
72 | "dumi": "^2.0.2",
73 | "eslint": "^8.23.0",
74 | "father": "^4.1.0",
75 | "husky": "^8.0.1",
76 | "jest": "^29.7.0",
77 | "lint-staged": "^13.0.3",
78 | "prettier": "^2.7.1",
79 | "prettier-plugin-organize-imports": "^3.0.0",
80 | "prettier-plugin-packagejson": "^2.2.18",
81 | "react": "^18.0.0",
82 | "react-dom": "^18.0.0",
83 | "stylelint": "^14.9.1",
84 | "ts-jest": "^29.1.1",
85 | "vite": "^4.4.9"
86 | },
87 | "peerDependencies": {
88 | "@types/pdfmake": ">=0.2.2",
89 | "react": ">=16.9.0"
90 | },
91 | "publishConfig": {
92 | "access": "public"
93 | },
94 | "authors": [
95 | "PengShengjie <271533323@qq.com>"
96 | ]
97 | }
98 |
--------------------------------------------------------------------------------
/src/__test__/jsxToPdfDocument.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { jsxToPdfDocument } from '..';
3 | describe('document', () => {
4 | it('document right', () => {
5 | const jsx = 123;
6 | expect(jsxToPdfDocument(jsx)).toEqual({
7 | content: '123',
8 | pageSize: 'A4',
9 | });
10 | });
11 | it('document more props', () => {
12 | const jsx = (
13 |
18 | 123
19 |
20 | );
21 | expect(jsxToPdfDocument(jsx)).toEqual({
22 | content: '123',
23 | pageSize: 'A4',
24 | pageMargins: [40, 40],
25 | images: {
26 | test: 'https://picsum.photos/seed/picsum/200/300',
27 | },
28 | });
29 | });
30 |
31 | it('document more props other', () => {
32 | const jsx = (
33 |
38 | 123
39 |
40 | );
41 | expect(jsxToPdfDocument(jsx)).toEqual({
42 | content: '123',
43 | pageSize: 'A4',
44 | pageMargins: [40, 40],
45 | images: {
46 | test: 'https://picsum.photos/seed/picsum/200/300',
47 | },
48 | });
49 | });
50 | it('document empty', () => {
51 | const jsx = ;
52 | expect(jsxToPdfDocument(jsx)).toEqual({
53 | content: '',
54 | });
55 | });
56 | });
57 |
58 | describe('text', () => {
59 | it('text right', () => {
60 | const jsx = (
61 |
62 | 123
63 |
64 | );
65 | expect(jsxToPdfDocument(jsx).content).toEqual({
66 | text: '123',
67 | });
68 | });
69 | it('text empty', () => {
70 | const jsx = (
71 |
72 | 123
73 |
74 | );
75 | expect(jsxToPdfDocument(jsx).content).toEqual({
76 | text: '123',
77 | });
78 | });
79 |
80 | it('text more props', () => {
81 | const jsx = (
82 |
83 |
84 | 666
85 |
86 |
87 | );
88 | expect(jsxToPdfDocument(jsx).content).toEqual({
89 | text: '666',
90 | color: 'red',
91 | fillColor: '#ddd',
92 | });
93 | });
94 | it('text more', () => {
95 | const jsx = (
96 |
97 | 1
98 | 2
99 |
100 | );
101 | expect(jsxToPdfDocument(jsx).content).toEqual([
102 | {
103 | text: '1',
104 | color: 'red',
105 | },
106 | {
107 | text: '2',
108 | color: 'blue',
109 | },
110 | ]);
111 | });
112 | });
113 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CanvasEllipse,
3 | CanvasLine,
4 | CanvasPolyline,
5 | CanvasRect,
6 | ContentCanvas,
7 | ContentColumns,
8 | ContentImage,
9 | ContentLink,
10 | ContentOrderedList,
11 | ContentQr,
12 | ContentStack,
13 | ContentTable,
14 | ContentText,
15 | ContentTocItem,
16 | ContentUnorderedList,
17 | Table,
18 | TableOfContent,
19 | TDocumentDefinitions,
20 | } from 'pdfmake/interfaces';
21 | import { ReactNode } from 'react';
22 |
23 | export { jsxToPdfDocument, parseElement } from './jsxToPdfDocument';
24 | export { registerStrategy, unregisterStrategy } from './strategy';
25 | export const html = String.raw;
26 |
27 | type WithChildren<
28 | T extends Record,
29 | K extends string | number | symbol,
30 | > = Omit & { key?: string | number | null };
31 |
32 | type Src = {
33 | src: string;
34 | };
35 |
36 | declare global {
37 | // eslint-disable-next-line @typescript-eslint/no-namespace
38 | namespace JSX {
39 | interface IntrinsicElements {
40 | 'p-text': WithChildren, 'text'>;
41 | 'p-img': WithChildren & Src;
42 | 'p-qr': WithChildren;
43 | 'p-col': WithChildren;
44 | 'p-table': WithChildren, 'body'>;
45 | 'p-tr': WithChildren;
46 | 'p-th': WithChildren;
47 | 'p-td': WithChildren;
48 | 'p-ol': WithChildren;
49 | 'p-ul': WithChildren;
50 | 'p-svg': IntrinsicElements['svg'];
51 | 'p-link': WithChildren & Src;
52 | 'p-stack': WithChildren;
53 | 'p-toc': WithChildren;
54 | 'p-canvas': WithChildren;
55 | 'p-rect': WithChildren;
56 | 'p-line': WithChildren;
57 | 'p-ellipse': WithChildren;
58 | 'p-polyline': WithChildren;
59 | 'p-document': WithChildren;
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/src/jsxToPdfDocument.ts:
--------------------------------------------------------------------------------
1 | import { strategy } from './strategy';
2 |
3 | import { Content, TDocumentDefinitions } from 'pdfmake/interfaces';
4 | import { ReactElement } from 'react';
5 |
6 | export function flatJSXElement(jsx: undefined, ctx?: any): undefined;
7 | export function flatJSXElement(jsx: ReactElement, ctx?: any): ReactElement;
8 | export function flatJSXElement(jsx: ReactElement[], ctx?: any): ReactElement[];
9 | export function flatJSXElement(jsx: any, ctx: any) {
10 | if (!(typeof jsx === 'object' && jsx !== null)) {
11 | return jsx || '';
12 | } else if (Array.isArray(jsx)) {
13 | return jsx.map((j) => flatJSXElement(j, ctx));
14 | } else if (typeof jsx.type === 'string') {
15 | if (!jsx.type.startsWith('p-')) {
16 | throw new Error(
17 | `PDF Element must be start widh [p-], but find the tagname ${jsx.type}, tagname may be [p-${jsx.type}]`,
18 | );
19 | }
20 | return {
21 | ...jsx,
22 | props: {
23 | ...jsx.props,
24 | children: flatJSXElement(jsx.props.children, ctx),
25 | },
26 | };
27 | } else if (typeof jsx.type === 'function') {
28 | return flatJSXElement(jsx.type(jsx.props, ctx), ctx);
29 | } else if (jsx.type === Symbol.for('react.fragment')) {
30 | return flatJSXElement(jsx.props.children, ctx);
31 | }
32 | }
33 |
34 | export function parseElement(element: ReactElement | ReactElement[]): Content {
35 | if (Array.isArray(element)) {
36 | return element.map((e) => parseElement(e));
37 | }
38 |
39 | let iterator = strategy.entries();
40 | let next = iterator.next();
41 | while (!next.done) {
42 | const [rule, handler] = next.value;
43 | if (rule(element as ReactElement)) {
44 | return handler(element as ReactElement);
45 | }
46 | next = iterator.next();
47 | }
48 | return undefined as unknown as Content;
49 | }
50 |
51 | type Option = {
52 | ctx: any
53 | }
54 |
55 | export function jsxToPdfDocument(element: ReactElement, option?: Option): TDocumentDefinitions {
56 | const jsx = flatJSXElement(element, option?.ctx || {});
57 | return parseElement(jsx) as unknown as TDocumentDefinitions;
58 | }
59 |
--------------------------------------------------------------------------------
/src/strategy/canvas.ts:
--------------------------------------------------------------------------------
1 | import { parseElement } from '../jsxToPdfDocument';
2 |
3 | import { Handler, Rule } from '.';
4 |
5 | export const canvasRule: Rule = (element) => element.type === 'p-canvas';
6 |
7 | export const canvasHandler: Handler = (element) => {
8 | const { children, ...rest } = element.props;
9 | const result = parseElement(children) || '';
10 | return {
11 | ...rest,
12 | canvas: Array.isArray(result) ? result : [result],
13 | };
14 | };
15 |
16 | const canvasChildElementsType = ['p-rect', 'p-line', 'p-ellipse', 'p-polyline'];
17 |
18 | export const canvasChildrenRule: Rule = (element) =>
19 | canvasChildElementsType.includes(element.type as string);
20 |
21 | export const canvasChildrenHandler: Handler = (element) => {
22 | const { ...rest } = element.props;
23 | // const result = parseElement(children) || '';
24 | return {
25 | type: (element.type as string).slice(2),
26 | ...rest,
27 | };
28 | };
29 |
--------------------------------------------------------------------------------
/src/strategy/columns.ts:
--------------------------------------------------------------------------------
1 | import { parseElement } from '../jsxToPdfDocument';
2 |
3 | import { Handler, Rule } from '.';
4 |
5 | export const colRule: Rule = (element) => element.type === 'p-col';
6 | export const colHandler: Handler = (element) => {
7 | const { children, ...rest } = element.props;
8 | return {
9 | columns: [].concat(parseElement(children) as any),
10 | ...rest,
11 | };
12 | };
13 |
--------------------------------------------------------------------------------
/src/strategy/document.ts:
--------------------------------------------------------------------------------
1 | import { parseElement } from '../jsxToPdfDocument';
2 |
3 | import { Handler, Rule } from '.';
4 |
5 | export const docRule: Rule = (element) => element.type === 'p-document';
6 |
7 | export const docHandler: Handler = (element) => {
8 | const { children, ...rest } = element.props;
9 | return {
10 | ...rest,
11 | content: parseElement(children),
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/src/strategy/img.ts:
--------------------------------------------------------------------------------
1 | import { Handler, Rule } from '.';
2 |
3 | export const imgRule: Rule = (element) => element.type === 'p-img';
4 | export const imgHandler: Handler = (element) => {
5 | const { src, ...rest } = element.props;
6 | return {
7 | ...rest,
8 | image: src,
9 | };
10 | };
11 |
--------------------------------------------------------------------------------
/src/strategy/index.ts:
--------------------------------------------------------------------------------
1 | import {
2 | canvasChildrenHandler,
3 | canvasChildrenRule,
4 | canvasHandler,
5 | canvasRule,
6 | } from './canvas';
7 | import { colHandler, colRule } from './columns';
8 | import { docHandler, docRule } from './document';
9 | import { imgHandler, imgRule } from './img';
10 | import { linkHandler, linkRule } from './link';
11 | import { olHandler, olRule } from './ol';
12 | import { primitiveHandler, primitiveRule } from './primitive';
13 | import { qrHandler, qrRule } from './qr';
14 | import { stackHandler, stackRule } from './stack';
15 | import { svgHandler, svgRule } from './svg';
16 | import { tableHandler, tableRule, tdHandler, tdRule } from './table';
17 | import { textHandler, textRule } from './text';
18 | import { tocHandler, tocRule } from './toc';
19 | import { ulHandler, ulRule } from './ul';
20 |
21 | import { ReactElement } from 'react';
22 |
23 | export type Rule = (element: ReactElement) => boolean;
24 | export type Handler = (element: ReactElement) => any;
25 |
26 | type Strategy = Map;
27 |
28 | export const strategy: Strategy = new Map();
29 |
30 | strategy.set(primitiveRule, primitiveHandler);
31 | strategy.set(textRule, textHandler);
32 | strategy.set(tableRule, tableHandler);
33 | strategy.set(tdRule, tdHandler);
34 | strategy.set(imgRule, imgHandler);
35 | strategy.set(colRule, colHandler);
36 | strategy.set(ulRule, ulHandler);
37 | strategy.set(olRule, olHandler);
38 | strategy.set(docRule, docHandler);
39 | strategy.set(stackRule, stackHandler);
40 | strategy.set(linkRule, linkHandler);
41 | strategy.set(svgRule, svgHandler);
42 | strategy.set(qrRule, qrHandler);
43 | strategy.set(tocRule, tocHandler);
44 | strategy.set(canvasRule, canvasHandler);
45 | strategy.set(canvasChildrenRule, canvasChildrenHandler);
46 |
47 | export const registerStrategy = (rule: Rule, handler: Handler) => {
48 | strategy.set(rule, handler);
49 | };
50 |
51 | export const unregisterStrategy = (rule: Rule) => {
52 | strategy.delete(rule);
53 | };
54 |
--------------------------------------------------------------------------------
/src/strategy/link.ts:
--------------------------------------------------------------------------------
1 | import { Handler, Rule } from '.';
2 |
3 | import { parseElement } from '../jsxToPdfDocument';
4 | import { pickKeyByObject } from '../utils';
5 |
6 | export const linkRule: Rule = (element) => element.type === 'p-link';
7 |
8 | export const linkHandler: Handler = (element) => {
9 | const { src, linkToPage, linkToDestination, children } = element.props;
10 |
11 | return {
12 | ...pickKeyByObject(
13 | { linkToPage, linkToDestination },
14 | 'linkToPage',
15 | 'linkToDestination',
16 | ),
17 | link: src,
18 | text: parseElement(children),
19 | };
20 | };
21 |
--------------------------------------------------------------------------------
/src/strategy/ol.ts:
--------------------------------------------------------------------------------
1 | import { parseElement } from '../jsxToPdfDocument';
2 |
3 | import { Handler, Rule } from '.';
4 |
5 | export const olRule: Rule = (element) => element.type === 'p-ol';
6 |
7 | export const olHandler: Handler = (element) => {
8 | const { children, ...rest } = element.props;
9 | return {
10 | ...rest,
11 | ol: [].concat(parseElement(children) as any),
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/src/strategy/primitive.ts:
--------------------------------------------------------------------------------
1 | import { Handler, Rule } from '.';
2 |
3 | export const primitiveRule: Rule = (element) =>
4 | typeof element === 'string' || typeof element === 'number' || !element;
5 | export const primitiveHandler: Handler = (element) => {
6 | if (element === null || element === undefined) {
7 | return '';
8 | }
9 | return element;
10 | };
11 |
--------------------------------------------------------------------------------
/src/strategy/qr.ts:
--------------------------------------------------------------------------------
1 | import { Handler, Rule } from '.';
2 |
3 | export const qrRule: Rule = (element) => element.type === 'p-qr';
4 | export const qrHandler: Handler = (element) => {
5 | const { children, ...rest } = element.props;
6 | if (typeof children === 'object' && children !== null) {
7 | throw new Error('p-qr children must be string');
8 | }
9 | return {
10 | ...rest,
11 | qr: children,
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/src/strategy/stack.ts:
--------------------------------------------------------------------------------
1 | import { parseElement } from '../jsxToPdfDocument';
2 |
3 | import { Handler, Rule } from '.';
4 |
5 | export const stackRule: Rule = (element) => element.type === 'p-stack';
6 |
7 | export const stackHandler: Handler = (element) => {
8 | const { children, ...rest } = element.props;
9 | const result = parseElement(children) || '';
10 | return {
11 | ...rest,
12 | stack: Array.isArray(result) ? result : [result],
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/src/strategy/svg.ts:
--------------------------------------------------------------------------------
1 | import { Handler, Rule } from '.';
2 |
3 | export const svgRule: Rule = (element) => element.type === 'p-svg';
4 | export const svgHandler: Handler = (element) => {
5 | const { children, ...rest } = element.props;
6 | return {
7 | ...rest,
8 | svg: children,
9 | };
10 | };
11 |
--------------------------------------------------------------------------------
/src/strategy/table.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { parseElement } from '../jsxToPdfDocument';
3 | import { is2DArray, isObject, pickKeyByObject, toArray } from '../utils';
4 |
5 | import { Handler, Rule } from '.';
6 |
7 | export const tdRule: Rule = (element) => element.type === 'p-td';
8 | export const tdHandler: Handler = (element) => {
9 | if (!isObject(element)) {
10 | return element;
11 | } else {
12 | const { children, ...otherProps } = element.props;
13 | if (
14 | Object.keys(otherProps).length === 0 &&
15 | children !== null &&
16 | typeof children !== 'object'
17 | ) {
18 | return parseElement(element.props.children);
19 | } else {
20 | return parseElement();
21 | }
22 | }
23 | };
24 |
25 | export const tableRule: Rule = (element) => element.type === 'p-table';
26 | export const tableHandler: Handler = (element) => {
27 | const { children, layout, ...rest } = element!.props;
28 |
29 | const body = toArray(children)
30 | .filter((e) => e.type === 'p-tr' || e.type === 'p-th')
31 | .map((th) => {
32 | const { children, ...thProps } = th.props;
33 | const tds = toArray(children)
34 | .filter((e) => e.type === 'p-td')
35 | .map((td, idx) => );
36 | return parseElement(tds);
37 | }) as any;
38 |
39 | const is2D = is2DArray(body);
40 | if (!is2D) {
41 | throw new Error('the table body is not 2D Array');
42 | }
43 | return {
44 | layout,
45 | ...pickKeyByObject({ layout }, 'layout'),
46 | table: {
47 | ...rest,
48 | body,
49 | },
50 | };
51 | };
52 |
--------------------------------------------------------------------------------
/src/strategy/text.ts:
--------------------------------------------------------------------------------
1 | import { Handler, Rule } from '.';
2 | import { parseElement } from '../jsxToPdfDocument';
3 |
4 | export const textRule: Rule = (element) => element.type === 'p-text';
5 | export const textHandler: Handler = (element) => {
6 | const { children, ...rest } = element.props;
7 | return {
8 | ...rest,
9 | text: parseElement(children),
10 | };
11 | };
12 |
--------------------------------------------------------------------------------
/src/strategy/toc.ts:
--------------------------------------------------------------------------------
1 | import { Handler, Rule } from '.';
2 | import { parseElement } from '../jsxToPdfDocument';
3 |
4 | export const tocRule: Rule = (element) => element.type === 'p-toc';
5 | export const tocHandler: Handler = (element) => {
6 | const { children, ...rest } = element.props;
7 | return {
8 | toc: {
9 | ...rest,
10 | title: parseElement(children),
11 | },
12 | };
13 | };
14 |
--------------------------------------------------------------------------------
/src/strategy/ul.ts:
--------------------------------------------------------------------------------
1 | import { parseElement } from '../jsxToPdfDocument';
2 |
3 | import { Handler, Rule } from '.';
4 |
5 | export const ulRule: Rule = (element) => element.type === 'p-ul';
6 | export const ulHandler: Handler = (element) => {
7 | const { children, ...rest } = element.props;
8 | return {
9 | ...rest,
10 | ul: [].concat(parseElement(children) as any),
11 | };
12 | };
13 |
--------------------------------------------------------------------------------
/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { ReactElement } from 'react';
2 |
3 | export const toArray = (element: ReactElement): ReactElement[] => {
4 | if (Array.isArray(element)) {
5 | return element.reduce((pre, item) => [...pre, ...toArray(item)], []);
6 | }
7 |
8 | if (element) {
9 | return [element];
10 | }
11 | return [];
12 | };
13 |
14 | export const isObject: (o: any) => boolean = (o) => {
15 | return typeof o === 'object' && o !== null;
16 | };
17 |
18 | export const pickKeyByObject = (
19 | obj: T,
20 | ...keys: (keyof T)[]
21 | ): Partial => {
22 | const initObj: Partial = {};
23 |
24 | return keys.reduce((pre, key) => {
25 | if (obj[key] !== undefined) {
26 | pre[key] = obj[key];
27 | }
28 | return pre;
29 | }, initObj);
30 | };
31 |
32 | export const is2DArray = (body: ReactElement[][]) => {
33 | const firstRowLength = body[0].length;
34 | return body.every((row) => {
35 | return row.length === firstRowLength;
36 | });
37 | };
38 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "declaration": true,
5 | "skipLibCheck": true,
6 | "esModuleInterop": true,
7 | "jsx": "react",
8 | "baseUrl": "./"
9 | },
10 | "include": [
11 | ".dumirc.ts",
12 | "src/**/*"
13 | ]
14 | }
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | export default defineConfig({
3 | root: './example',
4 | build: {
5 | outDir: '../dist/example',
6 | },
7 | server: {
8 | open: '/index.html',
9 | },
10 |
11 | })
--------------------------------------------------------------------------------