├── lib ├── wrapper.js ├── element_def.js ├── helpers │ ├── general.js │ └── dom.js ├── ElementsMap.js ├── ElementDef.js └── dom.js ├── samples ├── templates │ ├── footer.pdfml │ └── header.pdfml ├── pdfmake-samples │ ├── basics.pdfml │ └── tables.pdfml ├── image.pdfml ├── main.pdfml ├── pdfml-attrs.pdfml ├── br.pdfml ├── print-if.pdfml ├── simple.pdfml ├── server.js ├── table.pdfml └── tables.ejs ├── .gitignore ├── fonts ├── Roboto │ ├── Roboto-Black.ttf │ ├── Roboto-Bold.ttf │ ├── Roboto-Light.ttf │ ├── Roboto-Thin.ttf │ ├── Roboto-Italic.ttf │ ├── Roboto-Medium.ttf │ ├── Roboto-Regular.ttf │ ├── Roboto-BoldItalic.ttf │ ├── Roboto-ThinItalic.ttf │ ├── Roboto-BlackItalic.ttf │ ├── Roboto-LightItalic.ttf │ ├── Roboto-MediumItalic.ttf │ └── LICENSE.txt ├── Avenir │ ├── AvenirLTStd-Light.ttf │ └── AvenirLTStd-Roman.ttf └── Geogrotesque │ ├── Geogtq-Lg.ttf │ ├── Geogtq-Md.ttf │ └── Geogtq-Rg.ttf ├── package.json ├── test ├── general.js ├── image.js ├── pdfmake-samples.js ├── index.js └── tables.js ├── index.js └── readme.md /lib/wrapper.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /samples/templates/footer.pdfml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /samples/templates/header.pdfml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /fonts/Roboto/Roboto-Black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimonbrandsdorfer/pdfml/HEAD/fonts/Roboto/Roboto-Black.ttf -------------------------------------------------------------------------------- /fonts/Roboto/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimonbrandsdorfer/pdfml/HEAD/fonts/Roboto/Roboto-Bold.ttf -------------------------------------------------------------------------------- /fonts/Roboto/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimonbrandsdorfer/pdfml/HEAD/fonts/Roboto/Roboto-Light.ttf -------------------------------------------------------------------------------- /fonts/Roboto/Roboto-Thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimonbrandsdorfer/pdfml/HEAD/fonts/Roboto/Roboto-Thin.ttf -------------------------------------------------------------------------------- /fonts/Roboto/Roboto-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimonbrandsdorfer/pdfml/HEAD/fonts/Roboto/Roboto-Italic.ttf -------------------------------------------------------------------------------- /fonts/Roboto/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimonbrandsdorfer/pdfml/HEAD/fonts/Roboto/Roboto-Medium.ttf -------------------------------------------------------------------------------- /fonts/Roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimonbrandsdorfer/pdfml/HEAD/fonts/Roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /fonts/Avenir/AvenirLTStd-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimonbrandsdorfer/pdfml/HEAD/fonts/Avenir/AvenirLTStd-Light.ttf -------------------------------------------------------------------------------- /fonts/Avenir/AvenirLTStd-Roman.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimonbrandsdorfer/pdfml/HEAD/fonts/Avenir/AvenirLTStd-Roman.ttf -------------------------------------------------------------------------------- /fonts/Geogrotesque/Geogtq-Lg.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimonbrandsdorfer/pdfml/HEAD/fonts/Geogrotesque/Geogtq-Lg.ttf -------------------------------------------------------------------------------- /fonts/Geogrotesque/Geogtq-Md.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimonbrandsdorfer/pdfml/HEAD/fonts/Geogrotesque/Geogtq-Md.ttf -------------------------------------------------------------------------------- /fonts/Geogrotesque/Geogtq-Rg.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimonbrandsdorfer/pdfml/HEAD/fonts/Geogrotesque/Geogtq-Rg.ttf -------------------------------------------------------------------------------- /fonts/Roboto/Roboto-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimonbrandsdorfer/pdfml/HEAD/fonts/Roboto/Roboto-BoldItalic.ttf -------------------------------------------------------------------------------- /fonts/Roboto/Roboto-ThinItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimonbrandsdorfer/pdfml/HEAD/fonts/Roboto/Roboto-ThinItalic.ttf -------------------------------------------------------------------------------- /fonts/Roboto/Roboto-BlackItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimonbrandsdorfer/pdfml/HEAD/fonts/Roboto/Roboto-BlackItalic.ttf -------------------------------------------------------------------------------- /fonts/Roboto/Roboto-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimonbrandsdorfer/pdfml/HEAD/fonts/Roboto/Roboto-LightItalic.ttf -------------------------------------------------------------------------------- /fonts/Roboto/Roboto-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shimonbrandsdorfer/pdfml/HEAD/fonts/Roboto/Roboto-MediumItalic.ttf -------------------------------------------------------------------------------- /samples/pdfmake-samples/basics.pdfml: -------------------------------------------------------------------------------- 1 | 2 | 3 | First paragraph 4 | Another paragraph, this time a little bit longer to make sure, this line will be divided into at least two lines 5 | 6 | -------------------------------------------------------------------------------- /samples/image.pdfml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
Header
5 |
6 | 7 | 8 | 9 | 12 | 13 |
-------------------------------------------------------------------------------- /samples/main.pdfml: -------------------------------------------------------------------------------- 1 | <%- include('./templates/header.pdfml') %> 2 | 3 |
4 |

This is one block of text

5 |

This is another block of text

6 |
7 | 8 |

This is one block of text

9 |

This is another block of text

10 |
11 | 12 | <%- include('./templates/footer.pdfml') %> -------------------------------------------------------------------------------- /samples/pdfmake-samples/tables.pdfml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Column 1 7 | Column 2 8 | Column 3 9 | 10 | 11 |
12 | 13 |
-------------------------------------------------------------------------------- /samples/pdfml-attrs.pdfml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
Header
10 |
11 |
12 | 13 | 14 | 15 | 16 |
-------------------------------------------------------------------------------- /lib/element_def.js: -------------------------------------------------------------------------------- 1 | const ElementDef = require("./ElementDef"); 2 | const ElementsMap = require("./ElementsMap"); 3 | 4 | //keep a chache of element definition objects 5 | const elementsDefCache = {}; 6 | 7 | /** 8 | * 9 | * @param {String} elemName 10 | * @returns ElementDef 11 | */ 12 | module.exports = (elemName) => { 13 | if (elementsDefCache[elemName]) return elementsDefCache[elemName]; 14 | elementsDefCache[elemName] = new ElementDef(elemName, ElementsMap[elemName]); 15 | return elementsDefCache[elemName]; 16 | }; 17 | -------------------------------------------------------------------------------- /samples/br.pdfml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Header
11 |
12 |
13 | 14 | 15 | 16 | BreakLine 17 |
18 | 19 |
-------------------------------------------------------------------------------- /samples/print-if.pdfml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Header
11 |
12 |
13 | 14 | 15 | 16 | <%= text %> 17 | 18 |
-------------------------------------------------------------------------------- /samples/simple.pdfml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Header
11 |
12 |
13 | 14 | 15 | 16 | <%= text %> 17 | 18 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pdfml", 3 | "version": "2.0.0", 4 | "description": "A Markup Language for PDF files", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha", 8 | "preversion": "npm test" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/nutrition-power/pdfml.git" 13 | }, 14 | "keywords": [ 15 | "pdf", 16 | "markup", 17 | "ejs" 18 | ], 19 | "author": "shimonbrandsdorfer", 20 | "license": "ISC", 21 | "bugs": { 22 | "url": "https://github.com/nutrition-power/pdfml/issues" 23 | }, 24 | "homepage": "https://github.com/nutrition-power/pdfml#readme", 25 | "dependencies": { 26 | "ejs": "^3.1.6", 27 | "pdfmake": "^0.2.4", 28 | "xml2js": "^0.4.23" 29 | }, 30 | "devDependencies": { 31 | "chai": "^4.3.4", 32 | "express": "^4.18.2", 33 | "mocha": "^9.1.3" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /samples/server.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const pdfml = require("../index"); 3 | const Path = require("path"); 4 | const app = express(); 5 | 6 | 7 | const PORT = 3000; 8 | 9 | app.get("/:filename", async (req, res, next) => { 10 | try { 11 | //you can query some data here 12 | let doc = await pdfml.render({ 13 | path: Path.join(__dirname, req.params.filename), //path to your ejs file 14 | data: {}, // data for context in your ejs file, 15 | fonts: {}, //if you want to supply fonts 16 | }); 17 | //set the content type to pdf 18 | res.setHeader("Content-type", "application/pdf"); 19 | //set the attachment header 20 | //res.attachment("PDF_FILE.pdf"); 21 | res.send(doc); 22 | } catch (err) { 23 | next(err); 24 | } 25 | }); 26 | 27 | app.listen(PORT, () => { 28 | console.log(`Example app listening on port ${PORT}!`); 29 | }); 30 | -------------------------------------------------------------------------------- /samples/table.pdfml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Header
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 22 | 23 | 24 | 25 |
20 | Fromatted Cell 21 | Plain Text
26 | 27 |
28 | 29 | -------------------------------------------------------------------------------- /samples/tables.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
Header
11 |
12 |
13 | 14 | 15 | 16 | 17 | 18 | <% rows.forEach((row, rIdx) => { %> 19 | 20 | <% row.forEach((clm, cIdx) => { %> 21 | 22 | <%= clm %> 23 | 24 | <% }) %> 25 | 26 | <% }) %> 27 | 28 |
29 | 30 |
31 | 32 | -------------------------------------------------------------------------------- /lib/helpers/general.js: -------------------------------------------------------------------------------- 1 | 2 | 3 | const isString = (obj) => { 4 | return typeof obj === 'string' || obj instanceof String; 5 | } 6 | 7 | const isArray = (obj) => { 8 | return Array.isArray(obj); 9 | } 10 | 11 | const isUndefined = (obj) => { 12 | return typeof obj === 'undefined'; 13 | } 14 | 15 | const camelCase = (str) => { 16 | return str.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase(); }); 17 | } 18 | 19 | const isFunction = (obj) => { 20 | return typeof obj === 'function'; 21 | } 22 | 23 | const strToArr = (string = '', forceArray) => { 24 | if(forceArray) string = String(string); 25 | if(!isString(string)) return string; 26 | return string.trim().split(' '); 27 | } 28 | 29 | const cleanText = (text) => { 30 | return text.replace(/(\r\n|\n|\r)/gm, " ").trim(); 31 | } 32 | 33 | const fillMissing = (arr, fillWith, desiredLength) => { 34 | if(arr.length >= desiredLength) return arr; 35 | let filled = new Array(desiredLength - arr.length).fill(fillWith); 36 | return arr.concat(filled); 37 | } 38 | 39 | 40 | module.exports = { 41 | isString, 42 | isUndefined, 43 | camelCase, 44 | isFunction, 45 | strToArr, 46 | cleanText, 47 | isArray, 48 | fillMissing 49 | } -------------------------------------------------------------------------------- /test/general.js: -------------------------------------------------------------------------------- 1 | const { getDD , render} = require('../index'); 2 | const { expect } = require('chai'); 3 | 4 | describe('Support render string', () => { 5 | 6 | const example = `Hello World!`; 7 | let _DD; 8 | before(async () => { 9 | _DD = await getDD(example, {}); 10 | }); 11 | 12 | it(`allow "${example}" to work`, () => { 13 | expect(_DD).to.be.ok; 14 | }); 15 | 16 | it('Generates dd as expected', () => { 17 | expect(_DD.content[0].text).to.equal('Hello World!'); 18 | }); 19 | }); 20 | 21 | 22 | describe('Support self closing tags', () => { 23 | 24 | const example = `
`; 25 | let _DD; 26 | before(async () => { 27 | _DD = await getDD(example, {}); 28 | }); 29 | 30 | it(`allow "${example}" to work`, () => { 31 | expect(_DD).to.be.ok; 32 | }); 33 | 34 | it('Generates a new line', () => { 35 | expect(_DD.content[0].text).to.equal('\n'); 36 | }); 37 | }); 38 | 39 | describe('Empty columns should work', () => { 40 | const example = ``; 41 | let _DD; 42 | before(async () => { 43 | _DD = await getDD(example, {}); 44 | }); 45 | 46 | it('Renderes properly', async () => { 47 | let pdfDoc = await render({ 48 | str : example, 49 | }); 50 | expect(pdfDoc).to.be.ok; 51 | }); 52 | 53 | it(`allow "${example}" to work`, () => { 54 | expect(_DD).to.be.ok; 55 | }); 56 | 57 | 58 | it('Generates an ampty array', () => { 59 | expect(_DD.content[0].columns).to.deep.equal([]); 60 | }); 61 | }); -------------------------------------------------------------------------------- /lib/helpers/dom.js: -------------------------------------------------------------------------------- 1 | const { isString, strToArr, cleanText, isArray } = require("./general"); 2 | 3 | /** 4 | * 5 | * @param {*} attrs 6 | * @param {*} keyVals 7 | * @param {*} attrDefs 8 | * @returns 9 | */ 10 | function processAttrs(attrs, keyVals = {}, attrDefs = {}) { 11 | for (var prop in attrs) { 12 | //parse value 13 | let attrDef = attrDefs[prop]; 14 | 15 | attrs[prop] = parseAttr(attrs[prop], keyVals); 16 | 17 | attrs[prop] = arrayLike(attrs[prop], attrDef && attrDef.type === "array"); 18 | //convert number values to number 19 | if(!isArray(attrs[prop])) attrs[prop] = numberLike(attrs[prop]); 20 | } 21 | return attrs; 22 | } 23 | 24 | function arrayLike(val, forceArray = false) { 25 | let arr = strToArr(val, forceArray); 26 | if (arr && arr.length > 1 || forceArray){ 27 | return arr.map(numberLike); 28 | } 29 | return val; 30 | } 31 | 32 | function numberLike(val) { 33 | return !isNaN(val) ? Number(val) : val; 34 | } 35 | 36 | function parseAttr(val, keyVals) { 37 | if (!isString(val)) return val; 38 | 39 | var regex = /\${(\w*)}/g; 40 | 41 | let _val = val.replace(regex, (a, inner) => { 42 | return keyVals[inner]; 43 | }); 44 | 45 | try { 46 | return eval(_val); 47 | } catch (e) { 48 | return _val; 49 | } 50 | } 51 | 52 | function processTextElem(elem, keyVals, _elem) { 53 | if(isString(_elem)) _elem = processText(_elem); 54 | else _elem.text = processText(_elem.text, _elem.preserveNL); 55 | 56 | return _elem; 57 | } 58 | 59 | //helper function for to process text 60 | function processText( text, preserveNL ) { 61 | if (preserveNL) return text; 62 | if (!isString(text)) return text; 63 | return cleanText(text); 64 | } 65 | 66 | 67 | module.exports = { 68 | processAttrs, 69 | processTextElem 70 | }; 71 | -------------------------------------------------------------------------------- /test/image.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const { getDD , render} = require('../index'); 3 | const path = require('path'); 4 | 5 | const dataUri = `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVQYV2NgYAAAAAMAAWgmWQ0AAAAASUVORK5CYII=`; 6 | 7 | describe('Render "image.pdfml" from xml to Document-Definition', () => { 8 | let dd; 9 | before(async () => { 10 | dd = await getDD(path.join(__dirname, '../samples/image.pdfml'), { dataUri }); 11 | }); 12 | 13 | it('Expect Document-Definition to be generated without error', () => { 14 | expect(dd).to.be.ok; 15 | }); 16 | 17 | it('expect image to be ab object', () => { 18 | expect(dd.content[0]).to.be.an('object') 19 | }); 20 | 21 | it('expect image to have a property image equal to data-url', () => { 22 | expect(dd.content[0].image).to.equal(dataUri); 23 | }); 24 | 25 | it('Render file without error', async () => { 26 | let pdfDoc = await render({ 27 | path : path.join(__dirname, '../samples/image.pdfml'), 28 | data: {dataUri} 29 | }); 30 | expect(pdfDoc).to.be.ok; 31 | }); 32 | 33 | 34 | }); 35 | 36 | describe('image inside columns', () => { 37 | const example = ` 38 | 39 | 40 | 41 | 42 | 43 | `; 44 | 45 | let dd; 46 | before(async () => { 47 | dd = await getDD(example, { dataUri }); 48 | }); 49 | 50 | it('Expect Document-Definition to be generated without error', () => { 51 | expect(dd).to.be.ok; 52 | }); 53 | 54 | it('expect image data uri to match', () => { 55 | expect(dd.content[0].columns[0].image).to.equal(dataUri); 56 | }); 57 | }); -------------------------------------------------------------------------------- /test/pdfmake-samples.js: -------------------------------------------------------------------------------- 1 | //this is testing the samples provided in pdfmake webiste 2 | //http://pdfmake.org/playground.html 3 | 4 | const expect = require('chai').expect; 5 | const { getDD , render} = require('../index'); 6 | const path = require('path'); 7 | 8 | describe('basics example', () => { 9 | const DD = { 10 | content: [ 11 | { text : 'First paragraph'}, 12 | { text : 'Another paragraph, this time a little bit longer to make sure, this line will be divided into at least two lines'} 13 | ] 14 | 15 | }; 16 | 17 | it('Generated properly', async () => { 18 | let pdfDoc = await render({ 19 | path: path.join(__dirname, '../samples/pdfmake-samples/basics.pdfml'), 20 | data: { } 21 | }); 22 | expect(pdfDoc).to.be.ok; 23 | }); 24 | 25 | it('PDF Content matches', async () => { 26 | let _DD = await getDD(path.join(__dirname, '../samples/pdfmake-samples/basics.pdfml'), {}); 27 | expect(_DD.content).to.deep.equal(DD.content); 28 | }); 29 | 30 | }); 31 | 32 | 33 | describe('Simple Tables example', () => { 34 | const DD = { 35 | content : [ 36 | { 37 | style: 'tableExample', 38 | table: { 39 | body: [ 40 | ['Column 1', 'Column 2', 'Column 3'] 41 | ] 42 | } 43 | } 44 | ] 45 | }; 46 | 47 | 48 | it('Generated properly', async () => { 49 | let pdfDoc = await render({ 50 | path: path.join(__dirname, '../samples/pdfmake-samples/tables.pdfml'), 51 | data: { } 52 | }); 53 | expect(pdfDoc).to.be.ok; 54 | }); 55 | 56 | it('PDF Content matches', async () => { 57 | let _DD = await getDD(path.join(__dirname, '../samples/pdfmake-samples/tables.pdfml'), {}); 58 | expect(_DD.content).to.deep.equal(DD.content); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /lib/ElementsMap.js: -------------------------------------------------------------------------------- 1 | //this file wraps the pdfml elements to make them more intuitive to use 2 | const { processTextElem, fillMissing } = require("./helpers/general"); 3 | 4 | module.exports = { 5 | hr: { onProcess: horizontalLine }, 6 | row: { mergeAsArray: true }, 7 | cell: { mergeAsArray: true }, 8 | tr: { mergeAsArray: true }, 9 | td: { mergeAsArray: true, type: "object" }, 10 | table: { 11 | type: "object", 12 | onProcess: table, 13 | empty: { body: [[]] }, 14 | attrDefs: { widths: { type: "array", }, heights: {type : 'array'} }, 15 | }, 16 | tbody: { onProcess: tbody, pdfMakeName: "body", empty: [[]] }, 17 | image: { onProcess: image }, 18 | img: { pdfMakeName: "image", onProcess: image }, 19 | br: { onProcess: breakLine }, 20 | columns: { isArray: true, empty: [] }, 21 | div: { pdfMakeName: "stack" }, 22 | p: { pdfMakeName: "text", onProcess: processTextElem }, 23 | text: { onProcess: processTextElem }, 24 | }; 25 | 26 | function image(elem, keyVals, _elem) { 27 | _elem.image = _elem.src; 28 | delete _elem.src; 29 | return _elem; 30 | } 31 | 32 | function breakLine() { 33 | return { text: "\n", preserveNL: true }; 34 | } 35 | 36 | function horizontalLine(obj) { 37 | const _defs = { 38 | color: "black", 39 | h: 0.5, 40 | w: 500, 41 | x: 0, 42 | y: 0, 43 | }; 44 | obj = Object.assign(_defs, obj); 45 | obj.type = "rect"; 46 | 47 | return { 48 | canvas: [obj], 49 | }; 50 | } 51 | 52 | function table(elem, keyVals, _elem) { 53 | const numOfCols = _elem.table.body[0].length; 54 | 55 | //loop through all attributes from elem, and attach them to the table 56 | for (let key in elem.$attrs) { 57 | if (key !== "widths" && key !== "heights") { 58 | _elem[key] = elem.$attrs[key]; 59 | } 60 | 61 | //TODO: abastrct this to a general setting for elements of type object (some attributes are properties of the parent object while others are properties of the table object) 62 | //if the attribute is widths or heights, fill the missing values with auto and attach it on the table 63 | if (key === "widths") { 64 | _elem.table.widths = fillMissing(elem.$attrs.widths, "auto", numOfCols); 65 | } 66 | 67 | if (key === "heights") { 68 | _elem.table.heights =fillMissing(elem.$attrs.heights, "auto", numOfCols); 69 | } 70 | } 71 | 72 | 73 | return _elem; 74 | } 75 | 76 | function tbody(elem, keyVals, _elem) { 77 | //get max number of columns 78 | const maxCols = _elem.body.reduce((acc, curr) => { 79 | return Math.max(acc, curr.length); 80 | }, 0); 81 | 82 | //add empty cells to make sure all rows have the same number of columns 83 | _elem.body = _elem.body.map((row) => { 84 | return fillMissing(row, '', maxCols); 85 | }); 86 | return _elem; 87 | } 88 | -------------------------------------------------------------------------------- /lib/ElementDef.js: -------------------------------------------------------------------------------- 1 | const { isUndefined, isString } = require("./helpers/general"); 2 | const { processAttrs, processTextElem } = require("./helpers/dom"); 3 | 4 | module.exports = class ElementDef { 5 | constructor(elemName, settings = {}) { 6 | this.elemName = elemName; 7 | this.settings = settings; 8 | this.type = settings.type || "array"; 9 | } 10 | 11 | get pdfMakeName() { 12 | return this.settings.pdfMakeName || this.elemName; 13 | } 14 | 15 | onEmpty() { 16 | if (this.settings.empty) return this.settings.empty; 17 | return this.settings.isArray ? [] : ""; 18 | } 19 | 20 | onProcess(elem, keyVals, _elem) { 21 | if (this.settings.onProcess) 22 | return this.settings.onProcess(elem, keyVals, _elem); 23 | if(this.settings.mergeAsArray) { 24 | _elem = _elem ? _elem[this.pdfMakeName] : elem._; 25 | } 26 | return isString(_elem) ? processTextElem(elem, keyVals, _elem) : _elem; 27 | } 28 | 29 | /** 30 | * 31 | * @param {Object} elem 32 | * @param {Object} keyVals 33 | * @param {Object} _elem 34 | * @param {Function} processCB 35 | * @returns {Object} | returns the element object that will be used in the pdfmake document 36 | */ 37 | processChildren(elem, keyVals, _elem, processCB) { 38 | if(!elem.children) return _elem; 39 | let children = elem.children 40 | .map((child) => { 41 | return processCB(child, keyVals); 42 | }) 43 | .filter((x) => !!x); 44 | 45 | if (this.type === "array") { 46 | _elem[this.pdfMakeName] = children; 47 | } else if(this.type === "object") { 48 | _elem[this.pdfMakeName] = children.reduce((acc, curr) => { 49 | Object.assign(acc, curr); 50 | return acc; 51 | }, {}); 52 | } 53 | return _elem; 54 | } 55 | 56 | /** 57 | * 58 | * @param {Object} elem | takes the element object as parsed from the xml 59 | * @param {Object} keyVals 60 | * @returns {Object} | returns the element object that will be used in the pdfmake document 61 | */ 62 | processElement(elem, keyVals, processCB) { 63 | let _elem = {}; 64 | 65 | //use the name of the attribute for the character 66 | _elem[this.pdfMakeName] = elem._ || this.onEmpty(); 67 | if(elem.attrs) elem.$attrs = processAttrs(elem.attrs, keyVals, this.settings.attrDefs); 68 | 69 | //if printIf is false, return undefined - this will stop processing the element and all its children 70 | if (elem.$attrs && !isUndefined(elem.$attrs.printIf) && !elem.$attrs.printIf) return; 71 | 72 | 73 | 74 | _elem = this.processChildren(elem, keyVals, _elem, processCB); 75 | 76 | Object.assign(_elem, elem.$attrs); 77 | //handle custom defined elements (that are not defined with pdf-make) 78 | _elem = this.onProcess(elem, keyVals, _elem); 79 | 80 | return _elem; 81 | } 82 | }; 83 | -------------------------------------------------------------------------------- /lib/dom.js: -------------------------------------------------------------------------------- 1 | const {isString} = require('./helpers/general'); 2 | const getElementDef = require('./element_def'); 3 | 4 | const {processAttrs} = require('./helpers/dom'); 5 | 6 | module.exports = function processDOM(DOM, options = {defaultStyle : {}}) { 7 | if(!DOM) throw new Error('PDFML file is empty'); 8 | let keyVals = processKeyVals(DOM); 9 | let content = processBody(DOM, keyVals); 10 | let styles = processStyles(DOM, keyVals); 11 | let header = processHeader(DOM, keyVals); 12 | let footer = processFooter(DOM, keyVals); 13 | 14 | let dd = { 15 | content, 16 | defaultStyle : options.defaultStyle, 17 | styles, 18 | header, 19 | footer 20 | }; 21 | Object.assign(dd, processAttrs(DOM.attrs)); 22 | return dd; 23 | } 24 | 25 | function processBody(doc, keyVals) { 26 | let body = getDirectElementByPath(doc, "body"); 27 | 28 | body = getChildrenOfEl(body); 29 | return processContent(body, keyVals); 30 | } 31 | 32 | 33 | /** 34 | * This function can be replaced processEl 35 | * @param {Array} elements - An array of elements to be the content array 36 | * @param {Object} keyVals 37 | */ 38 | function processContent(elements, keyVals) { 39 | if (!elements) return []; 40 | 41 | let content = elements.map(elem => { 42 | return processEl(elem, keyVals); 43 | }) 44 | .filter(x => !!x); 45 | 46 | return content; 47 | } 48 | 49 | function processStyles(doc) { 50 | let styles = getDirectElementByPath(doc, "head.styles"); 51 | styles = getChildrenOfEl(styles); 52 | let _styles = {}; 53 | styles && 54 | styles.forEach(style => { 55 | _styles[style["#name"]] = processEl(style); 56 | }); 57 | return _styles; 58 | } 59 | 60 | function processKeyVals(doc) { 61 | let values = getDirectElementByPath(doc, "head.values"); 62 | values = values && values[0]; 63 | return values && values.attrs; 64 | } 65 | 66 | function processHeader(doc) { 67 | return (current_page, page_count, page_size) => { 68 | let header = getDirectElementByPath(doc, "head.header"); 69 | header = getChildrenOfEl(header); 70 | let page_width = page_size.width; 71 | let content = processContent(header, { 72 | current_page, 73 | page_count, 74 | page_width 75 | }); 76 | return content; 77 | }; 78 | } 79 | 80 | function processFooter(doc) { 81 | return (current_page, page_count) => { 82 | let footer = getDirectElementByPath(doc, "head.footer"); 83 | footer = getChildrenOfEl(footer); 84 | return processContent(footer, { current_page, page_count }); 85 | }; 86 | } 87 | 88 | /** 89 | * 90 | * @param {Object} parent 91 | * @param {String} path ex : 'head.header' 92 | * @returns the first object found with specified path 93 | */ 94 | function getDirectElementByPath(parent, path) { 95 | let props = path.split("."); 96 | let val = parent, 97 | elem; 98 | for (var i = 0; i < props.length; i++) { 99 | let prop = props[i]; 100 | elem = val[prop]; 101 | if (!elem) return; 102 | if (Array.isArray(elem)) val = elem[0]; 103 | else val = elem; 104 | } 105 | return elem; 106 | } 107 | 108 | function getChildrenOfEl(el) { 109 | if (Array.isArray(el)) el = el[0]; 110 | return el && el.children; 111 | } 112 | 113 | /** 114 | * 115 | * @param {Object} elem | element to be processed 116 | * @param {Object} keyVals 117 | * 118 | * @returns {Object} processed element as pdfmake object 119 | */ 120 | /** 121 | */ 122 | function processEl(elem, keyVals) { 123 | 124 | let elDef = getElementDef(elem["#name"]); 125 | 126 | let _elem = elDef.processElement(elem, keyVals, processEl); 127 | 128 | 129 | if (_elem && _elem.value) handleVars(_elem, keyVals); 130 | 131 | return _elem; 132 | } 133 | 134 | function handleVars(_elem, keyVals) { 135 | _elem.text = keyVals[_elem.value]; 136 | delete _elem.value; 137 | } 138 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | const { getDD , render} = require('../index'); 2 | const path = require('path'); 3 | const { expect } = require('chai'); 4 | 5 | describe('Render "simple.pdfml" from xml to Document-Definition', () => { 6 | let dd; 7 | before(async () => { 8 | dd = await getDD(path.join(__dirname, '../samples/simple.pdfml'), { text: 'TEXT' }); 9 | }); 10 | 11 | it('Expect Document-Definition to be generated without error', () => { 12 | expect(dd).to.be.ok; 13 | }); 14 | 15 | it('Returns a JS Object', () => { 16 | expect(dd).to.be.an('object'); 17 | }); 18 | 19 | it('The body text is "TEXT" (using the ejs context)', () => { 20 | expect(dd.content[0].text).to.equal('TEXT'); 21 | }); 22 | 23 | it('The body style is "npStyle"', () => { 24 | expect(dd.content[0].style).to.equal('npStyle'); 25 | }); 26 | }); 27 | 28 | describe('Render "pdfml-attrs.pdfml" from xml to Document-Definition', () => { 29 | let dd; 30 | before(async () => { 31 | dd = await getDD(path.join(__dirname, '../samples/pdfml-attrs.pdfml'), { text: 'TEXT' }); 32 | }); 33 | 34 | it('Expect Document-Definition to be generated without error', () => { 35 | expect(dd).to.be.ok; 36 | }); 37 | 38 | it('The page size is "LETTER"', () => { 39 | expect(dd.pageSize).to.equal('LETTER'); 40 | }); 41 | 42 | it('The page orientation is "landscape"', () => { 43 | expect(dd.pageOrientation).to.equal('landscape'); 44 | }); 45 | 46 | it('The page margins is = "25 140 24 30"', () => { 47 | expect(dd.pageMargins).to.deep.equal([ 25, 140, 24, 30 ]); 48 | }); 49 | }); 50 | 51 | 52 | describe('Render "print-if.pdfml" from xml to Document-Definition', () => { 53 | let ddFalse, ddTrue; 54 | before(async () => { 55 | ddFalse = await getDD(path.join(__dirname, '../samples/print-if.pdfml'), { value: 'false', text: 'TEXT' }); 56 | ddTrue = await getDD(path.join(__dirname, '../samples/print-if.pdfml'), { value: 'true', text: 'TEXT' }); 57 | }); 58 | 59 | it('Expect Document-Definition to be generated without error', () => { 60 | expect(ddFalse).to.be.ok; 61 | expect(ddTrue).to.be.ok; 62 | }); 63 | 64 | 65 | it('The body text is empty (when print-if value is false)', () => { 66 | expect(ddFalse.content[0]).to.be.undefined; 67 | }); 68 | 69 | it('The body text to be "TEXT: (when print-if value is true)', () => { 70 | expect(ddTrue.content[0].text).to.equal('TEXT'); 71 | }); 72 | }); 73 | 74 | describe('Render "br.pdfml" from xml to Document-Definition', () => { 75 | let dd; 76 | before(async () => { 77 | dd = await getDD(path.join(__dirname, '../samples/br.pdfml'), { }); 78 | }); 79 | 80 | it('Expect Document-Definition to be generated without error', () => { 81 | expect(dd).to.be.ok; 82 | }); 83 | }); 84 | 85 | describe('Genereate PDF with ejs file', () => { 86 | 87 | it('Generated properly', async () => { 88 | let pdfDoc = await render({ 89 | path: path.join(__dirname, '../samples/simple.pdfml'), 90 | data: { text: 'TEXT' } 91 | }); 92 | 93 | expect(pdfDoc).to.be.ok; 94 | }) 95 | }); 96 | 97 | 98 | describe('Genereate PDF using include functions and templates', () => { 99 | 100 | let dd; 101 | before(async () => { 102 | dd = await getDD(path.join(__dirname, '../samples/main.pdfml'), {}); 103 | }); 104 | 105 | it('Generated properly', async () => { 106 | let pdfDoc = await render({ 107 | path: path.join(__dirname, '../samples/main.pdfml'), 108 | data: { } 109 | }); 110 | expect(pdfDoc).to.be.ok; 111 | }); 112 | 113 | it('PDF Content exists', () => { 114 | expect(dd.content).to.be.ok; 115 | }); 116 | 117 | it('PDF Content at least one element', () => { 118 | expect(dd.content.length).to.be.greaterThan(0); 119 | }); 120 | }); 121 | 122 | 123 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const ejs = require("ejs"); 2 | const { parseString } = require("xml2js"); 3 | const path = require("path"); 4 | const { camelCase } = require("./lib/helpers/general"); 5 | 6 | const ProcessDom = require("./lib/dom"); 7 | const PdfPrinter = require("pdfmake"); 8 | 9 | const fonts = { 10 | Avenir: { 11 | normal: getFontPath("Avenir/AvenirLTStd-Light"), 12 | bold: getFontPath("Avenir/AvenirLTStd-Roman") 13 | }, 14 | Geo: { 15 | normal: getFontPath("Geogrotesque/Geogtq-Rg"), 16 | bold: getFontPath("Geogrotesque/Geogtq-Md"), 17 | italics: getFontPath("Geogrotesque/Geogtq-Lg") 18 | }, 19 | Roboto: { 20 | normal: getFontPath("Roboto/Roboto-Regular"), 21 | bold: getFontPath("Roboto/Roboto-Bold"), 22 | italics: getFontPath("Geogrotesque/Geogtq-Lg") 23 | } 24 | }; 25 | 26 | 27 | /** 28 | * 29 | * @param {String} filePath or String 30 | * @param {Object} data | for ejs context 31 | * @param {} options 32 | * @returns {Promise} 33 | */ 34 | module.exports = { 35 | generatePDF, 36 | render, 37 | getDD 38 | } 39 | 40 | /** 41 | * 42 | * @param {String} textPath | xml as string or path to file 43 | * @param {*} data 44 | * @param {isFile : Boolean, ejs: Object} options 45 | * @returns {docDefinition} 46 | */ 47 | async function getDD(textPath, data, options = { ejs: {} }) { 48 | let xml; 49 | if(textPath.startsWith(" { 69 | 70 | parseString(xml, options, (err, result) => { 71 | if (err) reject(err); 72 | else resolve(result); 73 | }) 74 | }); 75 | } 76 | 77 | function renderFile(filePath, data, options) { 78 | return new Promise((resolve, reject) => { 79 | 80 | ejs.renderFile(filePath, data, options, (err, str) => { 81 | if (err) reject(err); 82 | else resolve(str); 83 | }); 84 | }); 85 | } 86 | 87 | function renderString(text, data, options) { 88 | return new Promise((resolve, reject) => { 89 | try{ 90 | resolve(ejs.render(text, data, options)); 91 | } catch (err) { 92 | reject(err); 93 | } 94 | }); 95 | } 96 | 97 | /** 98 | * 99 | * @param {Object. |path, str} options 100 | */ 101 | async function render(options) { 102 | let docDefinition = await getDD( 103 | options.path || options.str, 104 | options.data, 105 | { 106 | ...options, 107 | isText: !!options.str 108 | }); 109 | return generatePDF(docDefinition, { fonts: options.fonts }); 110 | } 111 | 112 | /** 113 | * 114 | * @param {Object} docDefinition 115 | * @param {Object} options 116 | * @returns Promise 117 | */ 118 | function generatePDF(docDefinition, options) { 119 | return new Promise((resolve, reject) => { 120 | const printer = new PdfPrinter({ ...fonts, ...options.fonts }); 121 | const doc = printer.createPdfKitDocument(docDefinition); 122 | 123 | let chunks = []; 124 | 125 | doc.on("data", chunk => { 126 | chunks.push(chunk); 127 | }); 128 | 129 | doc.on("end", () => { 130 | const result = Buffer.concat(chunks); 131 | resolve(result); 132 | return Promise.resolve(); 133 | }); 134 | 135 | doc.on('error', reject) 136 | 137 | doc.end(); 138 | }); 139 | 140 | 141 | } 142 | 143 | function getFontPath(fileName) { 144 | return path.join(__dirname, "fonts", `${fileName}.ttf`); 145 | } -------------------------------------------------------------------------------- /test/tables.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const { getDD , render} = require('../index'); 3 | const path = require('path'); 4 | 5 | 6 | describe('Render "table.pdfml" from xml to Document-Definition', () => { 7 | let dd; 8 | before(async () => { 9 | dd = await getDD(path.join(__dirname, '../samples/table.pdfml'), { rows : [['A', 'B'], ['C', 'D']] }); 10 | }); 11 | 12 | it('Expect Document-Definition to be generated without error', () => { 13 | expect(dd).to.be.ok; 14 | }); 15 | 16 | it('Render file without error', async () => { 17 | let pdfDoc = await render({ 18 | path : path.join(__dirname, '../samples/table.pdfml'), 19 | data: {} 20 | }); 21 | expect(pdfDoc).to.be.ok; 22 | }); 23 | }); 24 | 25 | describe('Detect mismatch of columns # and fill in empty cells for the missing', () => { 26 | let dd; 27 | const example = ` 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 |

A

B
37 | C 38 |
42 | 43 |
`; 44 | before(async () => { 45 | dd = await getDD(example, {}); 46 | }); 47 | 48 | it('Generate Document-Definition without error', () => { 49 | expect(dd).to.be.ok; 50 | }); 51 | 52 | it('Render table without error', async () => { 53 | let pdfDoc = await render({ 54 | str : example, 55 | data: {} 56 | }); 57 | expect(pdfDoc).to.be.ok; 58 | }); 59 | 60 | it('Expect the table to have 2 rows', () => { 61 | expect(dd.content[0].table.body.length).to.equal(2); 62 | }); 63 | 64 | it('Expect first row to have 2 columns', () => { 65 | expect(dd.content[0].table.body[0].length).to.equal(2); 66 | }); 67 | 68 | it('Expect second row to have 2 columns', () => { 69 | expect(dd.content[0].table.body[1].length).to.equal(2); 70 | }); 71 | 72 | it('Expect first cell of first row to be {text: "A"}', () => { 73 | expect(dd.content[0].table.body[0][0]).to.deep.equal({text: "A"}); 74 | }); 75 | 76 | it('Expect first cell of second row to be "C"', () => { 77 | expect(dd.content[0].table.body[1][0]).to.equal('C'); 78 | }); 79 | 80 | it('Expect second cell of second row to be empty', () => { 81 | expect(dd.content[0].table.body[1][1]).to.equal(''); 82 | }); 83 | 84 | it('Expect table widths to be [14, auto]', () => { 85 | expect(dd.content[0].table.widths).to.deep.equal([14, 'auto']); 86 | }); 87 | 88 | it('Expect table heights to be [14, auto]', () => { 89 | expect(dd.content[0].table.heights).to.deep.equal([14, 'auto']); 90 | }); 91 | 92 | it('Expect table layout to be "noBorders"', () => { 93 | expect(dd.content[0].layout).to.equal('noBorders'); 94 | }); 95 | 96 | 97 | }); 98 | 99 | describe('Widths in array', () => { 100 | let dd; 101 | const example = ` 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 113 | 114 | 115 |

A

B
111 | C 112 |
116 | 117 |
`; 118 | before(async () => { 119 | dd = await getDD(example, {}); 120 | }); 121 | 122 | it('Generate Document-Definition without error', () => { 123 | expect(dd).to.be.ok; 124 | }); 125 | 126 | it('Render table without error', async () => { 127 | let pdfDoc = await render({ 128 | str : example, 129 | data: {} 130 | }); 131 | expect(pdfDoc).to.be.ok; 132 | }); 133 | 134 | it('Expect the table to have 2 rows', () => { 135 | expect(dd.content[0].table.body.length).to.equal(2); 136 | }); 137 | 138 | it('Expect first row to have 2 columns', () => { 139 | expect(dd.content[0].table.body[0].length).to.equal(2); 140 | }); 141 | 142 | it('Expect second row to have 2 columns', () => { 143 | expect(dd.content[0].table.body[1].length).to.equal(2); 144 | }); 145 | 146 | it('Expect first cell of first row to be {text: "A"}', () => { 147 | expect(dd.content[0].table.body[0][0]).to.deep.equal({text: "A"}); 148 | }); 149 | 150 | it('Expect first cell of second row to be "C"', () => { 151 | expect(dd.content[0].table.body[1][0]).to.equal('C'); 152 | }); 153 | 154 | it('Expect second cell of second row to be empty', () => { 155 | expect(dd.content[0].table.body[1][1]).to.equal(''); 156 | }); 157 | 158 | it('Expect table widths to be [95,95,95,95,95,95,95]', () => { 159 | expect(dd.content[0].table.widths).to.deep.equal([95,95,95,95,95,95,95]); 160 | }); 161 | 162 | it('Expect table heights to be [12,50,50,50,50,50,50]', () => { 163 | expect(dd.content[0].table.heights).to.deep.equal([12,50,50,50,50,50,50]); 164 | }); 165 | 166 | it('Expect table layout to be "noBorders"', () => { 167 | expect(dd.content[0].layout).to.equal('noBorders'); 168 | }); 169 | 170 | 171 | }); -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 |

2 | PDFML - PDF Markup Language 3 |

4 | 5 |

6 | Write dynamic PDF files using a markdown language similar to HTML, Using the EJS rendering engine. 7 |

8 | 9 |

10 | This project is built using pdfmake and ejs 11 |

12 | 13 |

14 | Additionally, this will improve and auto fix your document defintion, so you don't have to worry about the PDF file being broken. 15 |

16 | 17 | 18 | ## Table of Contents 19 | 20 | 21 | - [Table of Contents](#table-of-contents) 22 | - [Installation](#installation) 23 | - [Usage](#usage) 24 | - [EJS file](#ejs-file) 25 | - [Convert to DocDefinition Object](#convert-to-docdefinition-object) 26 | - [Get A Buffer](#get-a-buffer) 27 | - [Serve with express.js](#serve-with-express.js) 28 | - [Use Templates](#use-templates) 29 | - [Elements](#elements) 30 | - [PDFML](#pdfml) 31 | - [p](#p) 32 | - [div](#div) 33 | - [columns](#columns) 34 | - [Table](#table) 35 | - [Br](#br) 36 | - [Image](#image) 37 | - [hr](#hr) 38 | - [Features](#features) 39 | - [Conditional Elements](#conditional-elements) 40 | - [Fonts](#Fonts) 41 | 42 | 43 | ## Installation 44 | 45 | 46 | ```sh 47 | npm i pdfml --save 48 | ``` 49 | 50 | 51 | ## Usage 52 | ```js 53 | const pdfml = require('pdfml'); 54 | pdfml.render({ 55 | path : PATH_TO_FILE_NAME 56 | data : RENDER_DATA 57 | }); 58 | ``` 59 | 60 | ### EJS file 61 | Create an ejs file, for example: 62 | ``` 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 |
Header
72 |
73 |
74 | 75 | 76 | 77 | <%= text %> 78 | 79 |
80 | ``` 81 | 82 | 83 | ### Convert to DocDefinition Object 84 | ```js 85 | const pdfml = require('pdfml'); 86 | let dd = pdfml.getDD(STRING_OR_PATH_TO_FILE, RENDER_DATA, OPTIONS); 87 | ``` 88 | 89 | ### Get A Buffer 90 | ```js 91 | const pdfml = require('pdfml'); 92 | let buffer = await pdfml.generatePDF(docDefinition, OPTIONS); 93 | ``` 94 | 95 | ### Serve with express.js 96 | ```js 97 | const pdfml = require('pdfml'); 98 | 99 | ///in your express router 100 | app.get("/:filename", async (req, res, next) => { 101 | try { 102 | //you can query some data here 103 | let doc = await pdfml.render({ 104 | path: Path.join(__dirname, req.params.filename), //path to your ejs file 105 | data: {}, // data for context in your ejs file, 106 | fonts: {}, //if you want to supply fonts 107 | }); 108 | //set the content type to pdf 109 | res.setHeader("Content-type", "application/pdf"); 110 | //set the attachment header 111 | res.attachment("PDF_FILE.pdf"); // remove this if you want to open the pdf in the browser 112 | res.send(doc); 113 | } catch (err) { 114 | next(err); 115 | } 116 | }); 117 | ``` 118 | ### Use Templates 119 | Synce this is powered by ejs, you can use the include function and setup multiple templates. 120 | This is very useful when you have generic parts of your document that you want to reuse. 121 | 122 | Consider the following file structure: 123 | 124 | - templates/ 125 | - header.pdfml 126 | - footer.pdfml 127 | - main.pdfml 128 | 129 | 130 | You can then use the include function to include the header and footer in your main file: 131 | ```xml 132 | <%- include('./templates/header.pdfml') %> 133 | 134 | Main Content 135 | 136 | <%- include('./templates/footer.pdfml') %> 137 | ``` 138 | 139 | ## Elements 140 | 141 | ### PDFML 142 | 143 | PDFML is the root element for the PDF document, and there is where you define all document-level Settings. 144 | 145 | 146 | Using the attributes of the `````` element ([see example](#pdfml-attributes-example)) you can define document level settings, such as page size, orientation, margins, etc. 147 | 148 | #### PDFML Attributes Example 149 | ```xml 150 | 151 | 152 | ``` 153 | 154 | 155 | ### p 156 | (or text) 157 | A ```p``` element is for displaying a paragraph of text. 158 | 159 | ```xml 160 |

This is plain text

161 | ``` 162 | 163 | ### div 164 | (also known as ```stack```) 165 | A ```div``` will keep the inner elements together, the inner elemets will be diplayed in block mode. 166 | 167 | ```xml 168 |
169 | This is one block of text 170 | This is another block of text 171 |
172 | ``` 173 | 174 | ### columns 175 | ```columns``` will keep the inner elements together, the inner elemets will be diplayed inline mode. 176 | 177 | ```xml 178 | 179 | This is one block of text 180 | This is another block of text 181 | 182 | ``` 183 | ### table 184 | 185 | For tables use the the following elements (body, row, cell): 186 | 187 | Note: The table will be rendered in a single page, if you want to break the table in multiple pages use the ```dont-break-rows="true"``` attribute. 188 | 189 | Note: PDFML is resilent to a mismatched number of columns in each row, it will fill the missing columns with empty cells. It will also fill in the widths and/or the heights attribute with the default width (auto) for each missing column. 190 | 191 | ```xml 192 | 193 | ```xml 194 | 195 | 196 | <% rows.forEach((row, rIdx) => { %> 197 | 198 | <% row.forEach((clm, cIdx) => { %> 199 | 202 | <% }) %> 203 | 204 | <% }) %> 205 | 206 |
200 | <%= clm %> 201 |
207 | ``` 208 | 209 | 210 | ## Br 211 | Just to break a line 212 | ```xml 213 |
214 | ``` 215 | 216 | ## Image 217 | ```xml 218 | 219 | ``` 220 | 221 | ## hr 222 | ```xml 223 |
224 | ``` 225 | 226 | ## Features 227 | 228 | ### Conditional Elements 229 | 230 | Use the attribute ```print-if``` and pass a boolean value to include this elements (and it's children or not). 231 | 232 | Note - that the inner content is still rendered with ejs, so the variables need to be defined. 233 | 234 | ## Fonts 235 | 236 | Supported fonts are: 237 | - Avenir 238 | - Geo 239 | - Roboto 240 | -------------------------------------------------------------------------------- /fonts/Roboto/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | --------------------------------------------------------------------------------