├── .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 | ![pdf-log](https://github.com/pengshengjie/react-jsx2pdf/assets/117100743/b3600b0b-8ef2-490b-b9a8-e575ea073294) 2 | 3 | [![NPM version](https://img.shields.io/npm/v/react-jsx2pdf.svg?style=flat)](https://npmjs.org/package/react-jsx2pdf) [![NPM downloads](http://img.shields.io/npm/dm/react-jsx2pdf.svg?style=flat)](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` 57 | 60 | `} 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 | ![image](https://github.com/pengshengjie/react-jsx2pdf/assets/117100743/e900d827-df4a-4189-82c2-e92c98133b6e) 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 | b217281fdf57fe6b0f4301b5a634e51 101 | 102 | 8bb3bd2e2b57a4df476e58548b268fb 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` 245 | 249 | 253 | 254 | 262 | 263 | 264 | 265 | 273 | 274 | 275 | 276 | 277 | 278 | `} 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 = ` 294 | 298 | 302 | 303 | 311 | 312 | 313 | 314 | 322 | 323 | 324 | 325 | 326 | 327 | `; 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 | }) --------------------------------------------------------------------------------