├── .eslintrc.json ├── .gitignore ├── .npmignore ├── .prettierrc ├── .vscode └── launch.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── commitlint.config.js ├── dist ├── html-to-docx.esm.js ├── html-to-docx.esm.js.map ├── html-to-docx.umd.js └── html-to-docx.umd.js.map ├── example ├── example-node.js └── example.js ├── index.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── docx-document.js ├── helpers │ ├── index.js │ ├── namespaces.js │ ├── render-document-file.js │ └── xml-builder.js ├── html-to-docx.js ├── schemas │ ├── content-types.js │ ├── core.js │ ├── document-rels.js │ ├── font-table.js │ ├── generic-rels.js │ ├── index.js │ ├── numbering.js │ ├── rels.js │ ├── settings.js │ ├── styles.js │ ├── theme.js │ └── web-settings.js └── utils │ ├── color-conversion.js │ └── unit-conversion.js └── template └── document.template.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb", "prettier"], 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "prettier/prettier": ["error"] 6 | }, 7 | "ignorePatterns": "dist/**/*.js" 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Node ### 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | lerna-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional REPL history 58 | .node_repl_history 59 | 60 | # Output of 'npm pack' 61 | *.tgz 62 | 63 | # Yarn Integrity file 64 | .yarn-integrity 65 | 66 | # dotenv environment variables file 67 | .env 68 | .env.test 69 | 70 | # parcel-bundler cache (https://parceljs.org/) 71 | .cache 72 | 73 | # next.js build output 74 | .next 75 | 76 | # nuxt.js build output 77 | .nuxt 78 | 79 | 80 | # Uncomment the public line if your project uses Gatsby 81 | # https://nextjs.org/blog/next-9-1#public-directory-support 82 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 83 | # public 84 | 85 | # Storybook build outputs 86 | .out 87 | .storybook-out 88 | 89 | # vuepress build output 90 | .vuepress/dist 91 | 92 | # Serverless directories 93 | .serverless/ 94 | 95 | # FuseBox cache 96 | .fusebox/ 97 | 98 | # DynamoDB Local files 99 | .dynamodb/ 100 | 101 | # Temporary folders 102 | tmp/ 103 | temp/ 104 | 105 | *.lock 106 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | !dist/* 3 | !example/* 4 | !package.json 5 | !package-lock.json 6 | !README.md 7 | !LICENSE -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true 4 | } 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node-terminal", 6 | "name": "Run Script: test", 7 | "request": "launch", 8 | "command": "npm run test", 9 | "cwd": "${workspaceFolder}" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 privateOmega 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | html-to-docx 2 | ============ 3 | 4 | [![NPM Version][npm-image]][npm-url] 5 | 6 | html-to-docx is a js library for converting HTML documents to DOCX format supported by Microsoft Word 2007+, LibreOffice Writer, Google Docs, WPS Writer etc. 7 | 8 | It was inspired by [html-docx-js] project but mitigates the problem of documents generated being non-compatiable with word processors like Google Docs and libreOffice Writer that doesn't support [altchunks] feature. 9 | 10 | html-to-docx earlier used to use [libtidy] to clean up the html before parsing, but had to remove it since it was causing so many dependency issues due to node-gyp. 11 | 12 | ### Disclaimer 13 | 14 | Even though there is an instance of html-to-docx running in production, please ensure that it covers all the cases that you might be encountering usually, since this is not a complete solution. 15 | 16 | Currently it doesn't work with browser directly, but it was tested against React. 17 | 18 | ## Installation 19 | 20 | Use the npm to install foobar. 21 | 22 | ```bash 23 | npm install html-to-docx 24 | ``` 25 | 26 | ## Usage 27 | 28 | ```js 29 | await HTMLtoDOCX(htmlString, headerHTMLString, documentOptions, footerHTMLString) 30 | ``` 31 | 32 | full fledged examples can be found under `example/` 33 | 34 | ### Parameters 35 | 36 | - `htmlString` <[String]> clean html string equivalent of document content. 37 | - `headerHTMLString` <[String]> clean html string equivalent of header. Defaults to `

` if header flag is `true`. 38 | - `documentOptions` 39 | - `orientation` <"portrait"|"landscape"> defines the general orientation of the document. Defaults to portrait. 40 | - `margins` 41 | - `top` <[Number]> distance between the top of the text margins for the main document and the top of the page for all pages in this section in [TWIP]. Defaults to 1440. Supports equivalent measurement in [pixel], [cm] or [inch]. 42 | - `right` <[Number]> distance between the right edge of the page and the right edge of the text extents for this document in [TWIP]. Defaults to 1800. Supports equivalent measurement in [pixel], [cm] or [inch]. 43 | - `bottom` <[Number]> distance between the bottom of text margins for the document and the bottom of the page in [TWIP]. Defaults to 1440. Supports equivalent measurement in [pixel], [cm] or [inch]. 44 | - `left` <[Number]> distance between the left edge of the page and the left edge of the text extents for this document in [TWIP]. Defaults to 1800. Supports equivalent measurement in [pixel], [cm] or [inch]. 45 | - `header` <[Number]> distance from the top edge of the page to the top edge of the header in [TWIP]. Defaults to 720. Supports equivalent measurement in [pixel], [cm] or [inch]. 46 | - `footer` <[Number]> distance from the bottom edge of the page to the bottom edge of the footer in [TWIP]. Defaults to 720. Supports equivalent measurement in [pixel], [cm] or [inch]. 47 | - `gutter` <[Number]> amount of extra space added to the specified margin, above any existing margin values. This setting is typically used when a document is being created for binding in [TWIP]. Defaults to 0. Supports equivalent measurement in [pixel], [cm] or [inch]. 48 | - `title` title of the document. 49 | - `subject` subject of the document. 50 | - `creator` creator of the document. Defaults to `html-to-docx` 51 | - `keywords` > keywords associated with the document. Defaults to ['html-to-docx']. 52 | - `description` description of the document. 53 | - `lastModifiedBy` last modifier of the document. Defaults to `html-to-docx`. 54 | - `revision` revision of the document. Defaults to `1`. 55 | - `createdAt` time of creation of the document. Defaults to current time. 56 | - `modifiedAt` time of last modification of the document. Defaults to current time. 57 | - `headerType` <"default"|"first"|"even"> type of header. Defaults to `default`. 58 | - `header` flag to enable header. Defaults to `false`. 59 | - `footerType` <"default"|"first"|"even"> type of footer. Defaults to `default`. 60 | - `footer` flag to enable footer. Defaults to `false`. 61 | - `font` font name to be used. Defaults to `Times New Roman`. 62 | - `fontSize` size of font in HIP(Half of point). Defaults to `22`. Supports equivalent measure in [pt]. 63 | - `complexScriptFontSize` size of complex script font in HIP(Half of point). Defaults to `22`. Supports equivalent measure in [pt]. 64 | - `table` 65 | - `row` 66 | - `cantSplit` flag to allow table row to split across pages. Defaults to `false`. 67 | - `pageNumber` flag to enable page number in footer. Defaults to `false`. Page number works only if footer flag is set as `true`. 68 | - `skipFirstHeaderFooter` flag to skip first page header and footer. Defaults to `false`. 69 | - `lineNumber` flag to enable line numbering. Defaults to `false`. 70 | - `lineNumberOptions` 71 | - `start` <[Number]> start of the numbering - 1. Defaults to `0`. 72 | - `countBy` <[Number]> skip numbering in how many lines in between + 1. Defaults to `1`. 73 | - `restart` <"continuous"|"newPage"|"newSection"> numbering restart strategy. Defaults to `continuous`. 74 | - `footerHTMLString` <[String]> clean html string equivalent of footer. Defaults to `

` if footer flag is `true`. 75 | 76 | ### Returns 77 | 78 | <[Promise]<[Buffer]|[Blob]>> 79 | 80 | ## Notes 81 | 82 | Currently page break can be implemented by having div with classname "page-break" or style "page-break-after" despite the values of the "page-break-after", and contents inside the div element will be ignored. `
` 83 | 84 | ## Contributing 85 | 86 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 87 | 88 | Please make sure to branch new branches off of develop for contribution. 89 | 90 | ## License 91 | 92 | MIT 93 | 94 | [npm-image]: https://img.shields.io/npm/v/html-to-docx.svg 95 | [npm-url]: https://npmjs.org/package/html-to-docx 96 | [html-docx-js]: https://github.com/evidenceprime/html-docx-js "html-docx-js" 97 | [altchunks]: https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.altchunk?view=openxml-2.8.1 "altchunks" 98 | [libtidy]: https://github.com/jure/node-libtidy "libtidy" 99 | [String]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#String_type "String" 100 | [Object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object "Object" 101 | [Number]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Number_type "Number" 102 | [TWIP]: https://en.wikipedia.org/wiki/Twip "TWIP" 103 | [Array]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array "Array" 104 | [Date]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date "Date" 105 | [Boolean]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures#Boolean_type "Boolean" 106 | [Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise "Promise" 107 | [Buffer]: https://nodejs.org/api/buffer.html#buffer_buffer "Buffer" 108 | [Blob]: https://developer.mozilla.org/en-US/docs/Web/API/Blob "Blob" 109 | [pixel]: https://en.wikipedia.org/wiki/Pixel#:~:text=Pixels%2C%20abbreviated%20as%20%22px%22,what%20screen%20resolution%20views%20it. "pixel" 110 | [cm]: https://en.wikipedia.org/wiki/Centimetre "cm" 111 | [inch]: https://en.wikipedia.org/wiki/Inch "inch" 112 | [pt]: https://en.wikipedia.org/wiki/Point_(typography) "pt" -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | }; 4 | -------------------------------------------------------------------------------- /example/example-node.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | const fs = require('fs'); 3 | // FIXME: Incase you have the npm package 4 | // const HTMLtoDOCX = require('html-to-docx'); 5 | const HTMLtoDOCX = require('../dist/html-to-docx.umd'); 6 | 7 | const filePath = './example.docx'; 8 | 9 | const htmlString = ` 10 | 11 | 12 | 13 | Document 14 | 15 | 16 |
17 |

Taken from wikipedia

18 | Red dot 22 |
23 |
24 |

This is heading 1

25 |

Content

26 |

This is heading 2

27 |

Content

28 |

This is heading 3

29 |

Content

30 |

This is heading 4

31 |

Content

32 |
This is heading 5
33 |

Content

34 |
This is heading 6
35 |

Content

36 |
37 |

38 | 39 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make 40 | a type specimen book. 41 | 42 | It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. 43 | It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages,and more recently with desktop publishing software 44 | like Aldus PageMaker including versions of Lorem Ipsum. 45 | Where does it come from? Contrary to popular belief, Lorem Ipsum is not simply random text. 46 | It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. 47 |

48 |
49 | For 50 years, WWF has been protecting the future of nature. The world's leading conservation organization, WWF works in 100 countries and is supported by 1.2 million members in the United States and close to 5 million globally. 50 |
51 |

52 | 53 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make 54 | a type specimen book. 55 | 56 |

57 | 60 |
61 |
    62 |
  1. Ordered list element
  2. 63 |
64 |
65 | 95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
CountryCapital
IndiaNew Delhi
United States of AmericaWashington DC
110 | 111 | `; 112 | 113 | (async () => { 114 | const fileBuffer = await HTMLtoDOCX(htmlString, null, { 115 | table: { row: { cantSplit: true } }, 116 | footer: true, 117 | pageNumber: true, 118 | }); 119 | 120 | fs.writeFile(filePath, fileBuffer, (error) => { 121 | if (error) { 122 | console.log('Docx file creation failed'); 123 | return; 124 | } 125 | console.log('Docx file created successfully'); 126 | }); 127 | })(); 128 | -------------------------------------------------------------------------------- /example/example.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-console */ 2 | import fs from 'fs'; 3 | // FIXME: Incase you have the npm package 4 | // import HTMLtoDOCX from 'html-to-docx'; 5 | import HTMLtoDOCX from '../dist/html-to-docx.esm'; 6 | 7 | const filePath = './example.docx'; 8 | 9 | const htmlString = ` 10 | 11 | 12 | 13 | Document 14 | 15 | 16 |
17 |

Taken from wikipedia

18 | Red dot 22 |
23 |
24 |

This is heading 1

25 |

Content

26 |

This is heading 2

27 |

Content

28 |

This is heading 3

29 |

Content

30 |

This is heading 4

31 |

Content

32 |
This is heading 5
33 |

Content

34 |
This is heading 6
35 |

Content

36 |
37 |

38 | 39 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make 40 | a type specimen book. 41 | 42 | It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. 43 | It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages,and more recently with desktop publishing software 44 | like Aldus PageMaker including versions of Lorem Ipsum. 45 | Where does it come from? Contrary to popular belief, Lorem Ipsum is not simply random text. 46 | It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. 47 |

48 |
49 | For 50 years, WWF has been protecting the future of nature. The world's leading conservation organization, WWF works in 100 countries and is supported by 1.2 million members in the United States and close to 5 million globally. 50 |
51 |

52 | 53 | Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make 54 | a type specimen book. 55 | 56 |

57 | 60 |
61 |
    62 |
  1. Ordered list element
  2. 63 |
64 |
65 | 95 |
96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 |
CountryCapital
IndiaNew Delhi
United States of AmericaWashington DC
110 | 111 | `; 112 | 113 | (async () => { 114 | const fileBuffer = await HTMLtoDOCX(htmlString, null, { 115 | table: { row: { cantSplit: true } }, 116 | footer: true, 117 | pageNumber: true, 118 | }); 119 | 120 | fs.writeFile(filePath, fileBuffer, (error) => { 121 | if (error) { 122 | console.log('Docx file creation failed'); 123 | return; 124 | } 125 | console.log('Docx file created successfully'); 126 | }); 127 | })(); 128 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-useless-escape */ 2 | /* eslint-disable no-param-reassign */ 3 | import JSZip from 'jszip'; 4 | import { addFilesToContainer } from './src/html-to-docx'; 5 | 6 | const minifyHTMLString = (htmlString) => { 7 | if (typeof htmlString === 'string' || htmlString instanceof String) { 8 | try { 9 | const minifiedHTMLString = htmlString 10 | .replace(/\n/g, ' ') 11 | .replace(/\r/g, ' ') 12 | .replace(/\r\n/g, ' ') 13 | .replace(/[\t]+\[\t ]+\<') 15 | .replace(/\>[\t ]+$/g, '>'); 16 | 17 | return minifiedHTMLString; 18 | } catch (error) { 19 | return null; 20 | } 21 | } else { 22 | return null; 23 | } 24 | }; 25 | 26 | async function generateContainer( 27 | htmlString, 28 | headerHTMLString, 29 | documentOptions = {}, 30 | footerHTMLString 31 | ) { 32 | const zip = new JSZip(); 33 | 34 | let contentHTML = htmlString; 35 | let headerHTML = headerHTMLString; 36 | let footerHTML = footerHTMLString; 37 | if (htmlString) { 38 | contentHTML = minifyHTMLString(contentHTML); 39 | } 40 | if (headerHTMLString) { 41 | headerHTML = minifyHTMLString(headerHTML); 42 | } 43 | if (footerHTMLString) { 44 | footerHTML = minifyHTMLString(footerHTML); 45 | } 46 | 47 | addFilesToContainer(zip, contentHTML, documentOptions, headerHTML, footerHTML); 48 | 49 | const buffer = await zip.generateAsync({ type: 'arraybuffer' }); 50 | if (Object.prototype.hasOwnProperty.call(global, 'Blob')) { 51 | // eslint-disable-next-line no-undef 52 | return new Blob([buffer], { 53 | type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 54 | }); 55 | } 56 | if (Object.prototype.hasOwnProperty.call(global, 'Buffer')) { 57 | return Buffer.from(new Uint8Array(buffer)); 58 | } 59 | throw new Error( 60 | 'Add blob support using a polyfill eg https://github.com/bjornstar/blob-polyfill' 61 | ); 62 | } 63 | 64 | export default generateContainer; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-to-docx", 3 | "version": "1.2.4", 4 | "description": "HTML to DOCX converter", 5 | "keywords": [ 6 | "html", 7 | "docx", 8 | "html-to-docx", 9 | "html to docx", 10 | "office", 11 | "word" 12 | ], 13 | "main": "dist/html-to-docx.umd.js", 14 | "module": "dist/html-to-docx.esm.js", 15 | "scripts": { 16 | "test": "npm run build && node example/example-node.js", 17 | "release": "standard-version", 18 | "lint": "eslint --fix .", 19 | "prettier:check": "prettier --check '**/*.{js}'", 20 | "validate": "run-s lint prettier:check", 21 | "build": "rollup -c" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/privateOmega/html-to-docx.git" 26 | }, 27 | "author": "privateOmega ", 28 | "contributors": [ 29 | "amrita-syn ", 30 | "charuthaB " 31 | ], 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/privateOmega/html-to-docx/issues" 35 | }, 36 | "homepage": "https://github.com/privateOmega/html-to-docx#readme", 37 | "devDependencies": { 38 | "@commitlint/cli": "^8.3.5", 39 | "@commitlint/config-conventional": "^8.3.4", 40 | "@rollup/plugin-commonjs": "^12.0.0", 41 | "@rollup/plugin-json": "^4.1.0", 42 | "@rollup/plugin-node-resolve": "^8.1.0", 43 | "eslint": "^6.8.0", 44 | "eslint-config-airbnb": "^18.2.0", 45 | "eslint-config-prettier": "^6.11.0", 46 | "eslint-plugin-import": "^2.22.0", 47 | "eslint-plugin-jsx-a11y": "^6.3.1", 48 | "eslint-plugin-prettier": "^3.1.4", 49 | "eslint-plugin-react": "^7.20.3", 50 | "eslint-plugin-react-hooks": "^2.5.1", 51 | "husky": "^4.2.5", 52 | "lint-staged": "^10.2.11", 53 | "npm-run-all": "^4.1.5", 54 | "prettier": "^2.0.5", 55 | "rollup": "^2.18.2", 56 | "rollup-plugin-cleaner": "^1.0.0", 57 | "rollup-plugin-node-builtins": "^2.1.2", 58 | "rollup-plugin-node-resolve": "^5.2.0", 59 | "rollup-plugin-terser": "^6.1.0", 60 | "standard-version": "^8.0.0" 61 | }, 62 | "dependencies": { 63 | "color-name": "^1.1.4", 64 | "escape-html": "^1.0.3", 65 | "html-to-vdom": "^0.7.0", 66 | "image-size": "^0.8.3", 67 | "jszip": "^3.5.0", 68 | "shortid": "^2.2.15", 69 | "virtual-dom": "^2.1.1", 70 | "xmlbuilder2": "2.1.2" 71 | }, 72 | "config": { 73 | "commitizen": { 74 | "path": "cz-conventional-changelog" 75 | } 76 | }, 77 | "husky": { 78 | "hooks": { 79 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 80 | "pre-commit": "lint-staged" 81 | } 82 | }, 83 | "lint-staged": { 84 | "src/**/*.js": [ 85 | "prettier --write", 86 | "eslint --fix", 87 | "git add" 88 | ] 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import json from '@rollup/plugin-json'; 3 | import commonjs from '@rollup/plugin-commonjs'; 4 | import { terser } from 'rollup-plugin-terser'; 5 | import cleaner from 'rollup-plugin-cleaner'; 6 | import builtins from 'rollup-plugin-node-builtins'; 7 | 8 | import * as meta from './package.json'; 9 | 10 | export default { 11 | input: 'index.js', 12 | external: ['color-name', 'escape-html', 'html-to-vdom', 'jszip', 'virtual-dom', 'xmlbuilder2'], 13 | plugins: [ 14 | resolve({ browser: true }), 15 | json({ include: 'package.json', preferConst: true }), 16 | commonjs(), 17 | builtins(), 18 | terser({ 19 | mangle: false, 20 | }), 21 | cleaner({ 22 | targets: ['./dist/'], 23 | }), 24 | ], 25 | output: [ 26 | { 27 | file: 'dist/html-to-docx.esm.js', 28 | format: 'es', 29 | sourcemap: true, 30 | banner: `// ${meta.homepage} v${meta.version} Copyright ${new Date().getFullYear()} ${ 31 | meta.author 32 | }`, 33 | }, 34 | { 35 | file: 'dist/html-to-docx.umd.js', 36 | format: 'umd', 37 | name: 'HTMLToDOCX', 38 | sourcemap: true, 39 | banner: `// ${meta.homepage} v${meta.version} Copyright ${new Date().getFullYear()} ${ 40 | meta.author 41 | }`, 42 | }, 43 | ], 44 | }; 45 | -------------------------------------------------------------------------------- /src/docx-document.js: -------------------------------------------------------------------------------- 1 | import { create, fragment } from 'xmlbuilder2'; 2 | import * as shortid from 'shortid'; 3 | 4 | import { 5 | generateCoreXML, 6 | generateStylesXML, 7 | generateNumberingXMLTemplate, 8 | generateThemeXML, 9 | documentRelsXML as documentRelsXMLString, 10 | settingsXML as settingsXMLString, 11 | webSettingsXML as webSettingsXMLString, 12 | contentTypesXML as contentTypesXMLString, 13 | fontTableXML as fontTableXMLString, 14 | genericRelsXML as genericRelsXMLString, 15 | } from './schemas'; 16 | import { convertVTreeToXML, namespaces } from './helpers'; 17 | import generateDocumentTemplate from '../template/document.template'; 18 | 19 | const landscapeMargins = { 20 | top: 1800, 21 | right: 1440, 22 | bottom: 1800, 23 | left: 1440, 24 | header: 720, 25 | footer: 720, 26 | gutter: 0, 27 | }; 28 | 29 | const portraitMargins = { 30 | top: 1440, 31 | right: 1800, 32 | bottom: 1440, 33 | left: 1800, 34 | header: 720, 35 | footer: 720, 36 | gutter: 0, 37 | }; 38 | 39 | class DocxDocument { 40 | constructor({ 41 | zip, 42 | htmlString, 43 | orientation, 44 | margins, 45 | title, 46 | subject, 47 | creator, 48 | keywords, 49 | description, 50 | lastModifiedBy, 51 | revision, 52 | createdAt, 53 | modifiedAt, 54 | headerType, 55 | header, 56 | footerType, 57 | footer, 58 | font, 59 | fontSize, 60 | complexScriptFontSize, 61 | table, 62 | pageNumber, 63 | skipFirstHeaderFooter, 64 | lineNumber, 65 | lineNumberOptions, 66 | }) { 67 | this.zip = zip; 68 | this.htmlString = htmlString; 69 | this.orientation = orientation; 70 | this.width = orientation === 'landscape' ? 15840 : 12240; 71 | this.height = orientation === 'landscape' ? 12240 : 15840; 72 | this.margins = 73 | // eslint-disable-next-line no-nested-ternary 74 | margins && Object.keys(margins).length 75 | ? margins 76 | : orientation === 'landscape' 77 | ? landscapeMargins 78 | : portraitMargins; 79 | this.availableDocumentSpace = this.width - this.margins.left - this.margins.right; 80 | this.title = title || ''; 81 | this.subject = subject || ''; 82 | this.creator = creator || 'html-to-docx'; 83 | this.keywords = keywords || ['html-to-docx']; 84 | this.description = description || ''; 85 | this.lastModifiedBy = lastModifiedBy || 'html-to-docx'; 86 | this.revision = revision || 1; 87 | this.createdAt = createdAt || new Date(); 88 | this.modifiedAt = modifiedAt || new Date(); 89 | this.headerType = headerType || 'default'; 90 | this.header = header || false; 91 | this.footerType = footerType || 'default'; 92 | this.footer = footer || false; 93 | this.font = font || 'Times New Roman'; 94 | this.fontSize = fontSize || 22; 95 | this.complexScriptFontSize = complexScriptFontSize || 22; 96 | this.tableRowCantSplit = (table && table.row && table.row.cantSplit) || false; 97 | this.pageNumber = pageNumber || false; 98 | this.skipFirstHeaderFooter = skipFirstHeaderFooter || false; 99 | this.lineNumber = lineNumber ? lineNumberOptions : null; 100 | 101 | this.lastNumberingId = 0; 102 | this.lastMediaId = 0; 103 | this.lastHeaderId = 0; 104 | this.lastFooterId = 0; 105 | this.stylesObjects = []; 106 | this.numberingObjects = []; 107 | this.relationshipFilename = 'document'; 108 | this.relationships = [{ fileName: 'document', lastRelsId: 4, rels: [] }]; 109 | this.mediaFiles = []; 110 | this.headerObjects = []; 111 | this.footerObjects = []; 112 | this.documentXML = null; 113 | 114 | this.generateContentTypesXML = this.generateContentTypesXML.bind(this); 115 | this.generateCoreXML = this.generateCoreXML.bind(this); 116 | this.generateDocumentXML = this.generateDocumentXML.bind(this); 117 | this.generateSettingsXML = this.generateSettingsXML.bind(this); 118 | this.generateWebSettingsXML = this.generateWebSettingsXML.bind(this); 119 | this.generateStylesXML = this.generateStylesXML.bind(this); 120 | this.generateFontTableXML = this.generateFontTableXML.bind(this); 121 | this.generateThemeXML = this.generateThemeXML.bind(this); 122 | this.generateNumberingXML = this.generateNumberingXML.bind(this); 123 | this.generateRelsXML = this.generateRelsXML.bind(this); 124 | this.createMediaFile = this.createMediaFile.bind(this); 125 | this.createDocumentRelationships = this.createDocumentRelationships.bind(this); 126 | this.generateHeaderXML = this.generateHeaderXML.bind(this); 127 | this.generateFooterXML = this.generateFooterXML.bind(this); 128 | } 129 | 130 | generateContentTypesXML() { 131 | const contentTypesXML = create({ encoding: 'UTF-8', standalone: true }, contentTypesXMLString); 132 | 133 | if (this.headerObjects && Array.isArray(this.headerObjects) && this.headerObjects.length) { 134 | this.headerObjects.forEach( 135 | // eslint-disable-next-line array-callback-return 136 | ({ headerId }) => { 137 | const contentTypesFragment = fragment({ 138 | defaultNamespace: { 139 | ele: 'http://schemas.openxmlformats.org/package/2006/content-types', 140 | }, 141 | }) 142 | .ele('Override') 143 | .att('PartName', `/word/header${headerId}.xml`) 144 | .att( 145 | 'ContentType', 146 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml' 147 | ) 148 | .up(); 149 | contentTypesXML.root().import(contentTypesFragment); 150 | } 151 | ); 152 | } 153 | if (this.footerObjects && Array.isArray(this.footerObjects) && this.footerObjects.length) { 154 | this.footerObjects.forEach( 155 | // eslint-disable-next-line array-callback-return 156 | ({ footerId }) => { 157 | const contentTypesFragment = fragment({ 158 | defaultNamespace: { 159 | ele: 'http://schemas.openxmlformats.org/package/2006/content-types', 160 | }, 161 | }) 162 | .ele('Override') 163 | .att('PartName', `/word/footer${footerId}.xml`) 164 | .att( 165 | 'ContentType', 166 | 'application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml' 167 | ) 168 | .up(); 169 | contentTypesXML.root().import(contentTypesFragment); 170 | } 171 | ); 172 | } 173 | 174 | return contentTypesXML.toString({ prettyPrint: true }); 175 | } 176 | 177 | generateCoreXML() { 178 | const coreXML = create( 179 | { encoding: 'UTF-8', standalone: true }, 180 | generateCoreXML( 181 | this.title, 182 | this.subject, 183 | this.creator, 184 | this.keywords, 185 | this.description, 186 | this.lastModifiedBy, 187 | this.revision, 188 | this.createdAt, 189 | this.modifiedAt 190 | ) 191 | ); 192 | 193 | return coreXML.toString({ prettyPrint: true }); 194 | } 195 | 196 | generateDocumentXML() { 197 | const documentXML = create( 198 | { encoding: 'UTF-8', standalone: true }, 199 | generateDocumentTemplate(this.width, this.height, this.orientation, this.margins) 200 | ); 201 | documentXML.root().first().import(this.documentXML); 202 | 203 | if ( 204 | this.header && 205 | this.headerObjects && 206 | Array.isArray(this.headerObjects) && 207 | this.headerObjects.length 208 | ) { 209 | const headerXmlFragment = fragment(); 210 | 211 | this.headerObjects.forEach( 212 | // eslint-disable-next-line array-callback-return 213 | ({ relationshipId, type }) => { 214 | const headerFragment = fragment({ 215 | namespaceAlias: { 216 | w: namespaces.w, 217 | r: namespaces.r, 218 | }, 219 | }) 220 | .ele('@w', 'headerReference') 221 | .att('@r', 'id', `rId${relationshipId}`) 222 | .att('@w', 'type', type) 223 | .up(); 224 | headerXmlFragment.import(headerFragment); 225 | } 226 | ); 227 | 228 | documentXML.root().first().first().import(headerXmlFragment); 229 | } 230 | if ( 231 | this.footer && 232 | this.footerObjects && 233 | Array.isArray(this.footerObjects) && 234 | this.footerObjects.length 235 | ) { 236 | const footerXmlFragment = fragment(); 237 | 238 | this.footerObjects.forEach( 239 | // eslint-disable-next-line array-callback-return 240 | ({ relationshipId, type }) => { 241 | const footerFragment = fragment({ 242 | namespaceAlias: { 243 | w: namespaces.w, 244 | r: namespaces.r, 245 | }, 246 | }) 247 | .ele('@w', 'footerReference') 248 | .att('@r', 'id', `rId${relationshipId}`) 249 | .att('@w', 'type', type) 250 | .up(); 251 | footerXmlFragment.import(footerFragment); 252 | } 253 | ); 254 | 255 | documentXML.root().first().first().import(footerXmlFragment); 256 | } 257 | if ((this.header || this.footer) && this.skipFirstHeaderFooter) { 258 | const titlePageFragment = fragment({ 259 | namespaceAlias: { 260 | w: namespaces.w, 261 | }, 262 | }).ele('@w', 'titlePg'); 263 | 264 | documentXML.root().first().first().import(titlePageFragment); 265 | } 266 | if (this.lineNumber) { 267 | const { countBy, start, restart } = this.lineNumber; 268 | const lineNumberFragment = fragment({ 269 | namespaceAlias: { 270 | w: namespaces.w, 271 | }, 272 | }) 273 | .ele('@w', 'lnNumType') 274 | .att('@w', 'countBy', countBy) 275 | .att('@w', 'start', start) 276 | .att('@w', 'restart', restart); 277 | documentXML.root().first().first().import(lineNumberFragment); 278 | } 279 | 280 | return documentXML.toString({ prettyPrint: true }); 281 | } 282 | 283 | // eslint-disable-next-line class-methods-use-this 284 | generateSettingsXML() { 285 | const settingsXML = create({ encoding: 'UTF-8', standalone: true }, settingsXMLString); 286 | 287 | return settingsXML.toString({ prettyPrint: true }); 288 | } 289 | 290 | // eslint-disable-next-line class-methods-use-this 291 | generateWebSettingsXML() { 292 | const webSettingsXML = create({ encoding: 'UTF-8', standalone: true }, webSettingsXMLString); 293 | 294 | return webSettingsXML.toString({ prettyPrint: true }); 295 | } 296 | 297 | // eslint-disable-next-line class-methods-use-this 298 | generateStylesXML() { 299 | const stylesXML = create( 300 | { encoding: 'UTF-8', standalone: true }, 301 | generateStylesXML(this.font, this.fontSize, this.complexScriptFontSize) 302 | ); 303 | 304 | return stylesXML.toString({ prettyPrint: true }); 305 | } 306 | 307 | // eslint-disable-next-line class-methods-use-this 308 | generateFontTableXML() { 309 | const fontTableXML = create({ encoding: 'UTF-8', standalone: true }, fontTableXMLString); 310 | 311 | return fontTableXML.toString({ prettyPrint: true }); 312 | } 313 | 314 | // eslint-disable-next-line class-methods-use-this 315 | generateThemeXML() { 316 | const themeXml = create({ encoding: 'UTF-8', standalone: true }, generateThemeXML(this.font)); 317 | 318 | return themeXml.toString({ prettyPrint: true }); 319 | } 320 | 321 | generateNumberingXML() { 322 | const numberingXML = create( 323 | { encoding: 'UTF-8', standalone: true }, 324 | generateNumberingXMLTemplate() 325 | ); 326 | 327 | const abstractNumberingFragments = fragment(); 328 | const numberingFragments = fragment(); 329 | 330 | this.numberingObjects.forEach( 331 | // eslint-disable-next-line array-callback-return 332 | ({ numberingId, listElements }) => { 333 | const abstractNumberingFragment = fragment({ 334 | namespaceAlias: { w: namespaces.w }, 335 | }) 336 | .ele('@w', 'abstractNum') 337 | .att('@w', 'abstractNumId', String(numberingId)) 338 | .ele('@w', 'multiLevelType') 339 | .att('@w', 'val', 'hybridMultilevel') 340 | .up(); 341 | 342 | listElements 343 | .filter((value, index, self) => { 344 | return self.findIndex((v) => v.level === value.level) === index; 345 | }) 346 | .forEach(({ level, type }) => { 347 | // our scheme is to cycle through 1 -> a -> i. so we can 348 | // deduce marker from level 349 | const levelOffset = level % 3; 350 | let markerType = 'decimal'; 351 | if (levelOffset === 1) { 352 | markerType = 'lowerLetter'; 353 | } else if (levelOffset === 2) { 354 | markerType = 'lowerRoman' 355 | } 356 | const levelFragment = fragment({ 357 | namespaceAlias: { w: namespaces.w }, 358 | }) 359 | .ele('@w', 'lvl') 360 | .att('@w', 'ilvl', level) 361 | .ele('@w', 'start') 362 | .att('@w', 'val', '1') 363 | .up() 364 | .ele('@w', 'numFmt') 365 | .att('@w', 'val', type === 'ol' ? markerType : 'bullet') // docx can only handle bullets for ul 366 | .up() 367 | .ele('@w', 'lvlText') 368 | .att('@w', 'val', type === 'ol' ? `%${level + 1}` : '') 369 | .up() 370 | .ele('@w', 'lvlJc') 371 | .att('@w', 'val', 'left') 372 | .up() 373 | .ele('@w', 'pPr') 374 | .ele('@w', 'tabs') 375 | .ele('@w', 'tab') 376 | .att('@w', 'val', 'num') 377 | .att('@w', 'pos', (level + 1) * 720) 378 | .up() 379 | .up() 380 | .ele('@w', 'ind') 381 | .att('@w', 'left', (level + 1) * 720) 382 | .att('@w', 'hanging', 360) 383 | .up() 384 | .up() 385 | .up(); 386 | 387 | if (type === 'ul') { 388 | const runPropertiesFragment = fragment({ 389 | namespaceAlias: { w: namespaces.w }, 390 | }) 391 | .ele('@w', 'rPr') 392 | .ele('@w', 'rFonts') 393 | .att('@w', 'ascii', 'Wingdings') 394 | .att('@w', 'hAnsi', 'Wingdings') 395 | .att('@w', 'hint', 'default') 396 | .up() 397 | .up(); 398 | 399 | levelFragment.last().import(runPropertiesFragment); 400 | } 401 | 402 | abstractNumberingFragment.import(levelFragment); 403 | }); 404 | 405 | abstractNumberingFragment.up(); 406 | 407 | const numberingFragment = fragment({ 408 | namespaceAlias: { w: namespaces.w }, 409 | }) 410 | .ele('@w', 'num') 411 | .att('@w', 'numId', String(numberingId)) 412 | .ele('@w', 'abstractNumId') 413 | .att('@w', 'val', String(numberingId)) 414 | .up() 415 | .up(); 416 | 417 | abstractNumberingFragments.import(abstractNumberingFragment); 418 | numberingFragments.import(numberingFragment); 419 | } 420 | ); 421 | 422 | numberingXML.root().import(abstractNumberingFragments); 423 | numberingXML.root().import(numberingFragments); 424 | 425 | return numberingXML.toString({ prettyPrint: true }); 426 | } 427 | 428 | // eslint-disable-next-line class-methods-use-this 429 | appendRelationships(xmlFragment, relationships) { 430 | relationships.forEach( 431 | // eslint-disable-next-line array-callback-return 432 | ({ relationshipId, type, target, targetMode }) => { 433 | const relationshipFragment = fragment({ 434 | defaultNamespace: { ele: 'http://schemas.openxmlformats.org/package/2006/relationships' }, 435 | }) 436 | .ele('Relationship') 437 | .att('Id', `rId${relationshipId}`) 438 | .att('Type', type) 439 | .att('Target', target) 440 | .att('TargetMode', targetMode) 441 | .up(); 442 | 443 | xmlFragment.import(relationshipFragment); 444 | } 445 | ); 446 | } 447 | 448 | generateRelsXML() { 449 | const relationshipXMLStrings = this.relationships.map(({ fileName, rels }) => { 450 | let xmlFragment; 451 | if (fileName === 'document') { 452 | xmlFragment = create({ encoding: 'UTF-8', standalone: true }, documentRelsXMLString); 453 | } else { 454 | xmlFragment = create({ encoding: 'UTF-8', standalone: true }, genericRelsXMLString); 455 | } 456 | this.appendRelationships(xmlFragment.root(), rels); 457 | 458 | return { 459 | fileName, 460 | xmlString: xmlFragment.toString({ prettyPrint: true }), 461 | }; 462 | }); 463 | 464 | return relationshipXMLStrings; 465 | } 466 | 467 | createNumbering(listElements) { 468 | this.lastNumberingId += 1; 469 | this.numberingObjects.push({ numberingId: this.lastNumberingId, listElements }); 470 | 471 | return this.lastNumberingId; 472 | } 473 | 474 | createMediaFile(base64String) { 475 | // eslint-disable-next-line no-useless-escape 476 | const matches = base64String.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/); 477 | if (matches.length !== 3) { 478 | throw new Error('Invalid base64 string'); 479 | } 480 | 481 | const base64FileContent = matches[2]; 482 | // matches array contains file type in base64 format - image/jpeg and base64 stringified data 483 | const fileExtension = 484 | matches[1].match(/\/(.*?)$/)[1] === 'octet-stream' ? 'png' : matches[1].match(/\/(.*?)$/)[1]; 485 | 486 | const fileNameWithExtension = `image-${shortid.generate()}.${fileExtension}`; 487 | 488 | this.lastMediaId += 1; 489 | 490 | return { id: this.lastMediaId, fileContent: base64FileContent, fileNameWithExtension }; 491 | } 492 | 493 | createDocumentRelationships(fileName = 'document', type, target, targetMode = 'External') { 494 | let relationshipObject = this.relationships.find( 495 | (relationship) => relationship.fileName === fileName 496 | ); 497 | let lastRelsId = 1; 498 | if (relationshipObject) { 499 | lastRelsId = relationshipObject.lastRelsId + 1; 500 | relationshipObject.lastRelsId = lastRelsId; 501 | } else { 502 | relationshipObject = { fileName, lastRelsId, rels: [] }; 503 | this.relationships.push(relationshipObject); 504 | } 505 | let relationshipType; 506 | switch (type) { 507 | case 'hyperlink': 508 | relationshipType = namespaces.hyperlinks; 509 | break; 510 | case 'image': 511 | relationshipType = namespaces.images; 512 | break; 513 | case 'header': 514 | relationshipType = namespaces.headers; 515 | break; 516 | case 'footer': 517 | relationshipType = namespaces.footers; 518 | break; 519 | case 'theme': 520 | relationshipType = namespaces.themes; 521 | break; 522 | default: 523 | break; 524 | } 525 | 526 | relationshipObject.rels.push({ 527 | relationshipId: lastRelsId, 528 | type: relationshipType, 529 | target, 530 | targetMode, 531 | }); 532 | 533 | return lastRelsId; 534 | } 535 | 536 | generateHeaderXML(vTree) { 537 | const headerXML = create({ 538 | encoding: 'UTF-8', 539 | standalone: true, 540 | namespaceAlias: { 541 | w: namespaces.w, 542 | ve: namespaces.ve, 543 | o: namespaces.o, 544 | r: namespaces.r, 545 | v: namespaces.v, 546 | wp: namespaces.wp, 547 | w10: namespaces.w10, 548 | }, 549 | }).ele('@w', 'hdr'); 550 | 551 | const XMLFragment = fragment(); 552 | convertVTreeToXML(this, vTree, XMLFragment); 553 | headerXML.root().import(XMLFragment); 554 | 555 | this.lastHeaderId += 1; 556 | 557 | return { headerId: this.lastHeaderId, headerXML }; 558 | } 559 | 560 | generateFooterXML(vTree) { 561 | const footerXML = create({ 562 | encoding: 'UTF-8', 563 | standalone: true, 564 | namespaceAlias: { 565 | w: namespaces.w, 566 | ve: namespaces.ve, 567 | o: namespaces.o, 568 | r: namespaces.r, 569 | v: namespaces.v, 570 | wp: namespaces.wp, 571 | w10: namespaces.w10, 572 | }, 573 | }).ele('@w', 'ftr'); 574 | 575 | const XMLFragment = fragment(); 576 | convertVTreeToXML(this, vTree, XMLFragment); 577 | if (XMLFragment.first().node.tagName === 'p' && this.pageNumber) { 578 | const fieldSimpleFragment = fragment({ 579 | namespaceAlias: { 580 | w: namespaces.w, 581 | }, 582 | }) 583 | .ele('@w', 'fldSimple') 584 | .att('@w', 'instr', 'PAGE') 585 | .ele('@w', 'r') 586 | .up() 587 | .up(); 588 | XMLFragment.first().import(fieldSimpleFragment); 589 | } 590 | footerXML.root().import(XMLFragment); 591 | 592 | this.lastFooterId += 1; 593 | 594 | return { footerId: this.lastFooterId, footerXML }; 595 | } 596 | } 597 | 598 | export default DocxDocument; 599 | -------------------------------------------------------------------------------- /src/helpers/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/prefer-default-export */ 2 | export { default as renderDocumentFile } from './render-document-file'; 3 | export { convertVTreeToXML } from './render-document-file'; 4 | export { default as namespaces } from './namespaces'; 5 | -------------------------------------------------------------------------------- /src/helpers/namespaces.js: -------------------------------------------------------------------------------- 1 | const namespaces = { 2 | a: 'http://schemas.openxmlformats.org/drawingml/2006/main', 3 | b: 'http://schemas.openxmlformats.org/officeDocument/2006/bibliography', 4 | cdr: 'http://schemas.openxmlformats.org/drawingml/2006/chartDrawing', 5 | dc: 'http://purl.org/dc/elements/1.1/', 6 | dcmitype: 'http://purl.org/dc/dcmitype/', 7 | dcterms: 'http://purl.org/dc/terms/', 8 | o: 'urn:schemas-microsoft-com:office:office', 9 | pic: 'http://schemas.openxmlformats.org/drawingml/2006/picture', 10 | r: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships', 11 | v: 'urn:schemas-microsoft-com:vml', 12 | ve: 'http://schemas.openxmlformats.org/markup-compatibility/2006', 13 | vt: 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes', 14 | w: 'http://schemas.openxmlformats.org/wordprocessingml/2006/main', 15 | w10: 'urn:schemas-microsoft-com:office:word', 16 | wp: 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing', 17 | wne: 'http://schemas.microsoft.com/office/word/2006/wordml', 18 | xsd: 'http://www.w3.org/2001/XMLSchema', 19 | xsi: 'http://www.w3.org/2001/XMLSchema-instance', 20 | numbering: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering', 21 | hyperlinks: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink', 22 | images: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image', 23 | styles: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles', 24 | headers: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/header', 25 | footers: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer', 26 | themes: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme', 27 | coreProperties: 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties', 28 | officeDocumentRelation: 29 | 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument', 30 | corePropertiesRelation: 31 | 'http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties', 32 | settingsRelation: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings', 33 | webSettingsRelation: 34 | 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings', 35 | sl: 'http://schemas.openxmlformats.org/schemaLibrary/2006/main', 36 | }; 37 | 38 | export default namespaces; 39 | -------------------------------------------------------------------------------- /src/helpers/render-document-file.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-case-declarations */ 2 | import { fragment } from 'xmlbuilder2'; 3 | import VNode from 'virtual-dom/vnode/vnode'; 4 | import VText from 'virtual-dom/vnode/vtext'; 5 | import isVNode from 'virtual-dom/vnode/is-vnode'; 6 | import isVText from 'virtual-dom/vnode/is-vtext'; 7 | import * as HTMLToVDOM_ from 'html-to-vdom'; 8 | import escape from 'escape-html'; 9 | import sizeOf from 'image-size'; 10 | 11 | // FIXME: remove the cyclic dependency 12 | // eslint-disable-next-line import/no-cycle 13 | import * as xmlBuilder from './xml-builder'; 14 | import namespaces from './namespaces'; 15 | 16 | const HTMLToVDOM = HTMLToVDOM_; 17 | const convertHTML = HTMLToVDOM({ 18 | VNode, 19 | VText, 20 | }); 21 | 22 | // eslint-disable-next-line consistent-return, no-shadow 23 | export const buildImage = (docxDocumentInstance, vNode, maximumWidth = null) => { 24 | let response = null; 25 | try { 26 | // libtidy encodes the image src 27 | response = docxDocumentInstance.createMediaFile(decodeURIComponent(vNode.properties.src)); 28 | } catch (error) { 29 | // NOOP 30 | } 31 | if (response) { 32 | docxDocumentInstance.zip 33 | .folder('word') 34 | .folder('media') 35 | .file(response.fileNameWithExtension, Buffer.from(response.fileContent, 'base64'), { 36 | createFolders: false, 37 | }); 38 | 39 | const documentRelsId = docxDocumentInstance.createDocumentRelationships( 40 | docxDocumentInstance.relationshipFilename, 41 | 'image', 42 | `media/${response.fileNameWithExtension}`, 43 | 'Internal' 44 | ); 45 | 46 | const imageBuffer = Buffer.from(response.fileContent, 'base64'); 47 | const imageProperties = sizeOf(imageBuffer); 48 | 49 | const imageFragment = xmlBuilder.buildParagraph( 50 | vNode, 51 | { 52 | type: 'picture', 53 | inlineOrAnchored: true, 54 | relationshipId: documentRelsId, 55 | ...response, 56 | maximumWidth: maximumWidth || docxDocumentInstance.availableDocumentSpace, 57 | originalWidth: imageProperties.width, 58 | originalHeight: imageProperties.height, 59 | }, 60 | docxDocumentInstance 61 | ); 62 | 63 | return imageFragment; 64 | } 65 | }; 66 | 67 | export const buildList = (vNode) => { 68 | const listElements = []; 69 | 70 | let vNodeObjects = [{ node: vNode, level: 0, type: vNode.tagName }]; 71 | while (vNodeObjects.length) { 72 | const tempVNodeObject = vNodeObjects.shift(); 73 | if ( 74 | isVText(tempVNodeObject.node) || 75 | (isVNode(tempVNodeObject.node) && !['ul', 'ol', 'li'].includes(tempVNodeObject.node.tagName)) 76 | ) { 77 | listElements.push({ 78 | node: tempVNodeObject.node, 79 | level: tempVNodeObject.level, 80 | type: tempVNodeObject.type, 81 | }); 82 | } 83 | 84 | if ( 85 | tempVNodeObject.node.children && 86 | tempVNodeObject.node.children.length && 87 | ['ul', 'ol', 'li'].includes(tempVNodeObject.node.tagName) 88 | ) { 89 | const tempVNodeObjects = tempVNodeObject.node.children.reduce((accumulator, childVNode) => { 90 | if (['ul', 'ol'].includes(childVNode.tagName)) { 91 | accumulator.push({ 92 | node: childVNode, 93 | level: tempVNodeObject.level + 1, 94 | type: childVNode.tagName, 95 | }); 96 | } else { 97 | // eslint-disable-next-line no-lonely-if 98 | if ( 99 | accumulator.length > 0 && 100 | isVNode(accumulator[accumulator.length - 1].node) && 101 | accumulator[accumulator.length - 1].node.tagName.toLowerCase() === 'p' 102 | ) { 103 | accumulator[accumulator.length - 1].node.children.push(childVNode); 104 | } else { 105 | const paragraphVNode = new VNode( 106 | 'p', 107 | null, 108 | // eslint-disable-next-line no-nested-ternary 109 | isVText(childVNode) 110 | ? [childVNode] 111 | : // eslint-disable-next-line no-nested-ternary 112 | isVNode(childVNode) 113 | ? childVNode.tagName.toLowerCase() === 'li' 114 | ? [...childVNode.children] 115 | : [childVNode] 116 | : [] 117 | ); 118 | accumulator.push({ 119 | // eslint-disable-next-line prettier/prettier, no-nested-ternary 120 | node: isVNode(childVNode) 121 | ? // eslint-disable-next-line prettier/prettier, no-nested-ternary 122 | childVNode.tagName.toLowerCase() === 'li' 123 | ? childVNode 124 | : childVNode.tagName.toLowerCase() !== 'p' 125 | ? paragraphVNode 126 | : childVNode 127 | : // eslint-disable-next-line prettier/prettier 128 | paragraphVNode, 129 | level: tempVNodeObject.level, 130 | type: tempVNodeObject.type, 131 | }); 132 | } 133 | } 134 | 135 | return accumulator; 136 | }, []); 137 | vNodeObjects = tempVNodeObjects.concat(vNodeObjects); 138 | } 139 | } 140 | 141 | return listElements; 142 | }; 143 | 144 | function findXMLEquivalent(docxDocumentInstance, vNode, xmlFragment) { 145 | if ( 146 | vNode.tagName === 'div' && 147 | (vNode.properties.attributes.class === 'page-break' || 148 | (vNode.properties.style && vNode.properties.style['page-break-after'])) 149 | ) { 150 | const paragraphFragment = fragment({ 151 | namespaceAlias: { w: namespaces.w }, 152 | }) 153 | .ele('@w', 'p') 154 | .ele('@w', 'r') 155 | .ele('@w', 'br') 156 | .att('@w', 'type', 'page') 157 | .up() 158 | .up() 159 | .up(); 160 | 161 | xmlFragment.import(paragraphFragment); 162 | return; 163 | } 164 | 165 | switch (vNode.tagName) { 166 | case 'h1': 167 | case 'h2': 168 | case 'h3': 169 | case 'h4': 170 | case 'h5': 171 | case 'h6': 172 | const headingFragment = xmlBuilder.buildParagraph( 173 | vNode, 174 | { 175 | paragraphStyle: `Heading${vNode.tagName[1]}`, 176 | }, 177 | docxDocumentInstance 178 | ); 179 | xmlFragment.import(headingFragment); 180 | return; 181 | case 'span': 182 | case 'strong': 183 | case 'b': 184 | case 'em': 185 | case 'i': 186 | case 'u': 187 | case 'ins': 188 | case 'strike': 189 | case 'del': 190 | case 's': 191 | case 'sub': 192 | case 'sup': 193 | case 'mark': 194 | case 'p': 195 | case 'a': 196 | case 'blockquote': 197 | case 'code': 198 | case 'pre': 199 | const paragraphFragment = xmlBuilder.buildParagraph(vNode, {}, docxDocumentInstance); 200 | xmlFragment.import(paragraphFragment); 201 | return; 202 | case 'figure': 203 | if (vNode.children && Array.isArray(vNode.children) && vNode.children.length) { 204 | // eslint-disable-next-line no-plusplus 205 | for (let index = 0; index < vNode.children.length; index++) { 206 | const childVNode = vNode.children[index]; 207 | if (childVNode.tagName === 'table') { 208 | const tableFragment = xmlBuilder.buildTable( 209 | childVNode, 210 | { 211 | maximumWidth: docxDocumentInstance.availableDocumentSpace, 212 | rowCantSplit: docxDocumentInstance.tableRowCantSplit, 213 | }, 214 | docxDocumentInstance 215 | ); 216 | xmlFragment.import(tableFragment); 217 | // Adding empty paragraph for space after table 218 | const emptyParagraphFragment = xmlBuilder.buildParagraph(null, {}); 219 | xmlFragment.import(emptyParagraphFragment); 220 | } else if (childVNode.tagName === 'img') { 221 | const imageFragment = buildImage(docxDocumentInstance, childVNode); 222 | if (imageFragment) { 223 | xmlFragment.import(imageFragment); 224 | } 225 | } 226 | } 227 | } 228 | return; 229 | case 'table': 230 | const tableFragment = xmlBuilder.buildTable( 231 | vNode, 232 | { 233 | maximumWidth: docxDocumentInstance.availableDocumentSpace, 234 | rowCantSplit: docxDocumentInstance.tableRowCantSplit, 235 | }, 236 | docxDocumentInstance 237 | ); 238 | xmlFragment.import(tableFragment); 239 | // Adding empty paragraph for space after table 240 | const emptyParagraphFragment = xmlBuilder.buildParagraph(null, {}); 241 | xmlFragment.import(emptyParagraphFragment); 242 | return; 243 | case 'ol': 244 | case 'ul': 245 | const listElements = buildList(vNode); 246 | const numberingId = docxDocumentInstance.createNumbering(listElements); 247 | // eslint-disable-next-line no-plusplus 248 | for (let index = 0; index < listElements.length; index++) { 249 | const listElement = listElements[index]; 250 | // eslint-disable-next-line no-shadow 251 | const paragraphFragment = xmlBuilder.buildParagraph( 252 | listElement.node, 253 | { 254 | numbering: { levelId: listElement.level, numberingId }, 255 | }, 256 | docxDocumentInstance 257 | ); 258 | xmlFragment.import(paragraphFragment); 259 | } 260 | return; 261 | case 'img': 262 | const imageFragment = buildImage(docxDocumentInstance, vNode); 263 | if (imageFragment) { 264 | xmlFragment.import(imageFragment); 265 | } 266 | return; 267 | case 'br': 268 | const linebreakFragment = xmlBuilder.buildParagraph(null, {}); 269 | xmlFragment.import(linebreakFragment); 270 | return; 271 | default: 272 | break; 273 | } 274 | if (vNode.children && Array.isArray(vNode.children) && vNode.children.length) { 275 | // eslint-disable-next-line no-plusplus 276 | for (let index = 0; index < vNode.children.length; index++) { 277 | const childVNode = vNode.children[index]; 278 | // eslint-disable-next-line no-use-before-define 279 | convertVTreeToXML(docxDocumentInstance, childVNode, xmlFragment); 280 | } 281 | } 282 | } 283 | 284 | // eslint-disable-next-line consistent-return 285 | export function convertVTreeToXML(docxDocumentInstance, vTree, xmlFragment) { 286 | if (!vTree) { 287 | // eslint-disable-next-line no-useless-return 288 | return ''; 289 | } 290 | if (Array.isArray(vTree) && vTree.length) { 291 | // eslint-disable-next-line no-plusplus 292 | for (let index = 0; index < vTree.length; index++) { 293 | const vNode = vTree[index]; 294 | convertVTreeToXML(docxDocumentInstance, vNode, xmlFragment); 295 | } 296 | } else if (isVNode(vTree)) { 297 | findXMLEquivalent(docxDocumentInstance, vTree, xmlFragment); 298 | } else if (isVText(vTree)) { 299 | xmlBuilder.buildTextElement(xmlFragment, escape(String(vTree.text))); 300 | } 301 | return xmlFragment; 302 | } 303 | 304 | function renderDocumentFile(docxDocumentInstance) { 305 | const vTree = convertHTML(docxDocumentInstance.htmlString); 306 | 307 | const xmlFragment = fragment({ 308 | namespaceAlias: { w: namespaces.w }, 309 | }); 310 | 311 | const populatedXmlFragment = convertVTreeToXML(docxDocumentInstance, vTree, xmlFragment); 312 | 313 | return populatedXmlFragment; 314 | } 315 | 316 | export default renderDocumentFile; 317 | -------------------------------------------------------------------------------- /src/helpers/xml-builder.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable radix */ 2 | /* eslint-disable no-param-reassign */ 3 | /* eslint-disable no-case-declarations */ 4 | /* eslint-disable no-plusplus */ 5 | /* eslint-disable no-else-return */ 6 | import { fragment } from 'xmlbuilder2'; 7 | import { VText } from 'virtual-dom'; 8 | import isVNode from 'virtual-dom/vnode/is-vnode'; 9 | import isVText from 'virtual-dom/vnode/is-vtext'; 10 | import colorNames from 'color-name'; 11 | 12 | // eslint-disable-next-line import/no-named-default 13 | import { default as namespaces } from './namespaces'; 14 | import { 15 | rgbToHex, 16 | hslToHex, 17 | hslRegex, 18 | rgbRegex, 19 | hexRegex, 20 | hex3Regex, 21 | hex3ToHex, 22 | } from '../utils/color-conversion'; 23 | import { 24 | pixelToEMU, 25 | pixelRegex, 26 | TWIPToEMU, 27 | percentageRegex, 28 | pointRegex, 29 | pointToHIP, 30 | HIPToTWIP, 31 | pointToTWIP, 32 | pixelToHIP, 33 | pixelToTWIP, 34 | pixelToEIP, 35 | pointToEIP, 36 | } from '../utils/unit-conversion'; 37 | // FIXME: remove the cyclic dependency 38 | // eslint-disable-next-line import/no-cycle 39 | import { buildImage } from './render-document-file'; 40 | 41 | // eslint-disable-next-line consistent-return 42 | const fixupColorCode = (colorCodeString) => { 43 | if (Object.prototype.hasOwnProperty.call(colorNames, colorCodeString.toLowerCase())) { 44 | const [red, green, blue] = colorNames[colorCodeString.toLowerCase()]; 45 | 46 | return rgbToHex(red, green, blue); 47 | } else if (rgbRegex.test(colorCodeString)) { 48 | const matchedParts = colorCodeString.match(rgbRegex); 49 | const red = matchedParts[1]; 50 | const green = matchedParts[2]; 51 | const blue = matchedParts[3]; 52 | 53 | return rgbToHex(red, green, blue); 54 | } else if (hslRegex.test(colorCodeString)) { 55 | const matchedParts = colorCodeString.match(hslRegex); 56 | const hue = matchedParts[1]; 57 | const saturation = matchedParts[2]; 58 | const luminosity = matchedParts[3]; 59 | 60 | return hslToHex(hue, saturation, luminosity); 61 | } else if (hexRegex.test(colorCodeString)) { 62 | const matchedParts = colorCodeString.match(hexRegex); 63 | 64 | return matchedParts[1]; 65 | } else if (hex3Regex.test(colorCodeString)) { 66 | const matchedParts = colorCodeString.match(hex3Regex); 67 | const red = matchedParts[1]; 68 | const green = matchedParts[2]; 69 | const blue = matchedParts[3]; 70 | 71 | return hex3ToHex(red, green, blue); 72 | } else { 73 | return '000000'; 74 | } 75 | }; 76 | 77 | const buildRunFontFragment = (fontName = 'Times New Roman') => { 78 | const runFontFragment = fragment({ 79 | namespaceAlias: { w: namespaces.w }, 80 | }) 81 | .ele('@w', 'rFonts') 82 | .att('@w', 'ascii', fontName) 83 | .att('@w', 'hAnsi', fontName) 84 | .up(); 85 | 86 | return runFontFragment; 87 | }; 88 | 89 | const buildRunStyleFragment = (type = 'Hyperlink') => { 90 | const runStyleFragment = fragment({ 91 | namespaceAlias: { w: namespaces.w }, 92 | }) 93 | .ele('@w', 'rStyle') 94 | .att('@w', 'val', type) 95 | .up(); 96 | 97 | return runStyleFragment; 98 | }; 99 | 100 | const buildTableRowHeight = (tableRowHeight) => { 101 | const tableRowHeightFragment = fragment({ 102 | namespaceAlias: { w: namespaces.w }, 103 | }) 104 | .ele('@w', 'trHeight') 105 | .att('@w', 'val', tableRowHeight) 106 | .att('@w', 'hRule', 'atLeast') 107 | .up(); 108 | 109 | return tableRowHeightFragment; 110 | }; 111 | 112 | const buildVerticalAlignment = (verticalAlignment) => { 113 | if (verticalAlignment.toLowerCase() === 'middle') { 114 | verticalAlignment = 'center'; 115 | } 116 | 117 | const verticalAlignmentFragment = fragment({ 118 | namespaceAlias: { w: namespaces.w }, 119 | }) 120 | .ele('@w', 'vAlign') 121 | .att('@w', 'val', verticalAlignment) 122 | .up(); 123 | 124 | return verticalAlignmentFragment; 125 | }; 126 | 127 | const buildVerticalMerge = (verticalMerge = 'continue') => { 128 | const verticalMergeFragment = fragment({ 129 | namespaceAlias: { w: namespaces.w }, 130 | }) 131 | .ele('@w', 'vMerge') 132 | .att('@w', 'val', verticalMerge) 133 | .up(); 134 | 135 | return verticalMergeFragment; 136 | }; 137 | 138 | const buildColor = (colorCode) => { 139 | const colorFragment = fragment({ 140 | namespaceAlias: { w: namespaces.w }, 141 | }) 142 | .ele('@w', 'color') 143 | .att('@w', 'val', colorCode) 144 | .up(); 145 | 146 | return colorFragment; 147 | }; 148 | 149 | const buildFontSize = (fontSize) => { 150 | const fontSizeFragment = fragment({ 151 | namespaceAlias: { w: namespaces.w }, 152 | }) 153 | .ele('@w', 'sz') 154 | .att('@w', 'val', fontSize) 155 | .up(); 156 | 157 | return fontSizeFragment; 158 | }; 159 | 160 | const buildShading = (colorCode) => { 161 | const shadingFragment = fragment({ 162 | namespaceAlias: { w: namespaces.w }, 163 | }) 164 | .ele('@w', 'shd') 165 | .att('@w', 'val', 'clear') 166 | .att('@w', 'fill', colorCode) 167 | .up(); 168 | 169 | return shadingFragment; 170 | }; 171 | 172 | const buildHighlight = (color = 'yellow') => { 173 | const highlightFragment = fragment({ 174 | namespaceAlias: { w: namespaces.w }, 175 | }) 176 | .ele('@w', 'highlight') 177 | .att('@w', 'val', color) 178 | .up(); 179 | 180 | return highlightFragment; 181 | }; 182 | 183 | const buildVertAlign = (type = 'baseline') => { 184 | const vertAlignFragment = fragment({ 185 | namespaceAlias: { w: namespaces.w }, 186 | }) 187 | .ele('@w', 'vertAlign') 188 | .att('@w', 'val', type) 189 | .up(); 190 | 191 | return vertAlignFragment; 192 | }; 193 | 194 | const buildStrike = () => { 195 | const strikeFragment = fragment({ 196 | namespaceAlias: { w: namespaces.w }, 197 | }) 198 | .ele('@w', 'strike') 199 | .att('@w', 'val', true) 200 | .up(); 201 | 202 | return strikeFragment; 203 | }; 204 | 205 | const buildBold = () => { 206 | const boldFragment = fragment({ 207 | namespaceAlias: { w: namespaces.w }, 208 | }) 209 | .ele('@w', 'b') 210 | .up(); 211 | 212 | return boldFragment; 213 | }; 214 | 215 | const buildItalics = () => { 216 | const italicsFragment = fragment({ 217 | namespaceAlias: { w: namespaces.w }, 218 | }) 219 | .ele('@w', 'i') 220 | .up(); 221 | 222 | return italicsFragment; 223 | }; 224 | 225 | const buildUnderline = (type = 'single') => { 226 | const underlineFragment = fragment({ 227 | namespaceAlias: { w: namespaces.w }, 228 | }) 229 | .ele('@w', 'u') 230 | .att('@w', 'val', type) 231 | .up(); 232 | 233 | return underlineFragment; 234 | }; 235 | 236 | const buildLineBreak = (type = 'textWrapping') => { 237 | const lineBreakFragment = fragment({ 238 | namespaceAlias: { w: namespaces.w }, 239 | }) 240 | .ele('@w', 'br') 241 | .att('@w', 'type', type) 242 | .up(); 243 | 244 | return lineBreakFragment; 245 | }; 246 | 247 | const buildBorder = ( 248 | borderSide = 'top', 249 | borderSize = 0, 250 | borderSpacing = 0, 251 | borderColor = fixupColorCode('black'), 252 | borderStroke = 'single' 253 | ) => { 254 | const borderFragment = fragment({ 255 | namespaceAlias: { w: namespaces.w }, 256 | }) 257 | .ele('@w', borderSide) 258 | .att('@w', 'val', borderStroke) 259 | .att('@w', 'sz', borderSize) 260 | .att('@w', 'space', borderSpacing) 261 | .att('@w', 'color', borderColor) 262 | .up(); 263 | 264 | return borderFragment; 265 | }; 266 | 267 | const buildTextElement = (text) => { 268 | const textFragment = fragment({ 269 | namespaceAlias: { w: namespaces.w }, 270 | }) 271 | .ele('@w', 't') 272 | .att('@xml', 'space', 'preserve') 273 | .txt(text) 274 | .up(); 275 | 276 | return textFragment; 277 | }; 278 | 279 | const buildRunProperties = (attributes) => { 280 | const runPropertiesFragment = fragment({ 281 | namespaceAlias: { w: namespaces.w }, 282 | }).ele('@w', 'rPr'); 283 | if (attributes && attributes.constructor === Object) { 284 | Object.keys(attributes).forEach((key) => { 285 | // eslint-disable-next-line default-case 286 | switch (key) { 287 | case 'strong': 288 | const boldFragment = buildBold(); 289 | runPropertiesFragment.import(boldFragment); 290 | break; 291 | case 'i': 292 | const italicsFragment = buildItalics(); 293 | runPropertiesFragment.import(italicsFragment); 294 | break; 295 | case 'u': 296 | const underlineFragment = buildUnderline(); 297 | runPropertiesFragment.import(underlineFragment); 298 | break; 299 | case 'color': 300 | const colorFragment = buildColor(attributes[key]); 301 | runPropertiesFragment.import(colorFragment); 302 | break; 303 | case 'backgroundColor': 304 | const shadingFragment = buildShading(attributes[key]); 305 | runPropertiesFragment.import(shadingFragment); 306 | break; 307 | case 'fontSize': 308 | const fontSizeFragment = buildFontSize(attributes[key]); 309 | runPropertiesFragment.import(fontSizeFragment); 310 | break; 311 | case 'hyperlink': 312 | const hyperlinkStyleFragment = buildRunStyleFragment('Hyperlink'); 313 | runPropertiesFragment.import(hyperlinkStyleFragment); 314 | break; 315 | case 'highlightColor': 316 | const highlightFragment = buildHighlight(attributes[key]); 317 | runPropertiesFragment.import(highlightFragment); 318 | break; 319 | case 'font': 320 | const runFontFragment = buildRunFontFragment('Courier'); 321 | runPropertiesFragment.import(runFontFragment); 322 | break; 323 | } 324 | }); 325 | } 326 | runPropertiesFragment.up(); 327 | 328 | return runPropertiesFragment; 329 | }; 330 | 331 | // eslint-disable-next-line consistent-return 332 | const buildTextFormatting = (vNode) => { 333 | // eslint-disable-next-line default-case 334 | switch (vNode.tagName) { 335 | case 'strong': 336 | case 'b': { 337 | const boldFragment = buildBold(); 338 | return boldFragment; 339 | } 340 | case 'em': 341 | case 'i': { 342 | const italicsFragment = buildItalics(); 343 | return italicsFragment; 344 | } 345 | case 'ins': 346 | case 'u': { 347 | const underlineFragment = buildUnderline(); 348 | return underlineFragment; 349 | } 350 | case 'strike': 351 | case 'del': 352 | case 's': { 353 | const strikeFragment = buildStrike(); 354 | return strikeFragment; 355 | } 356 | case 'sub': { 357 | const subscriptFragment = buildVertAlign('subscript'); 358 | return subscriptFragment; 359 | } 360 | case 'sup': { 361 | const superscriptFragment = buildVertAlign('superscript'); 362 | return superscriptFragment; 363 | } 364 | case 'mark': { 365 | const highlightFragment = buildHighlight(); 366 | return highlightFragment; 367 | } 368 | case 'code': { 369 | const highlightFragment = buildHighlight('lightGray'); 370 | return highlightFragment; 371 | } 372 | case 'pre': { 373 | const runFontFragment = buildRunFontFragment('Courier'); 374 | return runFontFragment; 375 | } 376 | } 377 | }; 378 | 379 | const buildRun = (vNode, attributes) => { 380 | const runFragment = fragment({ 381 | namespaceAlias: { w: namespaces.w }, 382 | }).ele('@w', 'r'); 383 | const runPropertiesFragment = buildRunProperties(attributes); 384 | 385 | if ( 386 | isVNode(vNode) && 387 | [ 388 | 'span', 389 | 'strong', 390 | 'b', 391 | 'em', 392 | 'i', 393 | 'u', 394 | 'ins', 395 | 'strike', 396 | 'del', 397 | 's', 398 | 'sub', 399 | 'sup', 400 | 'mark', 401 | 'blockquote', 402 | 'code', 403 | 'pre', 404 | ].includes(vNode.tagName) 405 | ) { 406 | const textArray = []; 407 | 408 | let vNodes = [vNode]; 409 | while (vNodes.length) { 410 | const tempVNode = vNodes.shift(); 411 | if (isVText(tempVNode)) { 412 | textArray.push(tempVNode.text); 413 | } 414 | if ( 415 | isVNode(tempVNode) && 416 | [ 417 | 'strong', 418 | 'b', 419 | 'em', 420 | 'i', 421 | 'u', 422 | 'ins', 423 | 'strike', 424 | 'del', 425 | 's', 426 | 'sub', 427 | 'sup', 428 | 'mark', 429 | 'code', 430 | 'pre', 431 | ].includes(tempVNode.tagName) 432 | ) { 433 | const formattingFragment = buildTextFormatting(tempVNode); 434 | runPropertiesFragment.import(formattingFragment); 435 | } 436 | 437 | if (tempVNode.children && tempVNode.children.length) { 438 | vNodes = tempVNode.children.slice().concat(vNodes); 439 | } 440 | } 441 | if (textArray.length) { 442 | const combinedString = textArray.join(''); 443 | // eslint-disable-next-line no-param-reassign 444 | vNode = new VText(combinedString); 445 | } 446 | } 447 | 448 | runFragment.import(runPropertiesFragment); 449 | if (isVText(vNode)) { 450 | const textFragment = buildTextElement(vNode.text); 451 | runFragment.import(textFragment); 452 | } else if (attributes && attributes.type === 'picture') { 453 | const { type, inlineOrAnchored, ...otherAttributes } = attributes; 454 | // eslint-disable-next-line no-use-before-define 455 | const imageFragment = buildDrawing(inlineOrAnchored, type, otherAttributes); 456 | runFragment.import(imageFragment); 457 | } else if (isVNode(vNode) && vNode.tagName === 'br') { 458 | const lineBreakFragment = buildLineBreak(); 459 | runFragment.import(lineBreakFragment); 460 | } 461 | runFragment.up(); 462 | 463 | return runFragment; 464 | }; 465 | 466 | // eslint-disable-next-line consistent-return 467 | const fixupLineHeight = (lineHeight, fontSize) => { 468 | // FIXME: If line height is anything other than a number 469 | // eslint-disable-next-line no-restricted-globals 470 | if (!isNaN(lineHeight)) { 471 | if (fontSize) { 472 | const actualLineHeight = +lineHeight * fontSize; 473 | 474 | return HIPToTWIP(actualLineHeight); 475 | } else { 476 | // 240 TWIP or 12 point is default line height 477 | return +lineHeight * 240; 478 | } 479 | } else { 480 | // 240 TWIP or 12 point is default line height 481 | return 240; 482 | } 483 | }; 484 | 485 | // eslint-disable-next-line consistent-return 486 | const fixupFontSize = (fontSizeString) => { 487 | if (pointRegex.test(fontSizeString)) { 488 | const matchedParts = fontSizeString.match(pointRegex); 489 | // convert point to half point 490 | return pointToHIP(matchedParts[1]); 491 | } else if (pixelRegex.test(fontSizeString)) { 492 | const matchedParts = fontSizeString.match(pixelRegex); 493 | // convert pixels to half point 494 | return pixelToHIP(matchedParts[1]); 495 | } 496 | }; 497 | 498 | // eslint-disable-next-line consistent-return 499 | const fixupRowHeight = (rowHeightString) => { 500 | if (pointRegex.test(rowHeightString)) { 501 | const matchedParts = rowHeightString.match(pointRegex); 502 | // convert point to half point 503 | return pointToTWIP(matchedParts[1]); 504 | } else if (pixelRegex.test(rowHeightString)) { 505 | const matchedParts = rowHeightString.match(pixelRegex); 506 | // convert pixels to half point 507 | return pixelToTWIP(matchedParts[1]); 508 | } 509 | }; 510 | 511 | const buildRunOrRuns = (vNode, attributes) => { 512 | if (isVNode(vNode) && vNode.tagName === 'span') { 513 | const runFragments = []; 514 | 515 | for (let index = 0; index < vNode.children.length; index++) { 516 | const childVNode = vNode.children[index]; 517 | const modifiedAttributes = { ...attributes }; 518 | if (isVNode(vNode) && vNode.properties && vNode.properties.style) { 519 | if ( 520 | vNode.properties.style.color && 521 | !['transparent', 'auto'].includes(vNode.properties.style.color) 522 | ) { 523 | modifiedAttributes.color = fixupColorCode(vNode.properties.style.color); 524 | } 525 | if ( 526 | vNode.properties.style['background-color'] && 527 | !['transparent', 'auto'].includes(vNode.properties.style['background-color']) 528 | ) { 529 | modifiedAttributes.backgroundColor = fixupColorCode( 530 | vNode.properties.style['background-color'] 531 | ); 532 | } 533 | if (vNode.properties.style['font-size']) { 534 | modifiedAttributes.fontSize = fixupFontSize(vNode.properties.style['font-size']); 535 | } 536 | } 537 | runFragments.push(buildRun(childVNode, modifiedAttributes)); 538 | } 539 | 540 | return runFragments; 541 | } else { 542 | const runFragment = buildRun(vNode, attributes); 543 | 544 | return runFragment; 545 | } 546 | }; 547 | 548 | const buildRunOrHyperLink = (vNode, attributes, docxDocumentInstance) => { 549 | if (isVNode(vNode) && vNode.tagName === 'a') { 550 | const relationshipId = docxDocumentInstance.createDocumentRelationships( 551 | docxDocumentInstance.relationshipFilename, 552 | 'hyperlink', 553 | vNode.properties && vNode.properties.href ? vNode.properties.href : '' 554 | ); 555 | const hyperlinkFragment = fragment({ 556 | namespaceAlias: { w: namespaces.w, r: namespaces.r }, 557 | }) 558 | .ele('@w', 'hyperlink') 559 | .att('@r', 'id', `rId${relationshipId}`); 560 | 561 | const modifiedAttributes = { ...attributes }; 562 | modifiedAttributes.hyperlink = true; 563 | 564 | const runFragments = buildRunOrRuns(vNode.children[0], modifiedAttributes); 565 | if (Array.isArray(runFragments)) { 566 | for (let index = 0; index < runFragments.length; index++) { 567 | const runFragment = runFragments[index]; 568 | 569 | hyperlinkFragment.import(runFragment); 570 | } 571 | } else { 572 | hyperlinkFragment.import(runFragments); 573 | } 574 | hyperlinkFragment.up(); 575 | 576 | return hyperlinkFragment; 577 | } 578 | const runFragments = buildRunOrRuns(vNode, attributes); 579 | 580 | return runFragments; 581 | }; 582 | 583 | const buildNumberingProperties = (levelId, numberingId) => { 584 | const numberingPropertiesFragment = fragment({ 585 | namespaceAlias: { w: namespaces.w }, 586 | }) 587 | .ele('@w', 'numPr') 588 | .ele('@w', 'ilvl') 589 | .att('@w', 'val', String(levelId)) 590 | .up() 591 | .ele('@w', 'numId') 592 | .att('@w', 'val', String(numberingId)) 593 | .up() 594 | .up(); 595 | 596 | return numberingPropertiesFragment; 597 | }; 598 | 599 | const buildNumberingInstances = () => { 600 | const numberingInstancesFragment = fragment({ 601 | namespaceAlias: { w: namespaces.w }, 602 | }) 603 | .ele('@w', 'num') 604 | .ele('@w', 'abstractNumId') 605 | .up() 606 | .up(); 607 | 608 | return numberingInstancesFragment; 609 | }; 610 | 611 | const buildSpacing = (lineSpacing, beforeSpacing, afterSpacing) => { 612 | const spacingFragment = fragment({ 613 | namespaceAlias: { w: namespaces.w }, 614 | }).ele('@w', 'spacing'); 615 | 616 | if (lineSpacing) { 617 | spacingFragment.att('@w', 'line', lineSpacing); 618 | } 619 | if (beforeSpacing) { 620 | spacingFragment.att('@w', 'before', beforeSpacing); 621 | } 622 | if (afterSpacing) { 623 | spacingFragment.att('@w', 'after', afterSpacing); 624 | } 625 | 626 | spacingFragment.att('@w', 'lineRule', 'exact').up(); 627 | 628 | return spacingFragment; 629 | }; 630 | 631 | const buildIndentation = (left = 720) => { 632 | const indentationFragment = fragment({ 633 | namespaceAlias: { w: namespaces.w }, 634 | }) 635 | .ele('@w', 'ind') 636 | .att('@w', 'left', left) 637 | .up(); 638 | 639 | return indentationFragment; 640 | }; 641 | 642 | const buildPStyle = (style = 'Normal') => { 643 | const pStyleFragment = fragment({ 644 | namespaceAlias: { w: namespaces.w }, 645 | }) 646 | .ele('@w', 'pStyle') 647 | .att('@w', 'val', style) 648 | .up(); 649 | 650 | return pStyleFragment; 651 | }; 652 | 653 | const buildHorizontalAlignment = (horizontalAlignment) => { 654 | if (horizontalAlignment === 'justify') { 655 | horizontalAlignment = 'both'; 656 | } 657 | const horizontalAlignmentFragment = fragment({ 658 | namespaceAlias: { w: namespaces.w }, 659 | }) 660 | .ele('@w', 'jc') 661 | .att('@w', 'val', horizontalAlignment) 662 | .up(); 663 | 664 | return horizontalAlignmentFragment; 665 | }; 666 | 667 | const buildParagraphBorder = () => { 668 | const paragraphBorderFragment = fragment({ 669 | namespaceAlias: { w: namespaces.w }, 670 | }).ele('@w', 'pBdr'); 671 | const bordersObject = { 672 | top: { 673 | size: 0, 674 | spacing: 3, 675 | color: 'FFFFFF', 676 | }, 677 | left: { 678 | size: 0, 679 | spacing: 3, 680 | color: 'FFFFFF', 681 | }, 682 | bottom: { 683 | size: 0, 684 | spacing: 3, 685 | color: 'FFFFFF', 686 | }, 687 | right: { 688 | size: 0, 689 | spacing: 3, 690 | color: 'FFFFFF', 691 | }, 692 | }; 693 | 694 | Object.keys(bordersObject).forEach((borderName) => { 695 | if (bordersObject[borderName]) { 696 | const { size, spacing, color } = bordersObject[borderName]; 697 | 698 | const borderFragment = buildBorder(borderName, size, spacing, color); 699 | paragraphBorderFragment.import(borderFragment); 700 | } 701 | }); 702 | 703 | paragraphBorderFragment.up(); 704 | 705 | return paragraphBorderFragment; 706 | }; 707 | 708 | const buildParagraphProperties = (attributes) => { 709 | const paragraphPropertiesFragment = fragment({ 710 | namespaceAlias: { w: namespaces.w }, 711 | }).ele('@w', 'pPr'); 712 | if (attributes && attributes.constructor === Object) { 713 | Object.keys(attributes).forEach((key) => { 714 | // eslint-disable-next-line default-case 715 | switch (key) { 716 | case 'numbering': 717 | const { levelId, numberingId } = attributes[key]; 718 | const numberingPropertiesFragment = buildNumberingProperties(levelId, numberingId); 719 | paragraphPropertiesFragment.import(numberingPropertiesFragment); 720 | // Delete used property 721 | // eslint-disable-next-line no-param-reassign 722 | delete attributes.numbering; 723 | break; 724 | case 'textAlign': 725 | const horizontalAlignmentFragment = buildHorizontalAlignment(attributes[key]); 726 | paragraphPropertiesFragment.import(horizontalAlignmentFragment); 727 | // Delete used property 728 | // eslint-disable-next-line no-param-reassign 729 | delete attributes.textAlign; 730 | break; 731 | case 'backgroundColor': 732 | // Add shading to Paragraph Properties only if display is block 733 | // Essentially if background color needs to be across the row 734 | if (attributes.display === 'block') { 735 | const shadingFragment = buildShading(attributes[key]); 736 | paragraphPropertiesFragment.import(shadingFragment); 737 | // FIXME: Inner padding in case of shaded paragraphs. 738 | const paragraphBorderFragment = buildParagraphBorder(); 739 | paragraphPropertiesFragment.import(paragraphBorderFragment); 740 | // Delete used property 741 | // eslint-disable-next-line no-param-reassign 742 | delete attributes.backgroundColor; 743 | } 744 | break; 745 | case 'paragraphStyle': 746 | const pStyleFragment = buildPStyle(attributes.paragraphStyle); 747 | paragraphPropertiesFragment.import(pStyleFragment); 748 | delete attributes.paragraphStyle; 749 | break; 750 | case 'indentation': 751 | const indentationFragment = buildIndentation(attributes[key].left); 752 | paragraphPropertiesFragment.import(indentationFragment); 753 | // Delete used property 754 | // eslint-disable-next-line no-param-reassign 755 | delete attributes.indentation; 756 | break; 757 | } 758 | }); 759 | 760 | const spacingFragment = buildSpacing( 761 | attributes.lineHeight, 762 | attributes.beforeSpacing, 763 | attributes.afterSpacing 764 | ); 765 | // Delete used properties 766 | // eslint-disable-next-line no-param-reassign 767 | delete attributes.lineHeight; 768 | // eslint-disable-next-line no-param-reassign 769 | delete attributes.beforeSpacing; 770 | // eslint-disable-next-line no-param-reassign 771 | delete attributes.afterSpacing; 772 | 773 | paragraphPropertiesFragment.import(spacingFragment); 774 | } 775 | paragraphPropertiesFragment.up(); 776 | 777 | return paragraphPropertiesFragment; 778 | }; 779 | 780 | const computeImageDimensions = (vNode, attributes) => { 781 | const { maximumWidth, originalWidth, originalHeight } = attributes; 782 | const aspectRatio = originalWidth / originalHeight; 783 | const maximumWidthInEMU = TWIPToEMU(maximumWidth); 784 | let originalWidthInEMU = pixelToEMU(originalWidth); 785 | let originalHeightInEMU = pixelToEMU(originalHeight); 786 | if (originalWidthInEMU > maximumWidthInEMU) { 787 | originalWidthInEMU = maximumWidthInEMU; 788 | originalHeightInEMU = Math.round(originalWidthInEMU / aspectRatio); 789 | } 790 | let modifiedHeight; 791 | let modifiedWidth; 792 | 793 | if (vNode.properties && vNode.properties.style) { 794 | if (vNode.properties.style.width) { 795 | if (vNode.properties.style.width !== 'auto') { 796 | if (pixelRegex.test(vNode.properties.style.width)) { 797 | modifiedWidth = pixelToEMU(vNode.properties.style.width.match(pixelRegex)[1]); 798 | } else if (percentageRegex.test(vNode.properties.style.width)) { 799 | const percentageValue = vNode.properties.style.width.match(percentageRegex)[1]; 800 | 801 | modifiedWidth = Math.round((percentageValue / 100) * originalWidthInEMU); 802 | } 803 | } else { 804 | // eslint-disable-next-line no-lonely-if 805 | if (vNode.properties.style.height && vNode.properties.style.height === 'auto') { 806 | modifiedWidth = originalWidthInEMU; 807 | modifiedHeight = originalHeightInEMU; 808 | } 809 | } 810 | } 811 | if (vNode.properties.style.height) { 812 | if (vNode.properties.style.height !== 'auto') { 813 | if (pixelRegex.test(vNode.properties.style.height)) { 814 | modifiedHeight = pixelToEMU(vNode.properties.style.height.match(pixelRegex)[1]); 815 | } else if (percentageRegex.test(vNode.properties.style.height)) { 816 | const percentageValue = vNode.properties.style.width.match(percentageRegex)[1]; 817 | 818 | modifiedHeight = Math.round((percentageValue / 100) * originalHeightInEMU); 819 | if (!modifiedWidth) { 820 | modifiedWidth = Math.round(modifiedHeight * aspectRatio); 821 | } 822 | } 823 | } else { 824 | // eslint-disable-next-line no-lonely-if 825 | if (modifiedWidth) { 826 | if (!modifiedHeight) { 827 | modifiedHeight = Math.round(modifiedWidth / aspectRatio); 828 | } 829 | } else { 830 | modifiedHeight = originalHeightInEMU; 831 | modifiedWidth = originalWidthInEMU; 832 | } 833 | } 834 | } 835 | if (modifiedWidth && !modifiedHeight) { 836 | modifiedHeight = Math.round(modifiedWidth / aspectRatio); 837 | } else if (modifiedHeight && !modifiedWidth) { 838 | modifiedWidth = Math.round(modifiedHeight * aspectRatio); 839 | } 840 | } else { 841 | modifiedWidth = originalWidthInEMU; 842 | modifiedHeight = originalHeightInEMU; 843 | } 844 | 845 | // eslint-disable-next-line no-param-reassign 846 | attributes.width = modifiedWidth; 847 | // eslint-disable-next-line no-param-reassign 848 | attributes.height = modifiedHeight; 849 | }; 850 | 851 | const buildParagraph = (vNode, attributes, docxDocumentInstance) => { 852 | const paragraphFragment = fragment({ 853 | namespaceAlias: { w: namespaces.w }, 854 | }).ele('@w', 'p'); 855 | const modifiedAttributes = { ...attributes }; 856 | if (isVNode(vNode) && vNode.properties && vNode.properties.style) { 857 | if ( 858 | vNode.properties.style.color && 859 | !['transparent', 'auto'].includes(vNode.properties.style.color) 860 | ) { 861 | modifiedAttributes.color = fixupColorCode(vNode.properties.style.color); 862 | } 863 | if ( 864 | vNode.properties.style['background-color'] && 865 | !['transparent', 'auto'].includes(vNode.properties.style['background-color']) 866 | ) { 867 | modifiedAttributes.backgroundColor = fixupColorCode( 868 | vNode.properties.style['background-color'] 869 | ); 870 | } 871 | if ( 872 | vNode.properties.style['vertical-align'] && 873 | ['top', 'middle', 'bottom'].includes(vNode.properties.style['vertical-align']) 874 | ) { 875 | modifiedAttributes.verticalAlign = vNode.properties.style['vertical-align']; 876 | } 877 | if ( 878 | vNode.properties.style['text-align'] && 879 | ['left', 'right', 'center', 'justify'].includes(vNode.properties.style['text-align']) 880 | ) { 881 | modifiedAttributes.textAlign = vNode.properties.style['text-align']; 882 | } 883 | // FIXME: remove bold check when other font weights are handled. 884 | if (vNode.properties.style['font-weight'] && vNode.properties.style['font-weight'] === 'bold') { 885 | modifiedAttributes.strong = vNode.properties.style['font-weight']; 886 | } 887 | if (vNode.properties.style['font-size']) { 888 | modifiedAttributes.fontSize = fixupFontSize(vNode.properties.style['font-size']); 889 | } 890 | if (vNode.properties.style['line-height']) { 891 | modifiedAttributes.lineHeight = fixupLineHeight( 892 | vNode.properties.style['line-height'], 893 | vNode.properties.style['font-size'] 894 | ? fixupFontSize(vNode.properties.style['font-size']) 895 | : null 896 | ); 897 | } 898 | if (vNode.properties.style.display) { 899 | modifiedAttributes.display = vNode.properties.style.display; 900 | } 901 | } 902 | if (isVNode(vNode) && vNode.tagName === 'blockquote') { 903 | modifiedAttributes.indentation = { left: 284 }; 904 | modifiedAttributes.textAlign = 'justify'; 905 | } else if (isVNode(vNode) && vNode.tagName === 'code') { 906 | modifiedAttributes.highlightColor = 'lightGray'; 907 | } else if (isVNode(vNode) && vNode.tagName === 'pre') { 908 | modifiedAttributes.font = 'Courier'; 909 | } 910 | const paragraphPropertiesFragment = buildParagraphProperties(modifiedAttributes); 911 | paragraphFragment.import(paragraphPropertiesFragment); 912 | if (isVNode(vNode) && vNode.children && Array.isArray(vNode.children) && vNode.children.length) { 913 | if ( 914 | [ 915 | 'span', 916 | 'strong', 917 | 'b', 918 | 'em', 919 | 'i', 920 | 'u', 921 | 'ins', 922 | 'strike', 923 | 'del', 924 | 's', 925 | 'sub', 926 | 'sup', 927 | 'mark', 928 | 'a', 929 | 'code', 930 | 'pre', 931 | ].includes(vNode.tagName) 932 | ) { 933 | const runOrHyperlinkFragments = buildRunOrHyperLink( 934 | vNode, 935 | modifiedAttributes, 936 | docxDocumentInstance 937 | ); 938 | if (Array.isArray(runOrHyperlinkFragments)) { 939 | for ( 940 | let iteratorIndex = 0; 941 | iteratorIndex < runOrHyperlinkFragments.length; 942 | iteratorIndex++ 943 | ) { 944 | const runOrHyperlinkFragment = runOrHyperlinkFragments[iteratorIndex]; 945 | 946 | paragraphFragment.import(runOrHyperlinkFragment); 947 | } 948 | } else { 949 | paragraphFragment.import(runOrHyperlinkFragments); 950 | } 951 | } else if (vNode.tagName === 'blockquote') { 952 | const runFragment = buildRun(vNode, attributes); 953 | paragraphFragment.import(runFragment); 954 | } else { 955 | for (let index = 0; index < vNode.children.length; index++) { 956 | const childVNode = vNode.children[index]; 957 | const runOrHyperlinkFragments = buildRunOrHyperLink( 958 | childVNode, 959 | modifiedAttributes, 960 | docxDocumentInstance 961 | ); 962 | if (Array.isArray(runOrHyperlinkFragments)) { 963 | for ( 964 | let iteratorIndex = 0; 965 | iteratorIndex < runOrHyperlinkFragments.length; 966 | iteratorIndex++ 967 | ) { 968 | const runOrHyperlinkFragment = runOrHyperlinkFragments[iteratorIndex]; 969 | 970 | paragraphFragment.import(runOrHyperlinkFragment); 971 | } 972 | } else { 973 | paragraphFragment.import(runOrHyperlinkFragments); 974 | } 975 | } 976 | } 977 | } else { 978 | // In case paragraphs has to be rendered where vText is present. Eg. table-cell 979 | // Or in case the vNode is something like img 980 | if (isVNode(vNode) && vNode.tagName === 'img') { 981 | computeImageDimensions(vNode, modifiedAttributes); 982 | } 983 | const runFragments = buildRunOrRuns(vNode, modifiedAttributes); 984 | if (Array.isArray(runFragments)) { 985 | for (let index = 0; index < runFragments.length; index++) { 986 | const runFragment = runFragments[index]; 987 | 988 | paragraphFragment.import(runFragment); 989 | } 990 | } else { 991 | paragraphFragment.import(runFragments); 992 | } 993 | } 994 | paragraphFragment.up(); 995 | 996 | return paragraphFragment; 997 | }; 998 | 999 | const buildGridSpanFragment = (spanValue) => { 1000 | const gridSpanFragment = fragment({ 1001 | namespaceAlias: { w: namespaces.w }, 1002 | }) 1003 | .ele('@w', 'gridSpan') 1004 | .att('@w', 'val', spanValue) 1005 | .up(); 1006 | 1007 | return gridSpanFragment; 1008 | }; 1009 | 1010 | const buildTableCellSpacing = (cellSpacing = 0) => { 1011 | const tableCellSpacingFragment = fragment({ 1012 | namespaceAlias: { w: namespaces.w }, 1013 | }) 1014 | .ele('@w', 'tblCellSpacing') 1015 | .att('@w', 'w', cellSpacing) 1016 | .att('@w', 'type', 'dxa') 1017 | .up(); 1018 | 1019 | return tableCellSpacingFragment; 1020 | }; 1021 | 1022 | const buildTableCellBorders = (tableCellBorder) => { 1023 | const tableCellBordersFragment = fragment({ 1024 | namespaceAlias: { w: namespaces.w }, 1025 | }).ele('@w', 'tcBorders'); 1026 | 1027 | const { color, stroke, ...borders } = tableCellBorder; 1028 | Object.keys(borders).forEach((border) => { 1029 | if (tableCellBorder[border]) { 1030 | const borderFragment = buildBorder(border, tableCellBorder[border], 0, color, stroke); 1031 | tableCellBordersFragment.import(borderFragment); 1032 | } 1033 | }); 1034 | 1035 | tableCellBordersFragment.up(); 1036 | 1037 | return tableCellBordersFragment; 1038 | }; 1039 | 1040 | const buildTableCellProperties = (attributes) => { 1041 | const tableCellPropertiesFragment = fragment({ 1042 | namespaceAlias: { w: namespaces.w }, 1043 | }).ele('@w', 'tcPr'); 1044 | if (attributes && attributes.constructor === Object) { 1045 | Object.keys(attributes).forEach((key) => { 1046 | // eslint-disable-next-line default-case 1047 | switch (key) { 1048 | case 'backgroundColor': 1049 | const shadingFragment = buildShading(attributes[key]); 1050 | tableCellPropertiesFragment.import(shadingFragment); 1051 | // Delete used property 1052 | // eslint-disable-next-line no-param-reassign 1053 | delete attributes.backgroundColor; 1054 | break; 1055 | case 'verticalAlign': 1056 | const verticalAlignmentFragment = buildVerticalAlignment(attributes[key]); 1057 | tableCellPropertiesFragment.import(verticalAlignmentFragment); 1058 | // Delete used property 1059 | // eslint-disable-next-line no-param-reassign 1060 | delete attributes.verticalAlign; 1061 | break; 1062 | case 'colSpan': 1063 | const gridSpanFragment = buildGridSpanFragment(attributes[key]); 1064 | tableCellPropertiesFragment.import(gridSpanFragment); 1065 | // Delete used property 1066 | // eslint-disable-next-line no-param-reassign 1067 | delete attributes.colSpan; 1068 | break; 1069 | case 'tableCellBorder': 1070 | const tableCellBorderFragment = buildTableCellBorders(attributes[key]); 1071 | tableCellPropertiesFragment.import(tableCellBorderFragment); 1072 | 1073 | // Delete used property 1074 | // eslint-disable-next-line no-param-reassign 1075 | delete attributes.tableCellBorder; 1076 | break; 1077 | case 'rowSpan': 1078 | const verticalMergeFragment = buildVerticalMerge(attributes[key]); 1079 | tableCellPropertiesFragment.import(verticalMergeFragment); 1080 | 1081 | delete attributes.rowSpan; 1082 | break; 1083 | } 1084 | }); 1085 | } 1086 | tableCellPropertiesFragment.up(); 1087 | 1088 | return tableCellPropertiesFragment; 1089 | }; 1090 | 1091 | const fixupTableCellBorder = (vNode, attributes) => { 1092 | if (Object.prototype.hasOwnProperty.call(vNode.properties.style, 'border')) { 1093 | if (vNode.properties.style.border === 'none' || vNode.properties.style.border === 0) { 1094 | attributes.tableCellBorder = {}; 1095 | } else { 1096 | // eslint-disable-next-line no-use-before-define 1097 | const [borderSize, borderStroke, borderColor] = cssBorderParser( 1098 | vNode.properties.style.border 1099 | ); 1100 | 1101 | attributes.tableCellBorder = { 1102 | top: borderSize, 1103 | left: borderSize, 1104 | bottom: borderSize, 1105 | right: borderSize, 1106 | color: borderColor, 1107 | stroke: borderStroke, 1108 | }; 1109 | } 1110 | } 1111 | if (vNode.properties.style['border-top'] && vNode.properties.style['border-top'] === '0') { 1112 | attributes.tableCellBorder = { 1113 | ...attributes.tableCellBorder, 1114 | top: 0, 1115 | }; 1116 | } else if (vNode.properties.style['border-top'] && vNode.properties.style['border-top'] !== '0') { 1117 | // eslint-disable-next-line no-use-before-define 1118 | const [borderSize, borderStroke, borderColor] = cssBorderParser( 1119 | vNode.properties.style['border-top'] 1120 | ); 1121 | attributes.tableCellBorder = { 1122 | ...attributes.tableCellBorder, 1123 | top: borderSize, 1124 | color: borderColor, 1125 | stroke: borderStroke, 1126 | }; 1127 | } 1128 | if (vNode.properties.style['border-left'] && vNode.properties.style['border-left'] === '0') { 1129 | attributes.tableCellBorder = { 1130 | ...attributes.tableCellBorder, 1131 | left: 0, 1132 | }; 1133 | } else if ( 1134 | vNode.properties.style['border-left'] && 1135 | vNode.properties.style['border-left'] !== '0' 1136 | ) { 1137 | // eslint-disable-next-line no-use-before-define 1138 | const [borderSize, borderStroke, borderColor] = cssBorderParser( 1139 | vNode.properties.style['border-left'] 1140 | ); 1141 | attributes.tableCellBorder = { 1142 | ...attributes.tableCellBorder, 1143 | left: borderSize, 1144 | color: borderColor, 1145 | stroke: borderStroke, 1146 | }; 1147 | } 1148 | if (vNode.properties.style['border-bottom'] && vNode.properties.style['border-bottom'] === '0') { 1149 | attributes.tableCellBorder = { 1150 | ...attributes.tableCellBorder, 1151 | bottom: 0, 1152 | }; 1153 | } else if ( 1154 | vNode.properties.style['border-bottom'] && 1155 | vNode.properties.style['border-bottom'] !== '0' 1156 | ) { 1157 | // eslint-disable-next-line no-use-before-define 1158 | const [borderSize, borderStroke, borderColor] = cssBorderParser( 1159 | vNode.properties.style['border-bottom'] 1160 | ); 1161 | attributes.tableCellBorder = { 1162 | ...attributes.tableCellBorder, 1163 | bottom: borderSize, 1164 | color: borderColor, 1165 | stroke: borderStroke, 1166 | }; 1167 | } 1168 | if (vNode.properties.style['border-right'] && vNode.properties.style['border-right'] === '0') { 1169 | attributes.tableCellBorder = { 1170 | ...attributes.tableCellBorder, 1171 | right: 0, 1172 | }; 1173 | } else if ( 1174 | vNode.properties.style['border-right'] && 1175 | vNode.properties.style['border-right'] !== '0' 1176 | ) { 1177 | // eslint-disable-next-line no-use-before-define 1178 | const [borderSize, borderStroke, borderColor] = cssBorderParser( 1179 | vNode.properties.style['border-right'] 1180 | ); 1181 | attributes.tableCellBorder = { 1182 | ...attributes.tableCellBorder, 1183 | right: borderSize, 1184 | color: borderColor, 1185 | stroke: borderStroke, 1186 | }; 1187 | } 1188 | }; 1189 | 1190 | const buildTableCell = (vNode, attributes, rowSpanMap, columnIndex, docxDocumentInstance) => { 1191 | const tableCellFragment = fragment({ 1192 | namespaceAlias: { w: namespaces.w }, 1193 | }).ele('@w', 'tc'); 1194 | 1195 | const modifiedAttributes = { ...attributes }; 1196 | if (isVNode(vNode) && vNode.properties) { 1197 | if (vNode.properties.rowSpan) { 1198 | rowSpanMap.set(columnIndex.index, { rowSpan: vNode.properties.rowSpan - 1, colSpan: 0 }); 1199 | modifiedAttributes.rowSpan = 'restart'; 1200 | } else { 1201 | const previousSpanObject = rowSpanMap.get(columnIndex.index); 1202 | rowSpanMap.set( 1203 | columnIndex.index, 1204 | // eslint-disable-next-line prefer-object-spread 1205 | Object.assign({}, previousSpanObject, { 1206 | rowSpan: 0, 1207 | colSpan: (previousSpanObject && previousSpanObject.colSpan) || 0, 1208 | }) 1209 | ); 1210 | } 1211 | if ( 1212 | vNode.properties.colSpan || 1213 | (vNode.properties.style && vNode.properties.style['column-span']) 1214 | ) { 1215 | modifiedAttributes.colSpan = 1216 | vNode.properties.colSpan || 1217 | (vNode.properties.style && vNode.properties.style['column-span']); 1218 | const previousSpanObject = rowSpanMap.get(columnIndex.index); 1219 | rowSpanMap.set( 1220 | columnIndex.index, 1221 | // eslint-disable-next-line prefer-object-spread 1222 | Object.assign({}, previousSpanObject, { 1223 | colSpan: parseInt(modifiedAttributes.colSpan) || 0, 1224 | }) 1225 | ); 1226 | columnIndex.index += parseInt(modifiedAttributes.colSpan) - 1; 1227 | } 1228 | if (vNode.properties.style) { 1229 | if ( 1230 | vNode.properties.style.color && 1231 | !['transparent', 'auto'].includes(vNode.properties.style.color) 1232 | ) { 1233 | modifiedAttributes.color = fixupColorCode(vNode.properties.style.color); 1234 | } 1235 | if ( 1236 | vNode.properties.style['background-color'] && 1237 | !['transparent', 'auto'].includes(vNode.properties.style['background-color']) 1238 | ) { 1239 | modifiedAttributes.backgroundColor = fixupColorCode( 1240 | vNode.properties.style['background-color'] 1241 | ); 1242 | } 1243 | if ( 1244 | vNode.properties.style['vertical-align'] && 1245 | ['top', 'middle', 'bottom'].includes(vNode.properties.style['vertical-align']) 1246 | ) { 1247 | modifiedAttributes.verticalAlign = vNode.properties.style['vertical-align']; 1248 | } 1249 | fixupTableCellBorder(vNode, modifiedAttributes); 1250 | } 1251 | } 1252 | const tableCellPropertiesFragment = buildTableCellProperties(modifiedAttributes); 1253 | tableCellFragment.import(tableCellPropertiesFragment); 1254 | if (vNode.children && Array.isArray(vNode.children) && vNode.children.length) { 1255 | for (let index = 0; index < vNode.children.length; index++) { 1256 | const childVNode = vNode.children[index]; 1257 | if (isVNode(childVNode) && childVNode.tagName === 'img') { 1258 | const imageFragment = buildImage( 1259 | docxDocumentInstance, 1260 | childVNode, 1261 | modifiedAttributes.maximumWidth 1262 | ); 1263 | if (imageFragment) { 1264 | tableCellFragment.import(imageFragment); 1265 | } 1266 | } else if (isVNode(childVNode) && childVNode.tagName === 'figure') { 1267 | if ( 1268 | childVNode.children && 1269 | Array.isArray(childVNode.children) && 1270 | childVNode.children.length 1271 | ) { 1272 | // eslint-disable-next-line no-plusplus 1273 | for (let iteratorIndex = 0; iteratorIndex < childVNode.children.length; iteratorIndex++) { 1274 | const grandChildVNode = childVNode.children[iteratorIndex]; 1275 | if (grandChildVNode.tagName === 'img') { 1276 | const imageFragment = buildImage( 1277 | docxDocumentInstance, 1278 | grandChildVNode, 1279 | modifiedAttributes.maximumWidth 1280 | ); 1281 | if (imageFragment) { 1282 | tableCellFragment.import(imageFragment); 1283 | } 1284 | } 1285 | } 1286 | } 1287 | } else { 1288 | const paragraphFragment = buildParagraph( 1289 | childVNode, 1290 | modifiedAttributes, 1291 | docxDocumentInstance 1292 | ); 1293 | tableCellFragment.import(paragraphFragment); 1294 | } 1295 | } 1296 | } else { 1297 | // TODO: Figure out why building with buildParagraph() isn't working 1298 | const paragraphFragment = fragment({ 1299 | namespaceAlias: { w: namespaces.w }, 1300 | }) 1301 | .ele('@w', 'p') 1302 | .up(); 1303 | tableCellFragment.import(paragraphFragment); 1304 | } 1305 | tableCellFragment.up(); 1306 | 1307 | return tableCellFragment; 1308 | }; 1309 | 1310 | const buildRowSpanCell = (rowSpanMap, columnIndex, attributes) => { 1311 | const rowSpanCellFragments = []; 1312 | let spanObject = rowSpanMap.get(columnIndex.index); 1313 | while (spanObject && spanObject.rowSpan) { 1314 | const rowSpanCellFragment = fragment({ 1315 | namespaceAlias: { w: namespaces.w }, 1316 | }).ele('@w', 'tc'); 1317 | 1318 | const tableCellPropertiesFragment = buildTableCellProperties({ 1319 | ...attributes, 1320 | rowSpan: 'continue', 1321 | colSpan: spanObject.colSpan ? spanObject.colSpan : 0, 1322 | }); 1323 | rowSpanCellFragment.import(tableCellPropertiesFragment); 1324 | 1325 | const paragraphFragment = fragment({ 1326 | namespaceAlias: { w: namespaces.w }, 1327 | }) 1328 | .ele('@w', 'p') 1329 | .up(); 1330 | rowSpanCellFragment.import(paragraphFragment); 1331 | rowSpanCellFragment.up(); 1332 | 1333 | rowSpanCellFragments.push(rowSpanCellFragment); 1334 | 1335 | if (spanObject.rowSpan - 1 === 0) { 1336 | rowSpanMap.delete(columnIndex.index); 1337 | } else { 1338 | rowSpanMap.set(columnIndex.index, { 1339 | rowSpan: spanObject.rowSpan - 1, 1340 | colSpan: spanObject.colSpan || 0, 1341 | }); 1342 | } 1343 | columnIndex.index += spanObject.colSpan || 1; 1344 | spanObject = rowSpanMap.get(columnIndex.index); 1345 | } 1346 | 1347 | return rowSpanCellFragments; 1348 | }; 1349 | 1350 | const buildTableRowProperties = (attributes) => { 1351 | const tableRowPropertiesFragment = fragment({ 1352 | namespaceAlias: { w: namespaces.w }, 1353 | }).ele('@w', 'trPr'); 1354 | if (attributes && attributes.constructor === Object) { 1355 | Object.keys(attributes).forEach((key) => { 1356 | // eslint-disable-next-line default-case 1357 | switch (key) { 1358 | case 'tableRowHeight': 1359 | const tableRowHeightFragment = buildTableRowHeight(attributes[key]); 1360 | tableRowPropertiesFragment.import(tableRowHeightFragment); 1361 | // Delete used property 1362 | // eslint-disable-next-line no-param-reassign 1363 | delete attributes.tableRowHeight; 1364 | break; 1365 | case 'rowCantSplit': 1366 | if (attributes.rowCantSplit) { 1367 | const cantSplitFragment = fragment({ 1368 | namespaceAlias: { w: namespaces.w }, 1369 | }) 1370 | .ele('@w', 'cantSplit') 1371 | .up(); 1372 | tableRowPropertiesFragment.import(cantSplitFragment); 1373 | // Delete used property 1374 | // eslint-disable-next-line no-param-reassign 1375 | delete attributes.rowCantSplit; 1376 | } 1377 | break; 1378 | } 1379 | }); 1380 | } 1381 | tableRowPropertiesFragment.up(); 1382 | return tableRowPropertiesFragment; 1383 | }; 1384 | 1385 | const buildTableRow = (vNode, attributes, rowSpanMap, docxDocumentInstance) => { 1386 | const tableRowFragment = fragment({ 1387 | namespaceAlias: { w: namespaces.w }, 1388 | }).ele('@w', 'tr'); 1389 | const modifiedAttributes = { ...attributes }; 1390 | if (isVNode(vNode) && vNode.properties) { 1391 | // FIXME: find a better way to get row height from cell style 1392 | if ( 1393 | (vNode.properties.style && vNode.properties.style.height) || 1394 | (vNode.children[0] && 1395 | isVNode(vNode.children[0]) && 1396 | vNode.children[0].properties.style && 1397 | vNode.children[0].properties.style.height) 1398 | ) { 1399 | modifiedAttributes.tableRowHeight = fixupRowHeight( 1400 | (vNode.properties.style && vNode.properties.style.height) || 1401 | (vNode.children[0] && 1402 | isVNode(vNode.children[0]) && 1403 | vNode.children[0].properties.style && 1404 | vNode.children[0].properties.style.height 1405 | ? vNode.children[0].properties.style.height 1406 | : undefined) 1407 | ); 1408 | } 1409 | if (vNode.properties.style) { 1410 | fixupTableCellBorder(vNode, modifiedAttributes); 1411 | } 1412 | } 1413 | const tableRowPropertiesFragment = buildTableRowProperties(modifiedAttributes); 1414 | tableRowFragment.import(tableRowPropertiesFragment); 1415 | 1416 | const columnIndex = { index: 0 }; 1417 | 1418 | if (vNode.children && Array.isArray(vNode.children) && vNode.children.length) { 1419 | const tableColumns = vNode.children.filter((childVNode) => 1420 | ['td', 'th'].includes(childVNode.tagName) 1421 | ); 1422 | const columnWidth = docxDocumentInstance.availableDocumentSpace / tableColumns.length; 1423 | 1424 | for (let index = 0; index < vNode.children.length; index++) { 1425 | const childVNode = vNode.children[index]; 1426 | if (['td', 'th'].includes(childVNode.tagName)) { 1427 | const rowSpanCellFragments = buildRowSpanCell(rowSpanMap, columnIndex, modifiedAttributes); 1428 | if (Array.isArray(rowSpanCellFragments)) { 1429 | for ( 1430 | let iteratorIndex = 0; 1431 | iteratorIndex < rowSpanCellFragments.length; 1432 | iteratorIndex++ 1433 | ) { 1434 | const rowSpanCellFragment = rowSpanCellFragments[iteratorIndex]; 1435 | 1436 | tableRowFragment.import(rowSpanCellFragment); 1437 | } 1438 | } 1439 | const tableCellFragment = buildTableCell( 1440 | childVNode, 1441 | { ...modifiedAttributes, maximumWidth: columnWidth }, 1442 | rowSpanMap, 1443 | columnIndex, 1444 | docxDocumentInstance 1445 | ); 1446 | columnIndex.index++; 1447 | 1448 | tableRowFragment.import(tableCellFragment); 1449 | } 1450 | } 1451 | } 1452 | if (columnIndex.index < rowSpanMap.size) { 1453 | const rowSpanCellFragments = buildRowSpanCell(rowSpanMap, columnIndex, modifiedAttributes); 1454 | if (Array.isArray(rowSpanCellFragments)) { 1455 | for (let iteratorIndex = 0; iteratorIndex < rowSpanCellFragments.length; iteratorIndex++) { 1456 | const rowSpanCellFragment = rowSpanCellFragments[iteratorIndex]; 1457 | 1458 | tableRowFragment.import(rowSpanCellFragment); 1459 | } 1460 | } 1461 | } 1462 | tableRowFragment.up(); 1463 | 1464 | return tableRowFragment; 1465 | }; 1466 | 1467 | const buildTableGridCol = (gridWidth) => { 1468 | const tableGridColFragment = fragment({ 1469 | namespaceAlias: { w: namespaces.w }, 1470 | }) 1471 | .ele('@w', 'gridCol') 1472 | .att('@w', 'w', String(gridWidth)); 1473 | 1474 | return tableGridColFragment; 1475 | }; 1476 | 1477 | const buildTableGrid = (vNode, attributes) => { 1478 | const tableGridFragment = fragment({ 1479 | namespaceAlias: { w: namespaces.w }, 1480 | }).ele('@w', 'tblGrid'); 1481 | if (vNode.children && Array.isArray(vNode.children) && vNode.children.length) { 1482 | const gridColumns = vNode.children.filter((childVNode) => childVNode.tagName === 'col'); 1483 | const gridWidth = attributes.maximumWidth / gridColumns.length; 1484 | 1485 | for (let index = 0; index < gridColumns.length; index++) { 1486 | const tableGridColFragment = buildTableGridCol(gridWidth); 1487 | tableGridFragment.import(tableGridColFragment); 1488 | } 1489 | } 1490 | tableGridFragment.up(); 1491 | 1492 | return tableGridFragment; 1493 | }; 1494 | 1495 | const buildTableGridFromTableRow = (vNode, attributes) => { 1496 | const tableGridFragment = fragment({ 1497 | namespaceAlias: { w: namespaces.w }, 1498 | }).ele('@w', 'tblGrid'); 1499 | if (vNode.children && Array.isArray(vNode.children) && vNode.children.length) { 1500 | const numberOfGridColumns = vNode.children.reduce((accumulator, childVNode) => { 1501 | const colSpan = 1502 | childVNode.properties.colSpan || 1503 | (childVNode.properties.style && childVNode.properties.style['column-span']); 1504 | 1505 | return accumulator + (colSpan ? parseInt(colSpan) : 1); 1506 | }, 0); 1507 | const gridWidth = attributes.maximumWidth / numberOfGridColumns; 1508 | 1509 | for (let index = 0; index < numberOfGridColumns; index++) { 1510 | const tableGridColFragment = buildTableGridCol(gridWidth); 1511 | tableGridFragment.import(tableGridColFragment); 1512 | } 1513 | } 1514 | tableGridFragment.up(); 1515 | 1516 | return tableGridFragment; 1517 | }; 1518 | 1519 | const buildTableBorders = (tableBorder) => { 1520 | const tableBordersFragment = fragment({ 1521 | namespaceAlias: { w: namespaces.w }, 1522 | }).ele('@w', 'tblBorders'); 1523 | 1524 | const { color, stroke, ...borders } = tableBorder; 1525 | 1526 | Object.keys(borders).forEach((border) => { 1527 | if (borders[border]) { 1528 | const borderFragment = buildBorder(border, borders[border], 0, color, stroke); 1529 | tableBordersFragment.import(borderFragment); 1530 | } 1531 | }); 1532 | 1533 | tableBordersFragment.up(); 1534 | 1535 | return tableBordersFragment; 1536 | }; 1537 | 1538 | const buildTableWidth = (tableWidth) => { 1539 | const tableWidthFragment = fragment({ 1540 | namespaceAlias: { w: namespaces.w }, 1541 | }) 1542 | .ele('@w', 'tblW') 1543 | .att('@w', 'type', 'dxa') 1544 | .att('@w', 'w', String(tableWidth)) 1545 | .up(); 1546 | 1547 | return tableWidthFragment; 1548 | }; 1549 | 1550 | const buildCellMargin = (side, margin) => { 1551 | const marginFragment = fragment({ 1552 | namespaceAlias: { w: namespaces.w }, 1553 | }) 1554 | .ele('@w', side) 1555 | .att('@w', 'type', 'dxa') 1556 | .att('@w', 'w', String(margin)) 1557 | .up(); 1558 | 1559 | return marginFragment; 1560 | }; 1561 | 1562 | const buildTableCellMargins = (margin) => { 1563 | const tableCellMarFragment = fragment({ 1564 | namespaceAlias: { w: namespaces.w }, 1565 | }).ele('@w', 'tblCellMar'); 1566 | 1567 | ['top', 'bottom'].forEach((side) => { 1568 | const marginFragment = buildCellMargin(side, margin / 2); 1569 | tableCellMarFragment.import(marginFragment); 1570 | }); 1571 | ['left', 'right'].forEach((side) => { 1572 | const marginFragment = buildCellMargin(side, margin); 1573 | tableCellMarFragment.import(marginFragment); 1574 | }); 1575 | 1576 | return tableCellMarFragment; 1577 | }; 1578 | 1579 | const buildTableProperties = (attributes) => { 1580 | const tablePropertiesFragment = fragment({ 1581 | namespaceAlias: { w: namespaces.w }, 1582 | }).ele('@w', 'tblPr'); 1583 | 1584 | if (attributes && attributes.constructor === Object) { 1585 | Object.keys(attributes).forEach((key) => { 1586 | // eslint-disable-next-line default-case 1587 | switch (key) { 1588 | case 'tableBorder': 1589 | const tableBordersFragment = buildTableBorders(attributes[key]); 1590 | tablePropertiesFragment.import(tableBordersFragment); 1591 | // Delete used property 1592 | // eslint-disable-next-line no-param-reassign 1593 | delete attributes.tableBorder; 1594 | break; 1595 | case 'tableCellSpacing': 1596 | const tableCellSpacingFragment = buildTableCellSpacing(attributes[key]); 1597 | tablePropertiesFragment.import(tableCellSpacingFragment); 1598 | // Delete used property 1599 | // eslint-disable-next-line no-param-reassign 1600 | delete attributes.tableCellSpacing; 1601 | break; 1602 | case 'width': 1603 | if (attributes[key]) { 1604 | const tableWidthFragment = buildTableWidth(attributes[key]); 1605 | tablePropertiesFragment.import(tableWidthFragment); 1606 | } 1607 | // Delete used property 1608 | // eslint-disable-next-line no-param-reassign 1609 | delete attributes.width; 1610 | break; 1611 | } 1612 | }); 1613 | } 1614 | const tableCellMarginFragment = buildTableCellMargins(160); 1615 | tablePropertiesFragment.import(tableCellMarginFragment); 1616 | 1617 | // by default, all tables are center aligned. 1618 | const alignmentFragment = buildHorizontalAlignment('center'); 1619 | tablePropertiesFragment.import(alignmentFragment); 1620 | 1621 | tablePropertiesFragment.up(); 1622 | 1623 | return tablePropertiesFragment; 1624 | }; 1625 | 1626 | const cssBorderParser = (borderString) => { 1627 | let [size, stroke, color] = borderString.split(' '); 1628 | 1629 | if (pointRegex.test(size)) { 1630 | const matchedParts = size.match(pointRegex); 1631 | // convert point to eighth of a point 1632 | size = pointToEIP(matchedParts[1]); 1633 | } else if (pixelRegex.test(size)) { 1634 | const matchedParts = size.match(pixelRegex); 1635 | // convert pixels to eighth of a point 1636 | size = pixelToEIP(matchedParts[1]); 1637 | } 1638 | stroke = stroke && ['dashed', 'dotted', 'double'].includes(stroke) ? stroke : 'single'; 1639 | 1640 | color = color && fixupColorCode(color).toUpperCase(); 1641 | 1642 | return [size, stroke, color]; 1643 | }; 1644 | 1645 | const buildTable = (vNode, attributes, docxDocumentInstance) => { 1646 | const tableFragment = fragment({ 1647 | namespaceAlias: { w: namespaces.w }, 1648 | }).ele('@w', 'tbl'); 1649 | const modifiedAttributes = { ...attributes }; 1650 | if (isVNode(vNode) && vNode.properties) { 1651 | const tableAttributes = vNode.properties.attributes || {}; 1652 | const tableStyles = vNode.properties.style || {}; 1653 | const tableBorders = {}; 1654 | const tableCellBorders = {}; 1655 | let [borderSize, borderStrike, borderColor] = [2, 'single', '000000']; 1656 | 1657 | // eslint-disable-next-line no-restricted-globals 1658 | if (!isNaN(tableAttributes.border)) { 1659 | borderSize = parseInt(tableAttributes.border, 10); 1660 | } 1661 | 1662 | // css style overrides table border properties 1663 | if (tableStyles.border) { 1664 | const [cssSize, cssStroke, cssColor] = cssBorderParser(tableStyles.border); 1665 | borderSize = cssSize || borderSize; 1666 | borderColor = cssColor || borderColor; 1667 | borderStrike = cssStroke || borderStrike; 1668 | } 1669 | 1670 | tableBorders.top = borderSize; 1671 | tableBorders.bottom = borderSize; 1672 | tableBorders.left = borderSize; 1673 | tableBorders.right = borderSize; 1674 | tableBorders.stroke = borderStrike; 1675 | tableBorders.color = borderColor; 1676 | 1677 | if (tableStyles['border-collapse'] === 'collapse') { 1678 | tableBorders.insideV = borderSize; 1679 | tableBorders.insideH = borderSize; 1680 | } else { 1681 | tableBorders.insideV = 0; 1682 | tableBorders.insideH = 0; 1683 | tableCellBorders.top = 1; 1684 | tableCellBorders.bottom = 1; 1685 | tableCellBorders.left = 1; 1686 | tableCellBorders.right = 1; 1687 | } 1688 | 1689 | modifiedAttributes.tableBorder = tableBorders; 1690 | modifiedAttributes.tableCellSpacing = 0; 1691 | 1692 | if (Object.keys(tableCellBorders).length) { 1693 | modifiedAttributes.tableCellBorder = tableCellBorders; 1694 | } 1695 | 1696 | let minimumWidth; 1697 | let maximumWidth; 1698 | let width; 1699 | // Calculate minimum width of table 1700 | if (pixelRegex.test(tableStyles['min-width'])) { 1701 | minimumWidth = pixelToTWIP(tableStyles['min-width'].match(pixelRegex)[1]); 1702 | } else if (percentageRegex.test(tableStyles['min-width'])) { 1703 | const percentageValue = tableStyles['min-width'].match(percentageRegex)[1]; 1704 | minimumWidth = Math.round((percentageValue / 100) * attributes.maximumWidth); 1705 | } 1706 | 1707 | // Calculate maximum width of table 1708 | if (pixelRegex.test(tableStyles['max-width'])) { 1709 | pixelRegex.lastIndex = 0; 1710 | maximumWidth = pixelToTWIP(tableStyles['max-width'].match(pixelRegex)[1]); 1711 | } else if (percentageRegex.test(tableStyles['max-width'])) { 1712 | percentageRegex.lastIndex = 0; 1713 | const percentageValue = tableStyles['max-width'].match(percentageRegex)[1]; 1714 | maximumWidth = Math.round((percentageValue / 100) * attributes.maximumWidth); 1715 | } 1716 | 1717 | // Calculate specified width of table 1718 | if (pixelRegex.test(tableStyles.width)) { 1719 | pixelRegex.lastIndex = 0; 1720 | width = pixelToTWIP(tableStyles.width.match(pixelRegex)[1]); 1721 | } else if (percentageRegex.test(tableStyles.width)) { 1722 | percentageRegex.lastIndex = 0; 1723 | const percentageValue = tableStyles.width.match(percentageRegex)[1]; 1724 | width = Math.round((percentageValue / 100) * attributes.maximumWidth); 1725 | } 1726 | 1727 | // If width isn't supplied, we should have min-width as the width. 1728 | if (width) { 1729 | modifiedAttributes.width = width; 1730 | if (maximumWidth) { 1731 | modifiedAttributes.width = Math.min(modifiedAttributes.width, maximumWidth); 1732 | } 1733 | if (minimumWidth) { 1734 | modifiedAttributes.width = Math.max(modifiedAttributes.width, minimumWidth); 1735 | } 1736 | } else if (minimumWidth) { 1737 | modifiedAttributes.width = minimumWidth; 1738 | } 1739 | if (modifiedAttributes.width) { 1740 | modifiedAttributes.width = Math.min(modifiedAttributes.width, attributes.maximumWidth); 1741 | } 1742 | } 1743 | const tablePropertiesFragment = buildTableProperties(modifiedAttributes); 1744 | tableFragment.import(tablePropertiesFragment); 1745 | 1746 | const rowSpanMap = new Map(); 1747 | 1748 | if (vNode.children && Array.isArray(vNode.children) && vNode.children.length) { 1749 | for (let index = 0; index < vNode.children.length; index++) { 1750 | const childVNode = vNode.children[index]; 1751 | if (childVNode.tagName === 'colgroup') { 1752 | const tableGridFragment = buildTableGrid(childVNode, modifiedAttributes); 1753 | tableFragment.import(tableGridFragment); 1754 | } else if (childVNode.tagName === 'thead') { 1755 | for (let iteratorIndex = 0; iteratorIndex < childVNode.children.length; iteratorIndex++) { 1756 | const grandChildVNode = childVNode.children[iteratorIndex]; 1757 | if (grandChildVNode.tagName === 'tr') { 1758 | if (iteratorIndex === 0) { 1759 | const tableGridFragment = buildTableGridFromTableRow( 1760 | grandChildVNode, 1761 | modifiedAttributes 1762 | ); 1763 | tableFragment.import(tableGridFragment); 1764 | } 1765 | const tableRowFragment = buildTableRow( 1766 | grandChildVNode, 1767 | modifiedAttributes, 1768 | rowSpanMap, 1769 | docxDocumentInstance 1770 | ); 1771 | tableFragment.import(tableRowFragment); 1772 | } 1773 | } 1774 | } else if (childVNode.tagName === 'tbody') { 1775 | for (let iteratorIndex = 0; iteratorIndex < childVNode.children.length; iteratorIndex++) { 1776 | const grandChildVNode = childVNode.children[iteratorIndex]; 1777 | if (grandChildVNode.tagName === 'tr') { 1778 | if (iteratorIndex === 0) { 1779 | const tableGridFragment = buildTableGridFromTableRow( 1780 | grandChildVNode, 1781 | modifiedAttributes 1782 | ); 1783 | tableFragment.import(tableGridFragment); 1784 | } 1785 | const tableRowFragment = buildTableRow( 1786 | grandChildVNode, 1787 | modifiedAttributes, 1788 | rowSpanMap, 1789 | docxDocumentInstance 1790 | ); 1791 | tableFragment.import(tableRowFragment); 1792 | } 1793 | } 1794 | } else if (childVNode.tagName === 'tr') { 1795 | if (index === 0) { 1796 | const tableGridFragment = buildTableGridFromTableRow(childVNode, modifiedAttributes); 1797 | tableFragment.import(tableGridFragment); 1798 | } 1799 | const tableRowFragment = buildTableRow( 1800 | childVNode, 1801 | modifiedAttributes, 1802 | rowSpanMap, 1803 | docxDocumentInstance 1804 | ); 1805 | tableFragment.import(tableRowFragment); 1806 | } 1807 | } 1808 | } 1809 | tableFragment.up(); 1810 | 1811 | return tableFragment; 1812 | }; 1813 | 1814 | const buildPresetGeometry = () => { 1815 | const presetGeometryFragment = fragment({ 1816 | namespaceAlias: { a: namespaces.a }, 1817 | }) 1818 | .ele('@a', 'prstGeom') 1819 | .att('prst', 'rect') 1820 | .up(); 1821 | 1822 | return presetGeometryFragment; 1823 | }; 1824 | 1825 | const buildExtents = ({ width, height }) => { 1826 | const extentsFragment = fragment({ 1827 | namespaceAlias: { a: namespaces.a }, 1828 | }) 1829 | .ele('@a', 'ext') 1830 | .att('cx', width) 1831 | .att('cy', height) 1832 | .up(); 1833 | 1834 | return extentsFragment; 1835 | }; 1836 | 1837 | const buildOffset = () => { 1838 | const offsetFragment = fragment({ 1839 | namespaceAlias: { a: namespaces.a }, 1840 | }) 1841 | .ele('@a', 'off') 1842 | .att('x', '0') 1843 | .att('y', '0') 1844 | .up(); 1845 | 1846 | return offsetFragment; 1847 | }; 1848 | 1849 | const buildGraphicFrameTransform = (attributes) => { 1850 | const graphicFrameTransformFragment = fragment({ 1851 | namespaceAlias: { a: namespaces.a }, 1852 | }).ele('@a', 'xfrm'); 1853 | 1854 | const offsetFragment = buildOffset(); 1855 | graphicFrameTransformFragment.import(offsetFragment); 1856 | const extentsFragment = buildExtents(attributes); 1857 | graphicFrameTransformFragment.import(extentsFragment); 1858 | 1859 | graphicFrameTransformFragment.up(); 1860 | 1861 | return graphicFrameTransformFragment; 1862 | }; 1863 | 1864 | const buildShapeProperties = (attributes) => { 1865 | const shapeProperties = fragment({ 1866 | namespaceAlias: { pic: namespaces.pic }, 1867 | }).ele('@pic', 'spPr'); 1868 | 1869 | const graphicFrameTransformFragment = buildGraphicFrameTransform(attributes); 1870 | shapeProperties.import(graphicFrameTransformFragment); 1871 | const presetGeometryFragment = buildPresetGeometry(); 1872 | shapeProperties.import(presetGeometryFragment); 1873 | 1874 | shapeProperties.up(); 1875 | 1876 | return shapeProperties; 1877 | }; 1878 | 1879 | const buildFillRect = () => { 1880 | const fillRectFragment = fragment({ 1881 | namespaceAlias: { a: namespaces.a }, 1882 | }) 1883 | .ele('@a', 'fillRect') 1884 | .up(); 1885 | 1886 | return fillRectFragment; 1887 | }; 1888 | 1889 | const buildStretch = () => { 1890 | const stretchFragment = fragment({ 1891 | namespaceAlias: { a: namespaces.a }, 1892 | }).ele('@a', 'stretch'); 1893 | 1894 | const fillRectFragment = buildFillRect(); 1895 | stretchFragment.import(fillRectFragment); 1896 | 1897 | stretchFragment.up(); 1898 | 1899 | return stretchFragment; 1900 | }; 1901 | 1902 | const buildSrcRectFragment = () => { 1903 | const srcRectFragment = fragment({ 1904 | namespaceAlias: { a: namespaces.a }, 1905 | }) 1906 | .ele('@a', 'srcRect') 1907 | .att('b', '0') 1908 | .att('l', '0') 1909 | .att('r', '0') 1910 | .att('t', '0') 1911 | .up(); 1912 | 1913 | return srcRectFragment; 1914 | }; 1915 | 1916 | const buildBinaryLargeImageOrPicture = (relationshipId) => { 1917 | const binaryLargeImageOrPictureFragment = fragment({ 1918 | namespaceAlias: { a: namespaces.a, r: namespaces.r }, 1919 | }) 1920 | .ele('@a', 'blip') 1921 | .att('@r', 'embed', `rId${relationshipId}`) 1922 | // FIXME: possible values 'email', 'none', 'print', 'hqprint', 'screen' 1923 | .att('cstate', 'print'); 1924 | 1925 | binaryLargeImageOrPictureFragment.up(); 1926 | 1927 | return binaryLargeImageOrPictureFragment; 1928 | }; 1929 | 1930 | const buildBinaryLargeImageOrPictureFill = (relationshipId) => { 1931 | const binaryLargeImageOrPictureFillFragment = fragment({ 1932 | namespaceAlias: { pic: namespaces.pic }, 1933 | }).ele('@pic', 'blipFill'); 1934 | const binaryLargeImageOrPictureFragment = buildBinaryLargeImageOrPicture(relationshipId); 1935 | binaryLargeImageOrPictureFillFragment.import(binaryLargeImageOrPictureFragment); 1936 | const srcRectFragment = buildSrcRectFragment(); 1937 | binaryLargeImageOrPictureFillFragment.import(srcRectFragment); 1938 | const stretchFragment = buildStretch(); 1939 | binaryLargeImageOrPictureFillFragment.import(stretchFragment); 1940 | 1941 | binaryLargeImageOrPictureFillFragment.up(); 1942 | 1943 | return binaryLargeImageOrPictureFillFragment; 1944 | }; 1945 | 1946 | const buildNonVisualPictureDrawingProperties = () => { 1947 | const nonVisualPictureDrawingPropertiesFragment = fragment({ 1948 | namespaceAlias: { pic: namespaces.pic }, 1949 | }).ele('@pic', 'cNvPicPr'); 1950 | 1951 | nonVisualPictureDrawingPropertiesFragment.up(); 1952 | 1953 | return nonVisualPictureDrawingPropertiesFragment; 1954 | }; 1955 | 1956 | const buildNonVisualDrawingProperties = ( 1957 | pictureId, 1958 | pictureNameWithExtension, 1959 | pictureDescription = '' 1960 | ) => { 1961 | const nonVisualDrawingPropertiesFragment = fragment({ 1962 | namespaceAlias: { pic: namespaces.pic }, 1963 | }) 1964 | .ele('@pic', 'cNvPr') 1965 | .att('id', pictureId) 1966 | .att('name', pictureNameWithExtension) 1967 | .att('descr', pictureDescription); 1968 | 1969 | nonVisualDrawingPropertiesFragment.up(); 1970 | 1971 | return nonVisualDrawingPropertiesFragment; 1972 | }; 1973 | 1974 | const buildNonVisualPictureProperties = ( 1975 | pictureId, 1976 | pictureNameWithExtension, 1977 | pictureDescription 1978 | ) => { 1979 | const nonVisualPicturePropertiesFragment = fragment({ 1980 | namespaceAlias: { pic: namespaces.pic }, 1981 | }).ele('@pic', 'nvPicPr'); 1982 | // TODO: Handle picture attributes 1983 | const nonVisualDrawingPropertiesFragment = buildNonVisualDrawingProperties( 1984 | pictureId, 1985 | pictureNameWithExtension, 1986 | pictureDescription 1987 | ); 1988 | nonVisualPicturePropertiesFragment.import(nonVisualDrawingPropertiesFragment); 1989 | const nonVisualPictureDrawingPropertiesFragment = buildNonVisualPictureDrawingProperties(); 1990 | nonVisualPicturePropertiesFragment.import(nonVisualPictureDrawingPropertiesFragment); 1991 | nonVisualPicturePropertiesFragment.up(); 1992 | 1993 | return nonVisualPicturePropertiesFragment; 1994 | }; 1995 | 1996 | const buildPicture = ({ 1997 | id, 1998 | fileNameWithExtension, 1999 | description, 2000 | relationshipId, 2001 | width, 2002 | height, 2003 | }) => { 2004 | const pictureFragment = fragment({ 2005 | namespaceAlias: { pic: namespaces.pic }, 2006 | }).ele('@pic', 'pic'); 2007 | const nonVisualPicturePropertiesFragment = buildNonVisualPictureProperties( 2008 | id, 2009 | fileNameWithExtension, 2010 | description 2011 | ); 2012 | pictureFragment.import(nonVisualPicturePropertiesFragment); 2013 | const binaryLargeImageOrPictureFill = buildBinaryLargeImageOrPictureFill(relationshipId); 2014 | pictureFragment.import(binaryLargeImageOrPictureFill); 2015 | const shapeProperties = buildShapeProperties({ width, height }); 2016 | pictureFragment.import(shapeProperties); 2017 | pictureFragment.up(); 2018 | 2019 | return pictureFragment; 2020 | }; 2021 | 2022 | const buildGraphicData = (graphicType, attributes) => { 2023 | const graphicDataFragment = fragment({ 2024 | namespaceAlias: { a: namespaces.a }, 2025 | }) 2026 | .ele('@a', 'graphicData') 2027 | .att('uri', 'http://schemas.openxmlformats.org/drawingml/2006/picture'); 2028 | if (graphicType === 'picture') { 2029 | const pictureFragment = buildPicture(attributes); 2030 | graphicDataFragment.import(pictureFragment); 2031 | } 2032 | graphicDataFragment.up(); 2033 | 2034 | return graphicDataFragment; 2035 | }; 2036 | 2037 | const buildGraphic = (graphicType, attributes) => { 2038 | const graphicFragment = fragment({ 2039 | namespaceAlias: { a: namespaces.a }, 2040 | }).ele('@a', 'graphic'); 2041 | // TODO: Handle drawing type 2042 | const graphicDataFragment = buildGraphicData(graphicType, attributes); 2043 | graphicFragment.import(graphicDataFragment); 2044 | graphicFragment.up(); 2045 | 2046 | return graphicFragment; 2047 | }; 2048 | 2049 | const buildDrawingObjectNonVisualProperties = (pictureId, pictureName) => { 2050 | const drawingObjectNonVisualPropertiesFragment = fragment({ 2051 | namespaceAlias: { wp: namespaces.wp }, 2052 | }) 2053 | .ele('@wp', 'docPr') 2054 | .att('id', pictureId) 2055 | .att('name', pictureName) 2056 | .up(); 2057 | 2058 | return drawingObjectNonVisualPropertiesFragment; 2059 | }; 2060 | 2061 | const buildWrapSquare = () => { 2062 | const wrapSquareFragment = fragment({ 2063 | namespaceAlias: { wp: namespaces.wp }, 2064 | }) 2065 | .ele('@wp', 'wrapSquare') 2066 | .att('wrapText', 'bothSides') 2067 | .att('distB', '228600') 2068 | .att('distT', '228600') 2069 | .att('distL', '228600') 2070 | .att('distR', '228600') 2071 | .up(); 2072 | 2073 | return wrapSquareFragment; 2074 | }; 2075 | 2076 | // eslint-disable-next-line no-unused-vars 2077 | const buildWrapNone = () => { 2078 | const wrapNoneFragment = fragment({ 2079 | namespaceAlias: { wp: namespaces.wp }, 2080 | }) 2081 | .ele('@wp', 'wrapNone') 2082 | .up(); 2083 | 2084 | return wrapNoneFragment; 2085 | }; 2086 | 2087 | const buildEffectExtentFragment = () => { 2088 | const effectExtentFragment = fragment({ 2089 | namespaceAlias: { wp: namespaces.wp }, 2090 | }) 2091 | .ele('@wp', 'effectExtent') 2092 | .att('b', '0') 2093 | .att('l', '0') 2094 | .att('r', '0') 2095 | .att('t', '0') 2096 | .up(); 2097 | 2098 | return effectExtentFragment; 2099 | }; 2100 | 2101 | const buildExtent = ({ width, height }) => { 2102 | const extentFragment = fragment({ 2103 | namespaceAlias: { wp: namespaces.wp }, 2104 | }) 2105 | .ele('@wp', 'extent') 2106 | .att('cx', width) 2107 | .att('cy', height) 2108 | .up(); 2109 | 2110 | return extentFragment; 2111 | }; 2112 | 2113 | const buildPositionV = () => { 2114 | const positionVFragment = fragment({ 2115 | namespaceAlias: { wp: namespaces.wp }, 2116 | }) 2117 | .ele('@wp', 'positionV') 2118 | .att('relativeFrom', 'paragraph') 2119 | .ele('@wp', 'posOffset') 2120 | .txt('19050') 2121 | .up() 2122 | .up(); 2123 | 2124 | return positionVFragment; 2125 | }; 2126 | 2127 | const buildPositionH = () => { 2128 | const positionHFragment = fragment({ 2129 | namespaceAlias: { wp: namespaces.wp }, 2130 | }) 2131 | .ele('@wp', 'positionH') 2132 | .att('relativeFrom', 'column') 2133 | .ele('@wp', 'posOffset') 2134 | .txt('19050') 2135 | .up() 2136 | .up(); 2137 | 2138 | return positionHFragment; 2139 | }; 2140 | 2141 | const buildSimplePos = () => { 2142 | const simplePosFragment = fragment({ 2143 | namespaceAlias: { wp: namespaces.wp }, 2144 | }) 2145 | .ele('@wp', 'simplePos') 2146 | .att('x', '0') 2147 | .att('y', '0') 2148 | .up(); 2149 | 2150 | return simplePosFragment; 2151 | }; 2152 | 2153 | const buildAnchoredDrawing = (graphicType, attributes) => { 2154 | const anchoredDrawingFragment = fragment({ 2155 | namespaceAlias: { wp: namespaces.wp }, 2156 | }) 2157 | .ele('@wp', 'anchor') 2158 | .att('distB', '0') 2159 | .att('distL', '0') 2160 | .att('distR', '0') 2161 | .att('distT', '0') 2162 | .att('relativeHeight', '0') 2163 | .att('behindDoc', 'false') 2164 | .att('locked', 'true') 2165 | .att('layoutInCell', 'true') 2166 | .att('allowOverlap', 'false') 2167 | .att('simplePos', 'false'); 2168 | // Even though simplePos isnt supported by Word 2007 simplePos is required. 2169 | const simplePosFragment = buildSimplePos(); 2170 | anchoredDrawingFragment.import(simplePosFragment); 2171 | const positionHFragment = buildPositionH(); 2172 | anchoredDrawingFragment.import(positionHFragment); 2173 | const positionVFragment = buildPositionV(); 2174 | anchoredDrawingFragment.import(positionVFragment); 2175 | const extentFragment = buildExtent({ width: attributes.width, height: attributes.height }); 2176 | anchoredDrawingFragment.import(extentFragment); 2177 | const effectExtentFragment = buildEffectExtentFragment(); 2178 | anchoredDrawingFragment.import(effectExtentFragment); 2179 | const wrapSquareFragment = buildWrapSquare(); 2180 | anchoredDrawingFragment.import(wrapSquareFragment); 2181 | const drawingObjectNonVisualPropertiesFragment = buildDrawingObjectNonVisualProperties( 2182 | attributes.id, 2183 | attributes.fileNameWithExtension 2184 | ); 2185 | anchoredDrawingFragment.import(drawingObjectNonVisualPropertiesFragment); 2186 | const graphicFragment = buildGraphic(graphicType, attributes); 2187 | anchoredDrawingFragment.import(graphicFragment); 2188 | 2189 | anchoredDrawingFragment.up(); 2190 | 2191 | return anchoredDrawingFragment; 2192 | }; 2193 | 2194 | const buildInlineDrawing = (graphicType, attributes) => { 2195 | const inlineDrawingFragment = fragment({ 2196 | namespaceAlias: { wp: namespaces.wp }, 2197 | }) 2198 | .ele('@wp', 'inline') 2199 | .att('distB', '0') 2200 | .att('distL', '0') 2201 | .att('distR', '0') 2202 | .att('distT', '0'); 2203 | 2204 | const extentFragment = buildExtent({ width: attributes.width, height: attributes.height }); 2205 | inlineDrawingFragment.import(extentFragment); 2206 | const effectExtentFragment = buildEffectExtentFragment(); 2207 | inlineDrawingFragment.import(effectExtentFragment); 2208 | const drawingObjectNonVisualPropertiesFragment = buildDrawingObjectNonVisualProperties( 2209 | attributes.id, 2210 | attributes.fileNameWithExtension 2211 | ); 2212 | inlineDrawingFragment.import(drawingObjectNonVisualPropertiesFragment); 2213 | const graphicFragment = buildGraphic(graphicType, attributes); 2214 | inlineDrawingFragment.import(graphicFragment); 2215 | 2216 | inlineDrawingFragment.up(); 2217 | 2218 | return inlineDrawingFragment; 2219 | }; 2220 | 2221 | const buildDrawing = (inlineOrAnchored = false, graphicType, attributes) => { 2222 | const drawingFragment = fragment({ 2223 | namespaceAlias: { w: namespaces.w }, 2224 | }).ele('@w', 'drawing'); 2225 | const inlineOrAnchoredDrawingFragment = inlineOrAnchored 2226 | ? buildInlineDrawing(graphicType, attributes) 2227 | : buildAnchoredDrawing(graphicType, attributes); 2228 | drawingFragment.import(inlineOrAnchoredDrawingFragment); 2229 | drawingFragment.up(); 2230 | 2231 | return drawingFragment; 2232 | }; 2233 | 2234 | export { 2235 | buildParagraph, 2236 | buildTable, 2237 | buildNumberingInstances, 2238 | buildLineBreak, 2239 | buildIndentation, 2240 | buildTextElement, 2241 | buildBold, 2242 | buildItalics, 2243 | buildUnderline, 2244 | buildDrawing, 2245 | fixupLineHeight, 2246 | }; 2247 | -------------------------------------------------------------------------------- /src/html-to-docx.js: -------------------------------------------------------------------------------- 1 | import { create } from 'xmlbuilder2'; 2 | import VNode from 'virtual-dom/vnode/vnode'; 3 | import VText from 'virtual-dom/vnode/vtext'; 4 | import * as HTMLToVDOM_ from 'html-to-vdom'; 5 | 6 | import { relsXML } from './schemas'; 7 | import DocxDocument from './docx-document'; 8 | import { renderDocumentFile } from './helpers'; 9 | import { 10 | pixelRegex, 11 | pixelToTWIP, 12 | cmRegex, 13 | cmToTWIP, 14 | inchRegex, 15 | inchToTWIP, 16 | pointRegex, 17 | pointToHIP, 18 | } from './utils/unit-conversion'; 19 | 20 | const HTMLToVDOM = HTMLToVDOM_; 21 | 22 | const convertHTML = HTMLToVDOM({ 23 | VNode, 24 | VText, 25 | }); 26 | 27 | const defaultDocumentOptions = { 28 | orientation: 'portrait', 29 | margins: { 30 | top: 1440, 31 | right: 1800, 32 | bottom: 1440, 33 | left: 1800, 34 | header: 720, 35 | footer: 720, 36 | gutter: 0, 37 | }, 38 | title: '', 39 | subject: '', 40 | creator: 'html-to-docx', 41 | keywords: ['html-to-docx'], 42 | description: '', 43 | lastModifiedBy: 'html-to-docx', 44 | revision: 1, 45 | createdAt: new Date(), 46 | modifiedAt: new Date(), 47 | headerType: 'default', 48 | header: false, 49 | footerType: 'default', 50 | footer: false, 51 | font: 'Times New Roman', 52 | fontSize: 22, 53 | complexScriptFontSize: 22, 54 | table: { 55 | row: { 56 | cantSplit: false, 57 | }, 58 | }, 59 | pageNumber: false, 60 | skipFirstHeaderFooter: false, 61 | lineNumber: false, 62 | lineNumberOptions: { 63 | countBy: 1, 64 | start: 0, 65 | restart: 'continuous', 66 | }, 67 | }; 68 | 69 | const mergeOptions = (options, patch) => ({ ...options, ...patch }); 70 | 71 | const fixupFontSize = (fontSize) => { 72 | let normalizedFontSize; 73 | if (pointRegex.test(fontSize)) { 74 | const matchedParts = fontSize.match(pointRegex); 75 | 76 | normalizedFontSize = pointToHIP(matchedParts[1]); 77 | } else if (fontSize) { 78 | // assuming it is already in HIP 79 | normalizedFontSize = fontSize; 80 | } else { 81 | normalizedFontSize = null; 82 | } 83 | 84 | return normalizedFontSize; 85 | }; 86 | 87 | const fixupMargins = (margins) => { 88 | let normalizedMargins = {}; 89 | if (typeof margins === 'object' && margins !== null) { 90 | Object.keys(margins).forEach((key) => { 91 | if (pixelRegex.test(margins[key])) { 92 | const matchedParts = margins[key].match(pixelRegex); 93 | normalizedMargins[key] = pixelToTWIP(matchedParts[1]); 94 | } else if (cmRegex.test(margins[key])) { 95 | const matchedParts = margins[key].match(cmRegex); 96 | normalizedMargins[key] = cmToTWIP(matchedParts[1]); 97 | } else if (inchRegex.test(margins[key])) { 98 | const matchedParts = margins[key].match(inchRegex); 99 | normalizedMargins[key] = inchToTWIP(matchedParts[1]); 100 | } else if (margins[key]) { 101 | normalizedMargins[key] = margins[key]; 102 | } else { 103 | // incase value is something like 0 104 | normalizedMargins[key] = defaultDocumentOptions.margins[key]; 105 | } 106 | }); 107 | } else { 108 | // eslint-disable-next-line no-param-reassign 109 | normalizedMargins = null; 110 | } 111 | 112 | return normalizedMargins; 113 | }; 114 | 115 | const normalizeDocumentOptions = (documentOptions) => { 116 | const normalizedDocumentOptions = { ...documentOptions }; 117 | Object.keys(documentOptions).forEach((key) => { 118 | // eslint-disable-next-line default-case 119 | switch (key) { 120 | case 'margins': 121 | normalizedDocumentOptions.margins = fixupMargins(documentOptions[key]); 122 | break; 123 | case 'fontSize': 124 | case 'complexScriptFontSize': 125 | normalizedDocumentOptions[key] = fixupFontSize(documentOptions[key]); 126 | break; 127 | } 128 | }); 129 | 130 | return normalizedDocumentOptions; 131 | }; 132 | 133 | // Ref: https://en.wikipedia.org/wiki/Office_Open_XML_file_formats 134 | // http://officeopenxml.com/anatomyofOOXML.php 135 | // eslint-disable-next-line import/prefer-default-export 136 | export function addFilesToContainer( 137 | zip, 138 | htmlString, 139 | suppliedDocumentOptions, 140 | headerHTMLString, 141 | footerHTMLString 142 | ) { 143 | const normalizedDocumentOptions = normalizeDocumentOptions(suppliedDocumentOptions); 144 | const documentOptions = mergeOptions(defaultDocumentOptions, normalizedDocumentOptions); 145 | 146 | if (documentOptions.header && !headerHTMLString) { 147 | // eslint-disable-next-line no-param-reassign 148 | headerHTMLString = '

'; 149 | } 150 | if (documentOptions.footer && !footerHTMLString) { 151 | // eslint-disable-next-line no-param-reassign 152 | footerHTMLString = '

'; 153 | } 154 | 155 | const docxDocument = new DocxDocument({ zip, htmlString, ...documentOptions }); 156 | // Conversion to Word XML happens here 157 | docxDocument.documentXML = renderDocumentFile(docxDocument); 158 | 159 | zip 160 | .folder('_rels') 161 | .file( 162 | '.rels', 163 | create({ encoding: 'UTF-8', standalone: true }, relsXML).toString({ prettyPrint: true }), 164 | { createFolders: false } 165 | ); 166 | 167 | zip.folder('docProps').file('core.xml', docxDocument.generateCoreXML(), { 168 | createFolders: false, 169 | }); 170 | 171 | if (docxDocument.header && headerHTMLString) { 172 | const vTree = convertHTML(headerHTMLString); 173 | 174 | docxDocument.relationshipFilename = 'header1'; 175 | const { headerId, headerXML } = docxDocument.generateHeaderXML(vTree); 176 | docxDocument.relationshipFilename = 'document'; 177 | 178 | const relationshipId = docxDocument.createDocumentRelationships( 179 | docxDocument.relationshipFilename, 180 | 'header', 181 | `header${headerId}.xml`, 182 | 'Internal' 183 | ); 184 | 185 | zip.folder('word').file(`header${headerId}.xml`, headerXML.toString({ prettyPrint: true }), { 186 | createFolders: false, 187 | }); 188 | 189 | docxDocument.headerObjects.push({ headerId, relationshipId, type: docxDocument.headerType }); 190 | } 191 | if (docxDocument.footer && footerHTMLString) { 192 | const vTree = convertHTML(footerHTMLString); 193 | 194 | docxDocument.relationshipFilename = 'footer1'; 195 | const { footerId, footerXML } = docxDocument.generateFooterXML(vTree); 196 | docxDocument.relationshipFilename = 'document'; 197 | 198 | const relationshipId = docxDocument.createDocumentRelationships( 199 | docxDocument.relationshipFilename, 200 | 'footer', 201 | `footer${footerId}.xml`, 202 | 'Internal' 203 | ); 204 | 205 | zip.folder('word').file(`footer${footerId}.xml`, footerXML.toString({ prettyPrint: true }), { 206 | createFolders: false, 207 | }); 208 | 209 | docxDocument.footerObjects.push({ footerId, relationshipId, type: docxDocument.footerType }); 210 | } 211 | 212 | docxDocument.createDocumentRelationships( 213 | docxDocument.relationshipFilename, 214 | 'theme', 215 | 'theme/theme1.xml', 216 | 'Internal' 217 | ); 218 | zip.folder('word').folder('theme').file('theme1.xml', docxDocument.generateThemeXML(), { 219 | createFolders: false, 220 | }); 221 | 222 | zip 223 | .folder('word') 224 | .file('document.xml', docxDocument.generateDocumentXML(), { 225 | createFolders: false, 226 | }) 227 | .file('fontTable.xml', docxDocument.generateFontTableXML(), { 228 | createFolders: false, 229 | }) 230 | .file('styles.xml', docxDocument.generateStylesXML(), { 231 | createFolders: false, 232 | }) 233 | .file('numbering.xml', docxDocument.generateNumberingXML(), { 234 | createFolders: false, 235 | }) 236 | .file('settings.xml', docxDocument.generateSettingsXML(), { 237 | createFolders: false, 238 | }) 239 | .file('webSettings.xml', docxDocument.generateWebSettingsXML(), { 240 | createFolders: false, 241 | }); 242 | 243 | const relationshipXMLs = docxDocument.generateRelsXML(); 244 | if (relationshipXMLs && Array.isArray(relationshipXMLs)) { 245 | relationshipXMLs.forEach(({ fileName, xmlString }) => { 246 | zip.folder('word').folder('_rels').file(`${fileName}.xml.rels`, xmlString, { 247 | createFolders: false, 248 | }); 249 | }); 250 | } 251 | 252 | zip.file('[Content_Types].xml', docxDocument.generateContentTypesXML(), { createFolders: false }); 253 | 254 | return zip; 255 | } 256 | -------------------------------------------------------------------------------- /src/schemas/content-types.js: -------------------------------------------------------------------------------- 1 | const contentTypesXML = ` 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | `; 21 | 22 | export default contentTypesXML; 23 | -------------------------------------------------------------------------------- /src/schemas/core.js: -------------------------------------------------------------------------------- 1 | import { namespaces } from '../helpers'; 2 | 3 | const generateCoreXML = ( 4 | title = '', 5 | subject = '', 6 | creator = 'html-to-docx', 7 | keywords = ['html-to-docx'], 8 | description = '', 9 | lastModifiedBy = 'html-to-docx', 10 | revision = 1, 11 | createdAt = new Date(), 12 | modifiedAt = new Date() 13 | ) => { 14 | return ` 15 | 16 | 17 | 24 | ${title} 25 | ${subject} 26 | ${creator} 27 | ${ 28 | keywords && Array.isArray(keywords) 29 | ? `${keywords.join(', ')}` 30 | : '' 31 | } 32 | ${description} 33 | ${lastModifiedBy} 34 | ${revision} 35 | ${ 36 | createdAt instanceof Date ? createdAt.toISOString() : new Date().toISOString() 37 | } 38 | ${ 39 | modifiedAt instanceof Date ? modifiedAt.toISOString() : new Date().toISOString() 40 | } 41 | 42 | `; 43 | }; 44 | 45 | export default generateCoreXML; 46 | -------------------------------------------------------------------------------- /src/schemas/document-rels.js: -------------------------------------------------------------------------------- 1 | import { namespaces } from '../helpers'; 2 | 3 | const documentRelsXML = ` 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | `; 13 | 14 | export default documentRelsXML; 15 | -------------------------------------------------------------------------------- /src/schemas/font-table.js: -------------------------------------------------------------------------------- 1 | import { namespaces } from '../helpers'; 2 | 3 | const fontTableXML = ` 4 | 5 | 6 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | `; 33 | 34 | export default fontTableXML; 35 | -------------------------------------------------------------------------------- /src/schemas/generic-rels.js: -------------------------------------------------------------------------------- 1 | const genericRelsXML = ` 2 | 3 | 4 | 5 | 6 | `; 7 | 8 | export default genericRelsXML; 9 | -------------------------------------------------------------------------------- /src/schemas/index.js: -------------------------------------------------------------------------------- 1 | export { default as contentTypesXML } from './content-types'; 2 | export { default as generateCoreXML } from './core'; 3 | export { default as documentRelsXML } from './document-rels'; 4 | export { default as relsXML } from './rels'; 5 | export { default as generateNumberingXMLTemplate } from './numbering'; 6 | export { default as generateStylesXML } from './styles'; 7 | export { default as fontTableXML } from './font-table'; 8 | export { default as generateThemeXML } from './theme'; 9 | export { default as settingsXML } from './settings'; 10 | export { default as webSettingsXML } from './web-settings'; 11 | export { default as genericRelsXML } from './generic-rels'; 12 | -------------------------------------------------------------------------------- /src/schemas/numbering.js: -------------------------------------------------------------------------------- 1 | import { namespaces } from '../helpers'; 2 | 3 | const generateNumberingXMLTemplate = () => { 4 | return ` 5 | 6 | 7 | 16 | 17 | `; 18 | }; 19 | 20 | export default generateNumberingXMLTemplate; 21 | -------------------------------------------------------------------------------- /src/schemas/rels.js: -------------------------------------------------------------------------------- 1 | import { namespaces } from '../helpers'; 2 | 3 | const relsXML = ` 4 | 5 | 6 | 7 | 8 | 9 | 10 | `; 11 | 12 | export default relsXML; 13 | -------------------------------------------------------------------------------- /src/schemas/settings.js: -------------------------------------------------------------------------------- 1 | import { namespaces } from '../helpers'; 2 | 3 | const settingsXML = ` 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | `; 13 | 14 | export default settingsXML; 15 | -------------------------------------------------------------------------------- /src/schemas/styles.js: -------------------------------------------------------------------------------- 1 | import { namespaces } from '../helpers'; 2 | 3 | const generateStylesXML = (font = 'Times New Roman', fontSize = 22, complexScriptFontSize = 22) => { 4 | return ` 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | `; 147 | }; 148 | 149 | export default generateStylesXML; 150 | -------------------------------------------------------------------------------- /src/schemas/theme.js: -------------------------------------------------------------------------------- 1 | const generateThemeXML = (font = 'Times New Roman') => ` 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | `; 196 | 197 | export default generateThemeXML; 198 | -------------------------------------------------------------------------------- /src/schemas/web-settings.js: -------------------------------------------------------------------------------- 1 | import { namespaces } from '../helpers'; 2 | 3 | const webSettingsXML = ` 4 | 5 | 6 | 7 | 8 | `; 9 | 10 | export default webSettingsXML; 11 | -------------------------------------------------------------------------------- /src/utils/color-conversion.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-param-reassign */ 2 | 3 | export const rgbRegex = /rgb\((\d+),\s*([\d.]+),\s*([\d.]+)\)/i; 4 | export const hslRegex = /hsl\((\d+),\s*([\d.]+)%,\s*([\d.]+)%\)/i; 5 | export const hexRegex = /#([0-9A-F]{6})/i; 6 | export const hex3Regex = /#([0-9A-F])([0-9A-F])([0-9A-F])/i; 7 | 8 | // eslint-disable-next-line import/prefer-default-export 9 | export const rgbToHex = (red, green, blue) => { 10 | const hexColorCode = [red, green, blue] 11 | .map((x) => { 12 | // eslint-disable-next-line radix, no-param-reassign 13 | x = parseInt(x).toString(16); 14 | return x.length === 1 ? `0${x}` : x; 15 | }) 16 | .join(''); 17 | 18 | return hexColorCode; 19 | }; 20 | 21 | export const hslToHex = (hue, saturation, luminosity) => { 22 | hue /= 360; 23 | saturation /= 100; 24 | luminosity /= 100; 25 | // eslint-disable-next-line one-var 26 | let red, green, blue; 27 | if (saturation === 0) { 28 | // eslint-disable-next-line no-multi-assign 29 | red = green = blue = luminosity; // achromatic 30 | } else { 31 | const hue2rgb = (p, q, t) => { 32 | if (t < 0) t += 1; 33 | if (t > 1) t -= 1; 34 | if (t < 1 / 6) return p + (q - p) * 6 * t; 35 | if (t < 1 / 2) return q; 36 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; 37 | return p; 38 | }; 39 | const q = 40 | luminosity < 0.5 41 | ? luminosity * (1 + saturation) 42 | : luminosity + saturation - luminosity * saturation; 43 | const p = 2 * luminosity - q; 44 | red = hue2rgb(p, q, hue + 1 / 3); 45 | green = hue2rgb(p, q, hue); 46 | blue = hue2rgb(p, q, hue - 1 / 3); 47 | } 48 | return [red, green, blue] 49 | .map((x) => { 50 | const hex = Math.round(x * 255).toString(16); 51 | return hex.length === 1 ? `0${hex}` : hex; 52 | }) 53 | .join(''); 54 | }; 55 | 56 | export const hex3ToHex = (red, green, blue) => { 57 | const hexColorCode = [red, green, blue].map((x) => `${x}${x}`).join(''); 58 | 59 | return hexColorCode; 60 | }; 61 | -------------------------------------------------------------------------------- /src/utils/unit-conversion.js: -------------------------------------------------------------------------------- 1 | export const pixelRegex = /([\d.]+)px/i; 2 | export const percentageRegex = /([\d.]+)%/i; 3 | export const pointRegex = /(\d+)pt/i; 4 | export const cmRegex = /([\d.]+)cm/i; 5 | export const inchRegex = /([\d.]+)in/i; 6 | 7 | export const pixelToEMU = (pixelValue) => { 8 | return Math.round(pixelValue * 9525); 9 | }; 10 | 11 | export const EMUToPixel = (EMUValue) => { 12 | return Math.round(EMUValue / 9525); 13 | }; 14 | 15 | export const TWIPToEMU = (TWIPValue) => { 16 | return Math.round(TWIPValue * 635); 17 | }; 18 | 19 | export const EMUToTWIP = (EMUValue) => { 20 | return Math.round(EMUValue / 635); 21 | }; 22 | 23 | export const pointToTWIP = (pointValue) => { 24 | return Math.round(pointValue * 20); 25 | }; 26 | 27 | export const TWIPToPoint = (TWIPValue) => { 28 | return Math.round(TWIPValue / 20); 29 | }; 30 | 31 | export const pointToHIP = (pointValue) => { 32 | return Math.round(pointValue * 2); 33 | }; 34 | 35 | export const HIPToPoint = (HIPValue) => { 36 | return Math.round(HIPValue / 2); 37 | }; 38 | 39 | export const HIPToTWIP = (HIPValue) => { 40 | return Math.round(HIPValue * 10); 41 | }; 42 | 43 | export const TWIPToHIP = (TWIPValue) => { 44 | return Math.round(TWIPValue / 10); 45 | }; 46 | 47 | export const pixelToTWIP = (pixelValue) => { 48 | return EMUToTWIP(pixelToEMU(pixelValue)); 49 | }; 50 | 51 | export const TWIPToPixel = (TWIPValue) => { 52 | return EMUToPixel(TWIPToEMU(TWIPValue)); 53 | }; 54 | 55 | export const pixelToHIP = (pixelValue) => { 56 | return TWIPToHIP(EMUToTWIP(pixelToEMU(pixelValue))); 57 | }; 58 | 59 | export const HIPToPixel = (HIPValue) => { 60 | return EMUToPixel(TWIPToEMU(HIPToTWIP(HIPValue))); 61 | }; 62 | 63 | export const inchToPoint = (inchValue) => { 64 | return Math.round(inchValue * 72); 65 | }; 66 | 67 | export const inchToTWIP = (inchValue) => { 68 | return pointToTWIP(inchToPoint(inchValue)); 69 | }; 70 | 71 | export const cmToInch = (cmValue) => { 72 | return cmValue * 0.3937008; 73 | }; 74 | 75 | export const cmToTWIP = (cmValue) => { 76 | return inchToTWIP(cmToInch(cmValue)); 77 | }; 78 | 79 | export const pixelToPoint = (pixelValue) => { 80 | return HIPToPoint(pixelToHIP(pixelValue)); 81 | }; 82 | 83 | export const pointToPixel = (pointValue) => { 84 | return HIPToPixel(pointToHIP(pointValue)); 85 | }; 86 | 87 | export const EIPToPoint = (EIPValue) => { 88 | return Math.round(EIPValue / 8); 89 | }; 90 | 91 | export const pointToEIP = (PointValue) => { 92 | return Math.round(PointValue * 8); 93 | }; 94 | 95 | export const pixelToEIP = (pixelValue) => { 96 | return pointToEIP(pixelToPoint(pixelValue)); 97 | }; 98 | 99 | export const EIPToPixel = (EIPValue) => { 100 | return pointToPixel(EIPToPoint(EIPValue)); 101 | }; 102 | -------------------------------------------------------------------------------- /template/document.template.js: -------------------------------------------------------------------------------- 1 | import { namespaces } from '../src/helpers'; 2 | 3 | const generateDocumentTemplate = (width, height, orientation, margins) => { 4 | return ` 5 | 6 | 7 | 21 | 22 | 23 | 24 | 31 | 32 | 33 | 34 | `; 35 | }; 36 | 37 | export default generateDocumentTemplate; 38 | --------------------------------------------------------------------------------