html2pdfmake
Advanced tools
| const META = Symbol('__HTML2PDFMAKE'); | ||
| const NODE = 'NODE'; | ||
| const UID = 'UID'; | ||
| const END_WITH_NEWLINE = 'END_WITH_NEWLINE'; | ||
| const START_WITH_NEWLINE = 'START_WITH_NEW_LINE'; | ||
| const IS_NEWLINE = 'IS_NEWLINE'; | ||
| const START_WITH_WHITESPACE = 'START_WITH_WHITESPACE'; | ||
| const END_WITH_WHITESPACE = 'END_WITH_WHITESPACE'; | ||
| const IS_WHITESPACE = 'IS_WHITESPACE'; | ||
| const IS_INPUT = 'IS_INPUT'; | ||
| const MARGIN = 'MARGIN'; | ||
| const PADDING = 'PADDING'; | ||
| const POSITION = 'POSITION'; | ||
| const HANDLER = 'HANDLER'; | ||
| const PDFMAKE = 'PDFMAKE'; | ||
| const ITEMS = 'ITEMS'; // meta items | ||
| const STYLE = 'STYLE'; | ||
| const POS_TOP = 1; // CSS 0 | ||
| const POS_RIGHT = 2; // CSS 1 | ||
| const POS_BOTTOM = 3; // CSS 2 | ||
| const POS_LEFT = 0; // CSS 3 | ||
| const getPatterns = () => ({ | ||
| fill: { | ||
| boundingBox: [1, 1, 4, 4], | ||
| xStep: 1, | ||
| yStep: 1, | ||
| pattern: '1 w 0 1 m 4 5 l s 2 0 m 5 3 l s' | ||
| } | ||
| }); | ||
| class Context { | ||
| config; | ||
| styles; | ||
| images = {}; | ||
| constructor(config, styles) { | ||
| this.config = config; | ||
| this.styles = styles; | ||
| } | ||
| } | ||
| const globalStyles = () => ({ | ||
| ':root': { | ||
| 'font-size': '16px' | ||
| }, | ||
| h1: { | ||
| 'font-size': '32px', | ||
| 'margin-top': '21.44px', | ||
| 'margin-bottom': '21.44px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| h2: { | ||
| 'font-size': '24px', | ||
| 'margin-top': '19.92px', | ||
| 'margin-bottom': '19.92px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| h3: { | ||
| 'font-size': '18.72px', | ||
| 'margin-top': '18.72px', | ||
| 'margin-bottom': '18.72px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| h4: { | ||
| 'font-size': '16px', | ||
| 'margin-top': '21.28px', | ||
| 'margin-bottom': '21.28px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| h5: { | ||
| 'font-size': '13.28px', | ||
| 'margin-top': '22.17px', | ||
| 'margin-bottom': '22.17px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| h6: { | ||
| 'font-size': '10.72px', | ||
| 'margin-top': '24.97px', | ||
| 'margin-bottom': '24.97px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| b: { | ||
| 'font-weight': 'bold' | ||
| }, | ||
| strong: { | ||
| 'font-weight': 'bold' | ||
| }, | ||
| i: { | ||
| 'font-style': 'italic', | ||
| }, | ||
| em: { | ||
| 'font-style': 'italic' | ||
| }, | ||
| s: { | ||
| 'text-decoration': 'line-through' | ||
| }, | ||
| del: { | ||
| 'text-decoration': 'line-through' | ||
| }, | ||
| sub: { | ||
| 'font-size': '22px', | ||
| 'vertical-align': 'sub' | ||
| }, | ||
| small: { | ||
| 'font-size': '13px' | ||
| }, | ||
| u: { | ||
| 'text-decoration': 'underline' | ||
| }, | ||
| ul: { | ||
| 'margin-top': '16px', | ||
| 'margin-bottom': '16px', | ||
| 'padding-left': '20px' | ||
| }, | ||
| ol: { | ||
| 'margin-top': '16px', | ||
| 'margin-bottom': '16px', | ||
| 'padding-left': '20px' | ||
| }, | ||
| p: { | ||
| 'margin-top': '16px', | ||
| 'margin-bottom': '16px' | ||
| }, | ||
| table: { | ||
| border: 'none', | ||
| padding: '3px' | ||
| }, | ||
| td: { | ||
| border: 'none' | ||
| }, | ||
| tr: { | ||
| margin: '4px 0' | ||
| }, | ||
| th: { | ||
| 'font-weight': 'bold', | ||
| border: 'none', | ||
| 'text-align': 'center' | ||
| }, | ||
| a: { | ||
| color: '#0000ee', | ||
| 'text-decoration': 'underline' | ||
| }, | ||
| hr: { | ||
| 'border-top': '2px solid #9a9a9a', | ||
| 'border-bottom': '0', | ||
| 'border-left': '0px solid black', | ||
| 'border-right': '0', | ||
| margin: '8px 0' | ||
| } | ||
| }); | ||
| const defaultConfig = () => ({ | ||
| globalStyles: globalStyles(), | ||
| styles: {}, | ||
| collapseMargin: true, | ||
| collapseWhitespace: true, | ||
| }); | ||
| /** | ||
| * @description Method to check if an item is an object. Date and Function are considered | ||
| * an object, so if you need to exclude those, please update the method accordingly. | ||
| * @param item - The item that needs to be checked | ||
| * @return {Boolean} Whether or not @item is an object | ||
| */ | ||
| const isObject = (item) => { | ||
| return (item === Object(item) && !Array.isArray(item)); | ||
| }; | ||
| /** | ||
| * @description Method to perform a deep merge of objects | ||
| * @param {Object} target - The targeted object that needs to be merged with the supplied @sources | ||
| * @param {Array<Object>} sources - The source(s) that will be used to update the @target object | ||
| * @return {Object} The final merged object | ||
| */ | ||
| const merge = (target, ...sources) => { | ||
| // return the target if no sources passed | ||
| if (!sources.length) { | ||
| return target; | ||
| } | ||
| const result = target; | ||
| if (isObject(result)) { | ||
| for (let i = 0; i < sources.length; i += 1) { | ||
| if (isObject(sources[i])) { | ||
| const elm = sources[i]; | ||
| Object.keys(elm).forEach(key => { | ||
| if (isObject(elm[key])) { | ||
| if (!result[key] || !isObject(result[key])) { | ||
| result[key] = {}; | ||
| } | ||
| merge(result[key], elm[key]); | ||
| } | ||
| else { | ||
| result[key] = elm[key]; | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| return result; | ||
| }; | ||
| const isNotText = (item) => typeof item !== 'string'; | ||
| const isColgroup = (item) => item?.[META]?.[NODE]?.nodeName === 'COLGROUP'; | ||
| const isImage = (item) => 'image' in item; | ||
| const isTable = (item) => 'table' in item; | ||
| const isTextArray = (item) => !!item && typeof item !== 'string' && 'text' in item && Array.isArray(item.text); | ||
| const isTextSimple = (item) => typeof item !== 'string' && 'text' in item && typeof item.text === 'string'; | ||
| const isTextOrLeaf = (item) => 'text' in item || typeof item === 'string'; | ||
| const isList = (item) => 'ul' in item || 'ol' in item; | ||
| const isTdOrTh = (item) => item[META]?.[NODE] && (item[META]?.[NODE]?.nodeName === 'TD' || item[META]?.[NODE]?.nodeName === 'TH'); | ||
| const isHeadline = (item) => item[META]?.[NODE] && (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(item[META]?.[NODE]?.nodeName || '')); | ||
| const isElement = (el) => el.nodeType === 1; | ||
| const isNode = (el) => el.nodeType === 3 || el.nodeType === 8; | ||
| const isCollapsable = (item) => typeof item !== 'undefined' && typeof item !== 'string' && ('stack' in item || 'ul' in item || 'ol' in item) && 'margin' in item; | ||
| const getChildItems = (item) => { | ||
| if (typeof item === 'string') { | ||
| return []; | ||
| } | ||
| if ('stack' in item) { | ||
| return item.stack; | ||
| } | ||
| if ('text' in item && typeof item.text !== 'string') { | ||
| return item.text; | ||
| } | ||
| if ('table' in item) { | ||
| return item.table.body | ||
| .flatMap(tr => tr) | ||
| .filter(isNotText); | ||
| } | ||
| if ('ul' in item) { | ||
| return item.ul; | ||
| } | ||
| if ('ol' in item) { | ||
| return item.ol; | ||
| } | ||
| return []; | ||
| }; | ||
| const toUnit = (value, rootPt = 12) => { | ||
| // if it's just a number, then return it | ||
| if (typeof value === 'number') { | ||
| return isFinite(value) ? value : 0; | ||
| } | ||
| const val = Number(parseFloat(value)); | ||
| if (isNaN(val)) { | ||
| return 0; | ||
| } | ||
| const match = ('' + value).trim().match(/(pt|px|r?em|cm)$/); | ||
| if (!match) { | ||
| return val; | ||
| } | ||
| switch (match[1]) { | ||
| case 'em': | ||
| case 'rem': | ||
| return val * rootPt; | ||
| case 'px': | ||
| // 1px = 0.75 Point | ||
| return Number((val * 0.75).toFixed(2)); | ||
| case 'cm': | ||
| return Number((val * 28.34).toFixed(2)); | ||
| case 'mm': | ||
| return Number((val * 10 * 28.34).toFixed(2)); | ||
| default: | ||
| return val; | ||
| } | ||
| }; | ||
| const getUnitOrValue = (value) => typeof value === 'string' && (value.indexOf('%') > -1 || value.indexOf('auto') > -1) | ||
| ? value | ||
| : toUnit(value); | ||
| const toUnitOrValue = (value) => getUnitOrValue(value); | ||
| const toUnitsOrValues = (value) => value.map(v => getUnitOrValue(v)); | ||
| const expandValueToUnits = (value) => { | ||
| const values = toUnitsOrValues(value.split(' ') | ||
| .map(v => v.trim()) | ||
| .filter(v => v)); | ||
| if (values === null || !Array.isArray(values)) { | ||
| return null; | ||
| } | ||
| // values[0] = top | ||
| // values[1] = right | ||
| // values[2] = bottom | ||
| // values[3] = left | ||
| // pdfmake use left, top, right, bottom, || [horizontal, vertical] | ||
| // css use top, right, bottom, left | ||
| if (values.length === 1 && values[0] !== null) { | ||
| return [values[0], values[0], values[0], values[0]]; | ||
| } | ||
| else if (values.length === 2) { | ||
| // topbottom leftright | ||
| return [values[1], values[0], values[1], values[0]]; | ||
| } | ||
| else if (values.length === 3) { | ||
| // top bottom leftright | ||
| return [values[2], values[0], values[2], values[1]]; | ||
| } | ||
| else if (values.length === 4) { | ||
| return [values[3], values[0], values[1], values[2]]; | ||
| } | ||
| return null; | ||
| }; | ||
| const handleColumns = (item) => { | ||
| const childItems = getChildItems(item); | ||
| return { | ||
| columns: childItems | ||
| .flatMap((subItem) => { | ||
| if ('text' in subItem && Array.isArray(subItem.text)) { | ||
| return subItem.text | ||
| .filter(childItem => !childItem[META]?.[IS_WHITESPACE]) | ||
| .map(text => { | ||
| const width = toUnitOrValue(text[META]?.[STYLE]?.width || 'auto') || 'auto'; | ||
| return (typeof text === 'string') ? { | ||
| text, | ||
| width | ||
| } : { | ||
| ...text, | ||
| width | ||
| }; | ||
| }); | ||
| } | ||
| return { | ||
| stack: [].concat(subItem), | ||
| width: toUnitOrValue(subItem[META]?.[STYLE]?.width || 'auto') || 'auto' | ||
| }; | ||
| }), | ||
| columnGap: 'columnGap' in item ? item.columnGap : 0 | ||
| }; | ||
| }; | ||
| const handleImg = (image) => { | ||
| if (isImage(image) && typeof image.width === 'number' && typeof image.height === 'number') { | ||
| image.fit = [image.width, image.height]; | ||
| } | ||
| return image; | ||
| }; | ||
| const handleTable = (item) => { | ||
| if (isTable(item)) { | ||
| const bodyItem = item.table.body[0]?.[0]; | ||
| const tableItem = bodyItem && typeof bodyItem !== 'string' && 'table' in bodyItem ? bodyItem : null; | ||
| if (!tableItem) { | ||
| return item; | ||
| } | ||
| const innerTable = tableItem.table; | ||
| const colgroup = bodyItem[META]?.[ITEMS]?.colgroup; | ||
| if (colgroup && Array.isArray(colgroup)) { | ||
| innerTable.widths = innerTable.widths || []; | ||
| colgroup.forEach((col, i) => { | ||
| if (col[META]?.[STYLE]?.width && innerTable.widths) { | ||
| innerTable.widths[i] = getUnitOrValue(col[META]?.[STYLE]?.width || 'auto'); | ||
| } | ||
| }); | ||
| } | ||
| const trs = bodyItem[META]?.[ITEMS]?.trs; | ||
| if (Array.isArray(trs)) { | ||
| trs.forEach((tr, i) => { | ||
| if (tr[META]?.[STYLE]?.height && innerTable.heights) { | ||
| innerTable.heights[i] = getUnitOrValue(tr[META]?.[STYLE]?.height || 'auto'); | ||
| } | ||
| }); | ||
| } | ||
| const paddingsTopBottom = {}; | ||
| const paddingsLeftRight = {}; | ||
| innerTable.body | ||
| .forEach((row, trIndex) => { | ||
| row.forEach((column, tdIndex) => { | ||
| if (typeof column !== 'string') { | ||
| if (column[META]?.[PADDING]) { | ||
| paddingsTopBottom[trIndex] = paddingsTopBottom[trIndex] || [0, 0]; | ||
| paddingsLeftRight[tdIndex] = paddingsLeftRight[tdIndex] || [0, 0]; | ||
| paddingsTopBottom[trIndex] = [ | ||
| Math.max(paddingsTopBottom[trIndex][0], column[META]?.[PADDING]?.[POS_TOP] || 0), | ||
| Math.max(paddingsTopBottom[trIndex][1], column[META]?.[PADDING]?.[POS_BOTTOM] || 0) | ||
| ]; | ||
| paddingsLeftRight[tdIndex] = [ | ||
| Math.max(paddingsLeftRight[tdIndex][0], column[META]?.[PADDING]?.[POS_LEFT] || 0), | ||
| Math.max(paddingsLeftRight[tdIndex][1], column[META]?.[PADDING]?.[POS_RIGHT] || 0) | ||
| ]; | ||
| } | ||
| column.style = column.style || []; | ||
| column.style.push(tdIndex % 2 === 0 ? 'td:nth-child(even)' : 'td:nth-child(odd)'); | ||
| column.style.push(trIndex % 2 === 0 ? 'tr:nth-child(even)' : 'tr:nth-child(odd)'); | ||
| } | ||
| }); | ||
| }); | ||
| const tableLayout = {}; | ||
| const hasPaddingTopBottom = Object.keys(paddingsTopBottom).length > 0; | ||
| const hasPaddingLeftRight = Object.keys(paddingsLeftRight).length > 0; | ||
| if (hasPaddingTopBottom) { | ||
| tableLayout.paddingTop = (i) => { | ||
| if (paddingsTopBottom[i]) { | ||
| return paddingsTopBottom[i][0]; | ||
| } | ||
| return 0; | ||
| }; | ||
| tableLayout.paddingBottom = (i) => { | ||
| if (paddingsTopBottom[i]) { | ||
| return paddingsTopBottom[i][1]; | ||
| } | ||
| return 0; | ||
| }; | ||
| } | ||
| if (hasPaddingLeftRight) { | ||
| tableLayout.paddingRight = (i) => { | ||
| if (paddingsLeftRight[i]) { | ||
| return paddingsLeftRight[i][1]; | ||
| } | ||
| return 0; | ||
| }; | ||
| tableLayout.paddingLeft = (i) => { | ||
| if (paddingsLeftRight[i]) { | ||
| return paddingsLeftRight[i][0]; | ||
| } | ||
| return 0; | ||
| }; | ||
| } | ||
| if (hasPaddingLeftRight || hasPaddingTopBottom) { | ||
| tableItem.layout = tableLayout; | ||
| } | ||
| } | ||
| return item; | ||
| }; | ||
| const addTocItem = (item, tocStyle = {}) => { | ||
| if ('text' in item && typeof item.text === 'string') { | ||
| item.tocItem = true; | ||
| merge(item, tocStyle); | ||
| } | ||
| else if ('stack' in item) { | ||
| const text = item.stack.find(s => 'text' in s); | ||
| if (text && typeof text !== 'string') { | ||
| text.tocItem = true; | ||
| merge(text, tocStyle); | ||
| } | ||
| } | ||
| }; | ||
| const handleHeadlineToc = (item) => { | ||
| const tocStyle = {}; | ||
| if (item[META]?.[NODE]?.nodeName === 'H1') { | ||
| Object.assign(tocStyle, { | ||
| tocNumberStyle: { bold: true } | ||
| }); | ||
| } | ||
| else { | ||
| Object.assign(tocStyle, { | ||
| tocMargin: [10, 0, 0, 0] | ||
| }); | ||
| } | ||
| addTocItem(item, tocStyle); | ||
| return item; | ||
| }; | ||
| const handleItem = (item) => { | ||
| if (typeof item !== 'string' && item[META]?.[PDFMAKE]) { | ||
| merge(item, item[META]?.[PDFMAKE] || {}); | ||
| } | ||
| if (typeof item[META]?.[HANDLER] === 'function') { | ||
| return item[META]?.[HANDLER]?.(item) || null; | ||
| } | ||
| return item; | ||
| }; | ||
| let _uid = 0; | ||
| const getUniqueId = (item) => { | ||
| const meta = item[META] || {}; | ||
| const __uid = meta[UID]; | ||
| const el = item[META]?.[NODE]; | ||
| if (__uid) { | ||
| return __uid; | ||
| } | ||
| if (!el) { | ||
| return '#' + (_uid++); | ||
| } | ||
| if (isElement(el)) { | ||
| const id = el.getAttribute('id'); | ||
| if (id) { | ||
| return id; | ||
| } | ||
| } | ||
| const nodeName = el.nodeName.toLowerCase(); | ||
| // TODO add parent? Or name something else? | ||
| const uid = '#' + nodeName + '-' + (_uid++); | ||
| meta[UID] = uid; | ||
| item[META] = meta; | ||
| return uid; | ||
| }; | ||
| const attrToProps = (item) => { | ||
| const el = item[META]?.[NODE]; | ||
| if (!el || !('getAttribute' in el)) | ||
| return { [META]: { [STYLE]: {} } }; | ||
| const cssClass = el.getAttribute('class') || ''; | ||
| const cssClasses = [...new Set(cssClass.split(' ') | ||
| .filter((value) => value) | ||
| .map((value) => '.' + value.trim()))]; | ||
| const nodeName = el.nodeName.toLowerCase(); | ||
| const parentNodeName = el.parentNode ? el.parentNode.nodeName.toLowerCase() : null; | ||
| const styleNames = [ | ||
| nodeName, | ||
| ].concat(cssClasses); | ||
| if (cssClasses.length > 2) { | ||
| styleNames.push(cssClasses.join('')); // .a.b.c | ||
| } | ||
| if (parentNodeName) { | ||
| styleNames.push(parentNodeName + '>' + nodeName); | ||
| } | ||
| const uniqueId = getUniqueId(item); | ||
| styleNames.push(uniqueId); // Should be the last one | ||
| const props = { | ||
| [META]: { | ||
| [STYLE]: item[META]?.[STYLE] || {}, | ||
| ...(item[META] || {}) | ||
| }, | ||
| style: [...new Set((item.style || []).concat(styleNames))] | ||
| }; | ||
| for (let i = 0; i < el.attributes.length; i++) { | ||
| const name = el.attributes[i].name; | ||
| const value = el.getAttribute(name)?.trim() || null; | ||
| if (value == null) { | ||
| continue; | ||
| } | ||
| props[META][STYLE][name] = value; | ||
| switch (name) { | ||
| case 'rowspan': | ||
| props.rowSpan = parseInt(value, 10); | ||
| break; | ||
| case 'colspan': | ||
| props.colSpan = parseInt(value, 10); | ||
| break; | ||
| case 'value': | ||
| if (nodeName === 'li') { | ||
| props.counter = parseInt(value, 10); | ||
| } | ||
| break; | ||
| case 'start': // ol | ||
| if (nodeName === 'ol') { | ||
| props.start = parseInt(value, 10); | ||
| } | ||
| break; | ||
| case 'width': | ||
| if ('image' in item) { | ||
| props.width = toUnit(value); | ||
| } | ||
| break; | ||
| case 'height': | ||
| if ('image' in item) { | ||
| props.height = toUnit(value); | ||
| } | ||
| break; | ||
| case 'data-fit': | ||
| if (value === 'true') { | ||
| const width = el.getAttribute('width'); | ||
| const height = el.getAttribute('height'); | ||
| if (width && height) { | ||
| props.fit = [toUnit(width), toUnit(height)]; | ||
| } | ||
| } | ||
| break; | ||
| case 'data-toc-item': | ||
| if (value !== 'false') { | ||
| let toc = {}; | ||
| if (value) { | ||
| try { | ||
| toc = JSON.parse(value); | ||
| } | ||
| catch (e) { | ||
| console.warn('Not valid JSON format.', value); | ||
| } | ||
| } | ||
| props[META][HANDLER] = (item) => { | ||
| addTocItem(item, toc); | ||
| return item; | ||
| }; | ||
| } | ||
| break; | ||
| case 'data-pdfmake': | ||
| if (value) { | ||
| try { | ||
| props[META][PDFMAKE] = JSON.parse(value); | ||
| } | ||
| catch (e) { | ||
| console.warn('Not valid JSON format.', value); | ||
| } | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| return props; | ||
| }; | ||
| const inheritStyle = (styles) => { | ||
| // TODO what do we want to exclude ? | ||
| const pick = { | ||
| color: true, | ||
| 'font-family': true, | ||
| 'font-size': true, | ||
| 'font-weight': true, | ||
| 'font': true, | ||
| 'line-height': true, | ||
| 'list-style-type': true, | ||
| 'list-style': true, | ||
| 'text-align': true, | ||
| // TODO only if parent is text: [] | ||
| background: true, | ||
| 'font-style': true, | ||
| 'background-color': true, | ||
| 'font-feature-settings': true, | ||
| 'white-space': true, | ||
| 'vertical-align': true, | ||
| 'opacity': true, | ||
| 'text-decoration': true, | ||
| }; | ||
| return Object.keys(styles).reduce((p, c) => { | ||
| if (pick[c] || styles[c] === 'inherit') { | ||
| p[c] = styles[c]; | ||
| } | ||
| return p; | ||
| }, {}); | ||
| }; | ||
| const getBorderStyle = (value) => { | ||
| const border = value.split(' '); | ||
| const color = border[2] || 'black'; | ||
| const borderStyle = border[1] || 'solid'; | ||
| const width = toUnit(border[0]); | ||
| return { color, width, borderStyle }; | ||
| }; | ||
| const computeBorder = (item, props, directive, value) => { | ||
| const { color, width, borderStyle } = getBorderStyle(value); | ||
| const tdOrTh = isTdOrTh(item); | ||
| const setBorder = (index) => { | ||
| props.border = item.border || props.border || [false, false, false, false]; | ||
| props.borderColor = item.borderColor || props.borderColor || ['black', 'black', 'black', 'black']; | ||
| if (value === 'none') { | ||
| props.border[index] = false; | ||
| } | ||
| else { | ||
| props.border[index] = true; | ||
| props.borderColor[index] = color; | ||
| } | ||
| }; | ||
| switch (directive) { | ||
| case 'border': | ||
| if (isTable(item)) { | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| if (value === 'none') { | ||
| props.layout.hLineWidth = () => 0; | ||
| props.layout.vLineWidth = () => 0; | ||
| break; | ||
| } | ||
| props.layout.vLineColor = () => color; | ||
| props.layout.hLineColor = () => color; | ||
| props.layout.hLineWidth = (i, node) => (i === 0 || i === node.table.body.length) ? width : 0; | ||
| props.layout.vLineWidth = (i, node) => (i === 0 || i === node.table.widths?.length) ? width : 0; | ||
| if (borderStyle === 'dashed') { | ||
| props.layout.hLineStyle = () => ({ dash: { length: 2, space: 2 } }); | ||
| props.layout.vLineStyle = () => ({ dash: { length: 2, space: 2 } }); | ||
| } | ||
| } | ||
| else if (tdOrTh) { | ||
| setBorder(0); | ||
| setBorder(1); | ||
| setBorder(2); | ||
| setBorder(3); | ||
| } | ||
| break; | ||
| case 'border-bottom': | ||
| if (isTable(item)) { | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| const hLineWidth = props.layout.hLineWidth || (() => 0); | ||
| const hLineColor = props.layout.hLineColor || (() => 'black'); | ||
| props.layout.hLineWidth = (i, node) => (i === node.table.body.length) ? width : hLineWidth(i, node); | ||
| props.layout.hLineColor = (i, node) => (i === node.table.body.length) ? color : hLineColor(i, node); | ||
| } | ||
| else if (tdOrTh) { | ||
| setBorder(3); | ||
| } | ||
| break; | ||
| case 'border-top': | ||
| if (isTable(item)) { | ||
| const { color, width } = getBorderStyle(value); | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| const hLineWidth = props.layout.hLineWidth || (() => 1); | ||
| const hLineColor = props.layout.hLineColor || (() => 'black'); | ||
| props.layout.hLineWidth = (i, node) => (i === 0) ? width : hLineWidth(i, node); | ||
| props.layout.hLineColor = (i, node) => (i === 0) ? color : hLineColor(i, node); | ||
| } | ||
| else if (tdOrTh) { | ||
| setBorder(1); | ||
| } | ||
| break; | ||
| case 'border-right': | ||
| if (isTable(item)) { | ||
| const { color, width } = getBorderStyle(value); | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| const vLineWidth = props.layout.vLineWidth || (() => 1); | ||
| const vLineColor = props.layout.vLineColor || (() => 'black'); | ||
| props.layout.vLineWidth = (i, node) => (node.table.body.length === 1 ? i === node.table.body.length : i % node.table.body.length !== 0) ? width : vLineWidth(i, node); | ||
| props.layout.vLineColor = (i, node) => (node.table.body.length === 1 ? i === node.table.body.length : i % node.table.body.length !== 0) ? color : vLineColor(i, node); | ||
| } | ||
| else if (tdOrTh) { | ||
| setBorder(2); | ||
| } | ||
| break; | ||
| case 'border-left': | ||
| if (isTable(item)) { | ||
| const { color, width } = getBorderStyle(value); | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| const vLineWidth = props.layout.vLineWidth || (() => 1); | ||
| const vLineColor = props.layout.vLineColor || (() => 'black'); | ||
| props.layout.vLineWidth = (i, node) => (node.table.body.length === 1 ? i === 0 : i % node.table.body.length === 0) ? width : vLineWidth(i, node); | ||
| props.layout.vLineColor = (i, node) => (node.table.body.length === 1 ? i === 0 : i % node.table.body.length === 0) ? color : vLineColor(i, node); | ||
| } | ||
| else if (tdOrTh) { | ||
| setBorder(0); | ||
| } | ||
| break; | ||
| } | ||
| }; | ||
| const computeMargin = (itemProps, item, value, index) => { | ||
| const margin = itemProps[META][MARGIN] || item[META]?.[MARGIN] || [0, 0, 0, 0]; | ||
| margin[index] = value; | ||
| itemProps[META][MARGIN] = [...margin]; | ||
| itemProps.margin = margin; | ||
| const padding = itemProps[META][PADDING] || item[META]?.[PADDING] || [0, 0, 0, 0]; | ||
| const paddingValue = padding[index] || 0; | ||
| itemProps.margin[index] = value + paddingValue; | ||
| }; | ||
| const computePadding = (props, item, value, index) => { | ||
| if (isTable(item)) { | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| switch (index) { | ||
| case POS_LEFT: | ||
| props.layout.paddingLeft = () => toUnit(value); | ||
| break; | ||
| case POS_TOP: | ||
| props.layout.paddingTop = () => toUnit(value); | ||
| break; | ||
| case POS_RIGHT: | ||
| props.layout.paddingRight = () => toUnit(value); | ||
| break; | ||
| case POS_BOTTOM: | ||
| props.layout.paddingBottom = () => toUnit(value); | ||
| break; | ||
| default: | ||
| throw new Error('Unsupported index for padding: ' + index); | ||
| } | ||
| } | ||
| else { | ||
| const padding = props[META][PADDING] || item[META]?.[PADDING] || [0, 0, 0, 0]; | ||
| padding[index] = value; | ||
| props[META][PADDING] = [...padding]; | ||
| if (!isTdOrTh(item)) { | ||
| const margin = props[META][MARGIN] || item[META]?.[MARGIN] || [0, 0, 0, 0]; | ||
| props.margin = margin; | ||
| const marginValue = margin[index]; | ||
| props.margin[index] = value + marginValue; | ||
| } | ||
| } | ||
| }; | ||
| const styleToProps = (item, styles, parentStyles = {}) => { | ||
| const props = { | ||
| [META]: { | ||
| [STYLE]: {}, | ||
| ...(item[META] || {}), | ||
| } | ||
| }; | ||
| const meta = props[META]; | ||
| const image = isImage(item); | ||
| const table = isTable(item); | ||
| const text = isTextSimple(item); | ||
| const list = isList(item); | ||
| const rootFontSize = toUnit(parentStyles['font-size'] || '16px'); | ||
| if (isHeadline(item)) { | ||
| meta[HANDLER] = handleHeadlineToc; | ||
| } | ||
| Object.keys(styles).forEach((key) => { | ||
| const directive = key; | ||
| const value = ('' + styles[key]).trim(); | ||
| props[META][STYLE][directive] = value; | ||
| switch (directive) { | ||
| case 'padding': { | ||
| const paddings = expandValueToUnits(value); | ||
| if (table && paddings !== null) { | ||
| let layout = props.layout || item.layout || {}; | ||
| if (typeof layout === 'string') { | ||
| layout = {}; | ||
| } | ||
| layout.paddingLeft = () => Number(paddings[POS_LEFT]); | ||
| layout.paddingRight = () => Number(paddings[POS_RIGHT]); | ||
| layout.paddingTop = (i) => (i === 0) ? Number(paddings[POS_TOP]) : 0; | ||
| layout.paddingBottom = (i, node) => (i === node.table.body.length - 1) ? Number(paddings[POS_BOTTOM]) : 0; | ||
| props.layout = layout; | ||
| } | ||
| else if (paddings !== null) { | ||
| computePadding(props, item, Number(paddings[POS_TOP]), POS_TOP); | ||
| computePadding(props, item, Number(paddings[POS_LEFT]), POS_LEFT); | ||
| computePadding(props, item, Number(paddings[POS_RIGHT]), POS_RIGHT); | ||
| computePadding(props, item, Number(paddings[POS_BOTTOM]), POS_BOTTOM); | ||
| } | ||
| break; | ||
| } | ||
| case 'border': | ||
| case 'border-bottom': | ||
| case 'border-top': | ||
| case 'border-right': | ||
| case 'border-left': | ||
| computeBorder(item, props, directive, value); | ||
| break; | ||
| case 'font-size': { | ||
| props.fontSize = toUnit(value, rootFontSize); | ||
| break; | ||
| } | ||
| case 'line-height': | ||
| props.lineHeight = toUnit(value, rootFontSize); | ||
| break; | ||
| case 'letter-spacing': | ||
| props.characterSpacing = toUnit(value); | ||
| break; | ||
| case 'text-align': | ||
| props.alignment = value; | ||
| break; | ||
| case 'font-feature-settings': { | ||
| const settings = value.split(',').filter(s => s).map(s => s.replace(/['"]/g, '')); | ||
| const fontFeatures = item.fontFeatures || props.fontFeatures || []; | ||
| fontFeatures.push(...settings); | ||
| props.fontFeatures = fontFeatures; | ||
| break; | ||
| } | ||
| case 'font-weight': | ||
| switch (value) { | ||
| case 'bold': | ||
| props.bold = true; | ||
| break; | ||
| case 'normal': | ||
| props.bold = false; | ||
| break; | ||
| } | ||
| break; | ||
| case 'text-decoration': | ||
| switch (value) { | ||
| case 'underline': | ||
| props.decoration = 'underline'; | ||
| break; | ||
| case 'line-through': | ||
| props.decoration = 'lineThrough'; | ||
| break; | ||
| case 'overline': | ||
| props.decoration = 'overline'; | ||
| break; | ||
| } | ||
| break; | ||
| case 'text-decoration-color': | ||
| props.decorationColor = value; | ||
| break; | ||
| case 'text-decoration-style': | ||
| props.decorationStyle = value; | ||
| break; | ||
| case 'vertical-align': | ||
| if (value === 'sub') { | ||
| props.sub = true; | ||
| } | ||
| break; | ||
| case 'font-style': | ||
| switch (value) { | ||
| case 'italic': | ||
| props.italics = true; | ||
| break; | ||
| } | ||
| break; | ||
| case 'font-family': | ||
| props.font = value; | ||
| break; | ||
| case 'color': | ||
| props.color = value; | ||
| break; | ||
| case 'background': | ||
| case 'background-color': | ||
| if (table) { | ||
| let layout = item.layout || {}; | ||
| if (typeof layout === 'string') { | ||
| layout = {}; | ||
| } | ||
| layout.fillColor = () => value; | ||
| props.layout = layout; | ||
| } | ||
| else if (isTdOrTh(item)) { | ||
| props.fillColor = value; | ||
| } | ||
| else { | ||
| props.background = ['fill', value]; | ||
| } | ||
| break; | ||
| case 'margin': { | ||
| const margin = expandValueToUnits(value)?.map(value => typeof value === 'string' ? 0 : value); | ||
| if (margin) { | ||
| computeMargin(props, item, margin[POS_TOP], POS_TOP); | ||
| computeMargin(props, item, margin[POS_LEFT], POS_LEFT); | ||
| computeMargin(props, item, margin[POS_RIGHT], POS_RIGHT); | ||
| computeMargin(props, item, margin[POS_BOTTOM], POS_BOTTOM); | ||
| } | ||
| break; | ||
| } | ||
| case 'margin-left': | ||
| computeMargin(props, item, toUnit(value), POS_LEFT); | ||
| break; | ||
| case 'margin-top': | ||
| computeMargin(props, item, toUnit(value), POS_TOP); | ||
| break; | ||
| case 'margin-right': | ||
| computeMargin(props, item, toUnit(value), POS_RIGHT); | ||
| break; | ||
| case 'margin-bottom': | ||
| computeMargin(props, item, toUnit(value), POS_BOTTOM); | ||
| break; | ||
| case 'padding-left': | ||
| computePadding(props, item, toUnit(value), POS_LEFT); | ||
| break; | ||
| case 'padding-top': | ||
| computePadding(props, item, toUnit(value), POS_TOP); | ||
| break; | ||
| case 'padding-right': | ||
| computePadding(props, item, toUnit(value), POS_RIGHT); | ||
| break; | ||
| case 'padding-bottom': | ||
| computePadding(props, item, toUnit(value), POS_BOTTOM); | ||
| break; | ||
| case 'page-break-before': | ||
| if (value === 'always') { | ||
| props.pageBreak = 'before'; | ||
| } | ||
| break; | ||
| case 'page-break-after': | ||
| if (value === 'always') { | ||
| props.pageBreak = 'after'; | ||
| } | ||
| break; | ||
| case 'position': | ||
| if (value === 'absolute') { | ||
| meta[POSITION] = 'absolute'; | ||
| props.absolutePosition = {}; | ||
| } | ||
| else if (value === 'relative') { | ||
| meta[POSITION] = 'relative'; | ||
| props.relativePosition = {}; | ||
| } | ||
| break; | ||
| case 'left': | ||
| case 'top': | ||
| // TODO can be set before postion:absolute! | ||
| if (!props.absolutePosition && !props.relativePosition) { | ||
| console.error(directive + ' is set, but no absolute/relative position.'); | ||
| break; | ||
| } | ||
| if (props.absolutePosition) { | ||
| if (directive === 'left') { | ||
| props.absolutePosition.x = toUnit(value); | ||
| } | ||
| else if (directive === 'top') { | ||
| props.absolutePosition.y = toUnit(value); | ||
| } | ||
| } | ||
| else if (props.relativePosition) { | ||
| if (directive === 'left') { | ||
| props.relativePosition.x = toUnit(value); | ||
| } | ||
| else if (directive === 'top') { | ||
| props.relativePosition.y = toUnit(value); | ||
| } | ||
| } | ||
| else { | ||
| console.error(directive + ' is set, but no absolute/relative position.'); | ||
| break; | ||
| } | ||
| break; | ||
| case 'white-space': | ||
| if (value === 'pre' && meta[NODE]) { | ||
| if (text) { | ||
| props.text = meta[NODE]?.textContent || ''; | ||
| } | ||
| props.preserveLeadingSpaces = true; | ||
| } | ||
| break; | ||
| case 'display': | ||
| if (value === 'flex') { | ||
| props[META][HANDLER] = handleColumns; | ||
| } | ||
| else if (value === 'none') { | ||
| props[META][HANDLER] = () => null; | ||
| } | ||
| break; | ||
| case 'opacity': | ||
| props.opacity = Number(parseFloat(value)); | ||
| break; | ||
| case 'gap': | ||
| props.columnGap = toUnit(value); | ||
| break; | ||
| case 'list-style-type': | ||
| case 'list-style': | ||
| if (list) { | ||
| props.type = value; | ||
| } | ||
| else { | ||
| props.listType = value; | ||
| } | ||
| break; | ||
| case 'width': | ||
| if (table) { | ||
| if (value === '100%') { | ||
| item.table.widths = ['*']; | ||
| } | ||
| else { | ||
| const width = toUnitOrValue(value); | ||
| if (width !== null) { | ||
| item.table.widths = [width]; | ||
| } | ||
| } | ||
| } | ||
| else if (image) { | ||
| props.width = toUnit(value); | ||
| } | ||
| break; | ||
| case 'height': | ||
| if (image) { | ||
| props.height = toUnit(value); | ||
| } | ||
| break; | ||
| case 'max-height': | ||
| if (image) { | ||
| props.maxHeight = toUnit(value); | ||
| } | ||
| break; | ||
| case 'max-width': | ||
| if (image) { | ||
| props.maxWidth = toUnit(value); | ||
| } | ||
| break; | ||
| case 'min-height': | ||
| if (image) { | ||
| props.minHeight = toUnit(value); | ||
| } | ||
| break; | ||
| case 'min-width': | ||
| if (image) { | ||
| props.minWidth = toUnit(value); | ||
| } | ||
| break; | ||
| case 'object-fit': | ||
| if (value === 'contain' && image) { | ||
| meta[HANDLER] = handleImg; | ||
| } | ||
| break; | ||
| } | ||
| }); | ||
| return props; | ||
| }; | ||
| /** | ||
| * @param el DOM Element | ||
| */ | ||
| const getInlineStyles = (el) => ('getAttribute' in el ? el.getAttribute('style') || '' : '').split(';') | ||
| .map(style => style.trim().toLowerCase().split(':')) | ||
| .filter(style => style.length === 2) | ||
| .reduce((style, value) => { | ||
| style[value[0].trim()] = value[1].trim(); | ||
| return style; | ||
| }, {}); | ||
| const getDefaultStyles = (el, item, styles) => (item.style || []).concat(el.nodeName.toLowerCase()) | ||
| .filter((selector) => styles && styles[selector]) | ||
| .reduce((style, selector) => { | ||
| return { | ||
| ...style, | ||
| ...styles[selector] | ||
| }; | ||
| }, {}); | ||
| /** | ||
| * | ||
| * @param el DOM Element | ||
| * @param item | ||
| * @param styles additional styles | ||
| * @param parentStyles pick styles | ||
| */ | ||
| const computeProps = (el, item, styles, parentStyles = {}) => { | ||
| const defaultStyles = getDefaultStyles(el, item, styles); | ||
| const rootStyles = styles[':root'] || globalStyles()[':root']; | ||
| const inheritedStyles = inheritStyle(parentStyles); | ||
| const cssStyles = Object.assign({}, defaultStyles, inheritedStyles, getInlineStyles(el)); | ||
| const styleProps = styleToProps(item, cssStyles, Object.assign({}, rootStyles, inheritedStyles)); | ||
| const attrProps = attrToProps(item); | ||
| const props = { | ||
| ...styleProps, | ||
| ...attrProps, | ||
| [META]: { | ||
| ...(styleProps[META] || {}), | ||
| ...(attrProps[META] || {}), | ||
| [STYLE]: { | ||
| ...(styleProps[META][STYLE] || {}), | ||
| ...(attrProps[META][STYLE] || {}), | ||
| } | ||
| } | ||
| }; | ||
| return { | ||
| cssStyles, | ||
| props | ||
| }; | ||
| }; | ||
| const collapseMargin = (item, prevItem) => { | ||
| if (isCollapsable(item) && isCollapsable(prevItem)) { | ||
| const prevMargin = prevItem[META]?.[MARGIN] || [0, 0, 0, 0]; | ||
| prevItem[META] = { ...(prevItem[META] || {}), [MARGIN]: prevMargin }; | ||
| prevItem.margin[POS_BOTTOM] = prevItem[META]?.[PADDING]?.[POS_BOTTOM] || 0; | ||
| const itemMargin = item[META]?.[MARGIN] || [0, 0, 0, 0]; | ||
| const marginTop = Math.max(itemMargin[POS_TOP], prevMargin[POS_BOTTOM]); | ||
| itemMargin[POS_TOP] = marginTop; | ||
| prevMargin[POS_BOTTOM] = 0; | ||
| item[META] = { ...(item[META] || {}), [MARGIN]: itemMargin }; | ||
| item.margin[POS_TOP] = marginTop + (item[META]?.[PADDING]?.[POS_TOP] || 0); | ||
| } | ||
| }; | ||
| const findLastDeep = (ta) => { | ||
| const last = ta.text.at(-1); | ||
| if (isTextArray(last)) { | ||
| return findLastDeep(last); | ||
| } | ||
| return last; | ||
| }; | ||
| const findFirstArrayDeep = (ta) => { | ||
| const first = ta.text.at(0); | ||
| if (isTextArray(first)) { | ||
| return findFirstArrayDeep(first); | ||
| } | ||
| return ta.text; | ||
| }; | ||
| const collapseWhitespace = (item, nextText) => { | ||
| const prevLastText = findLastDeep(item); | ||
| const nextFirstTextArray = findFirstArrayDeep(nextText); | ||
| if (prevLastText && prevLastText[META]?.[IS_WHITESPACE] && nextFirstTextArray[0][META]?.[IS_WHITESPACE]) { | ||
| nextFirstTextArray.shift(); | ||
| } | ||
| }; | ||
| function isBase64(str) { | ||
| return /^data:image\/(jpeg|png|jpg);base64,/.test(str); | ||
| } | ||
| const parseImg = (el, ctx) => { | ||
| const src = el.getAttribute('src'); | ||
| if (!src) { | ||
| return null; | ||
| } | ||
| const name = el.getAttribute('name') || src; | ||
| let image; | ||
| if (isBase64(src)) { | ||
| image = src; | ||
| } | ||
| else if (ctx.images[name]) { | ||
| image = name; | ||
| } | ||
| else { | ||
| ctx.images[src] = name; | ||
| image = name; | ||
| } | ||
| return { | ||
| image, | ||
| [META]: {} | ||
| }; | ||
| }; | ||
| const parseSvg = (el) => { | ||
| // TODO is this okay? | ||
| const svgEl = el.cloneNode(true); | ||
| const width = el.getAttribute('width'); | ||
| const height = el.getAttribute('height'); | ||
| if (width) { | ||
| svgEl.setAttribute('width', '' + getUnitOrValue(width)); | ||
| } | ||
| if (height) { | ||
| svgEl.setAttribute('height', '' + getUnitOrValue(height)); | ||
| } | ||
| return { | ||
| svg: svgEl.outerHTML.replace(/\n(\s+)?/g, ''), | ||
| }; | ||
| }; | ||
| const parseTable = () => { | ||
| // TODO table in table? | ||
| return { | ||
| table: { | ||
| body: (items) => { | ||
| // tbody -> tr | ||
| const colgroup = items.find(isColgroup)?.stack[0] || []; | ||
| const tbody = items.filter(item => !isColgroup(item)); | ||
| const trs = tbody.flatMap((item) => 'stack' in item ? item.stack : []); | ||
| const body = trs.map((item) => getChildItems(item)); | ||
| if (body.length === 0) { | ||
| return []; | ||
| } | ||
| const longestRow = body.reduce((a, b) => a.length <= b.length ? b : a); | ||
| const table = { | ||
| body, | ||
| widths: new Array(longestRow.length).fill('auto'), | ||
| heights: new Array(trs.length).fill('auto') | ||
| }; | ||
| return [[{ | ||
| table, | ||
| layout: { | ||
| defaultBorder: false | ||
| }, | ||
| [META]: { | ||
| [ITEMS]: { | ||
| colgroup: 'text' in colgroup && Array.isArray(colgroup.text) ? colgroup.text : [], | ||
| trs | ||
| }, | ||
| } | ||
| }]]; | ||
| }, | ||
| // widths: ['*'], | ||
| }, | ||
| [META]: { | ||
| [HANDLER]: handleTable, | ||
| }, | ||
| layout: {} | ||
| }; | ||
| }; | ||
| const parseText = (el) => { | ||
| const text = el.textContent; | ||
| if (text === null) { | ||
| return null; | ||
| } | ||
| const keepNewLines = text.replace(/[^\S\r\n]+/, ''); | ||
| const trimmedText = text.replace(/\n|\t| +/g, ' ') | ||
| .replace(/^ +/, '') | ||
| .replace(/ +$/, ''); | ||
| //.trim() removes also | ||
| const endWithNL = keepNewLines[keepNewLines.length - 1] === '\n'; | ||
| const startWithNL = keepNewLines[0] === '\n'; | ||
| const startWithWhitespace = text[0] === ' '; | ||
| const endWithWhitespace = text[text.length - 1] === ' '; | ||
| return { | ||
| text: trimmedText, | ||
| [META]: { | ||
| [START_WITH_NEWLINE]: startWithNL, | ||
| [END_WITH_NEWLINE]: endWithNL, | ||
| [IS_NEWLINE]: startWithNL && endWithNL && trimmedText.length === 0, | ||
| [START_WITH_WHITESPACE]: startWithWhitespace, | ||
| [END_WITH_WHITESPACE]: endWithWhitespace, | ||
| [IS_WHITESPACE]: startWithWhitespace && endWithWhitespace && text.length === 1, | ||
| }, | ||
| }; | ||
| }; | ||
| const WHITESPACE = ' '; | ||
| const addWhitespace = (type) => ({ | ||
| text: WHITESPACE, | ||
| [META]: { | ||
| [IS_WHITESPACE]: type | ||
| } | ||
| }); | ||
| const parseAsHTMLCollection = (el) => ['TABLE', 'TBODY', 'TR', 'COLGROUP', 'COL', 'UL', 'OL', 'SELECT'].includes(el.nodeName) && 'children' in el; | ||
| const stackRegex = /^(address|blockquote|body|center|colgroup|dir|div|dl|fieldset|form|h[1-6]|hr|isindex|menu|noframes|noscript|ol|p|pre|table|ul|dd|dt|frameset|li|tbody|td|tfoot|th|thead|tr|html)$/i; | ||
| const isStackItem = (el) => stackRegex.test(el.nodeName); | ||
| const parseChildren = (el, ctx, parentStyles = {}) => { | ||
| const items = []; | ||
| const children = parseAsHTMLCollection(el) ? el.children : el.childNodes; | ||
| for (let i = 0; i < children.length; i++) { | ||
| const item = parseByRule(children[i], ctx, parentStyles); | ||
| if (item === null) { | ||
| continue; | ||
| } | ||
| const isNewline = !!item[META]?.[IS_NEWLINE]; | ||
| const prevItem = items[items.length - 1]; | ||
| if (ctx.config.collapseMargin && prevItem) { | ||
| collapseMargin(item, prevItem); | ||
| } | ||
| if (isNewline && (items.length === 0 || !children[i + 1] || prevItem && 'stack' in prevItem)) { | ||
| continue; | ||
| } | ||
| // Stack item | ||
| if (!('text' in item)) { | ||
| items.push(item); | ||
| continue; | ||
| } | ||
| const endWithNewLine = !!item[META]?.[END_WITH_NEWLINE]; | ||
| const startWithNewLine = !!item[META]?.[START_WITH_NEWLINE]; | ||
| const endWithWhiteSpace = !!item[META]?.[END_WITH_WHITESPACE]; | ||
| const startWithWhitespace = !!item[META]?.[START_WITH_WHITESPACE]; | ||
| const isWhitespace = !!item[META]?.[IS_WHITESPACE]; | ||
| const textItem = Array.isArray(item.text) | ||
| ? item : { text: [isWhitespace ? addWhitespace('newLine') : item] }; | ||
| if (!isNewline && !isWhitespace) { | ||
| // https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace | ||
| // add whitespace before | ||
| if (startWithNewLine || startWithWhitespace) { | ||
| textItem.text.unshift(addWhitespace(startWithNewLine ? 'startWithNewLine' : 'startWithWhitespace')); | ||
| } | ||
| // add whitespace after | ||
| if (endWithNewLine || endWithWhiteSpace) { | ||
| textItem.text.push(addWhitespace(endWithNewLine ? 'endWithNewLine' : 'endWithWhiteSpace')); | ||
| } | ||
| } | ||
| // Append text to last text element otherwise a new line is created | ||
| if (isTextArray(prevItem)) { | ||
| if (ctx.config.collapseWhitespace) { | ||
| collapseWhitespace(prevItem, textItem); | ||
| } | ||
| prevItem.text.push(textItem); | ||
| } | ||
| else { | ||
| // wrap so the next text items will be appended to it | ||
| items.push({ | ||
| text: [textItem] | ||
| }); | ||
| } | ||
| } | ||
| return items; | ||
| }; | ||
| const getNodeRule = (node) => { | ||
| const nodeName = node.nodeName.toLowerCase(); | ||
| switch (nodeName) { | ||
| case '#comment': | ||
| return () => null; | ||
| case '#text': | ||
| return parseText; | ||
| default: | ||
| return () => null; | ||
| } | ||
| }; | ||
| const getElementRule = (el) => { | ||
| const nodeName = el.nodeName.toLowerCase(); | ||
| switch (nodeName) { | ||
| case '#comment': | ||
| case 'option': // see <select> | ||
| case 'script': | ||
| case 'style': | ||
| case 'iframe': | ||
| case 'object': | ||
| return () => null; | ||
| case '#text': | ||
| return parseText; | ||
| case 'a': | ||
| return (el) => { | ||
| const href = el.getAttribute('href'); | ||
| if (!href) { | ||
| return parseElement(el); | ||
| } | ||
| const linkify = (item) => { | ||
| const children = getChildItems(item); | ||
| [].concat(children) | ||
| .forEach((link) => { | ||
| if (typeof link !== 'string') { | ||
| if (href[0] === '#') { | ||
| link.linkToDestination = href.slice(1); | ||
| } | ||
| else { | ||
| link.link = href; | ||
| } | ||
| } | ||
| linkify(link); | ||
| }); | ||
| }; | ||
| return { | ||
| text: items => { | ||
| items.forEach((link) => { | ||
| if (typeof link !== 'string') { | ||
| if (href[0] === '#') { | ||
| link.linkToDestination = href.slice(1); | ||
| } | ||
| else { | ||
| link.link = href; | ||
| } | ||
| } | ||
| linkify(link); | ||
| }); | ||
| return items; | ||
| } | ||
| }; | ||
| }; | ||
| case 'br': | ||
| return () => ({ | ||
| text: '\n', | ||
| [META]: { | ||
| [IS_NEWLINE]: true | ||
| } | ||
| }); | ||
| case 'qr-code': // CUSTOM | ||
| return (el) => { | ||
| const content = el.getAttribute('value'); | ||
| if (!content) { | ||
| return null; | ||
| } | ||
| const sizeAttr = el.getAttribute('data-size'); | ||
| const size = sizeAttr ? toUnit(sizeAttr) : toUnit('128px'); | ||
| return { | ||
| qr: content, | ||
| fit: size, | ||
| }; | ||
| }; | ||
| case 'toc': // CUSTOM | ||
| return (el) => { | ||
| const content = el.textContent; | ||
| if (!content) { | ||
| return null; | ||
| } | ||
| return { | ||
| toc: { | ||
| title: { | ||
| text: content, | ||
| bold: true, | ||
| fontSize: toUnit('22px'), | ||
| margin: [0, 10, 0, 10] | ||
| }, | ||
| }, | ||
| }; | ||
| }; | ||
| case 'table': | ||
| return parseTable; | ||
| case 'ul': | ||
| return () => { | ||
| return { | ||
| ul: (items) => items | ||
| }; | ||
| }; | ||
| case 'ol': | ||
| return () => { | ||
| return { | ||
| ol: (items) => items | ||
| }; | ||
| }; | ||
| case 'img': | ||
| return parseImg; | ||
| case 'svg': | ||
| return parseSvg; | ||
| case 'hr': | ||
| // TODO find better <hr> alternative? | ||
| return () => { | ||
| return { | ||
| table: { | ||
| widths: ['*'], | ||
| body: [ | ||
| [''], | ||
| ] | ||
| }, | ||
| style: ['hr'], | ||
| }; | ||
| }; | ||
| case 'input': | ||
| // TODO acro-form | ||
| return (el) => { | ||
| if (el instanceof HTMLInputElement) { | ||
| return { | ||
| text: 'value' in el ? el.value : '', | ||
| [META]: { | ||
| [IS_INPUT]: true | ||
| } | ||
| }; | ||
| } | ||
| return null; | ||
| }; | ||
| case 'select': | ||
| // TODO acro-form | ||
| return (el) => { | ||
| if (el instanceof HTMLSelectElement) { | ||
| const value = el.options[el.selectedIndex].value; | ||
| return { | ||
| text: value, | ||
| [META]: { | ||
| [IS_INPUT]: true | ||
| } | ||
| }; | ||
| } | ||
| return null; | ||
| }; | ||
| default: | ||
| return parseElement; | ||
| } | ||
| }; | ||
| const getItemByRule = (el, ctx) => { | ||
| if (typeof ctx.config.customRule === 'function') { | ||
| const result = ctx.config.customRule(el, ctx); | ||
| if (result === null) { | ||
| return null; | ||
| } | ||
| else if (result !== undefined) { | ||
| return result; | ||
| } | ||
| } | ||
| if (isElement(el)) { // ELEMENT_NODE | ||
| return getElementRule(el)(el, ctx); | ||
| } | ||
| else if (isNode(el)) { // TEXT_NODE || COMMENT_NODE | ||
| return getNodeRule(el)(el, ctx); | ||
| } | ||
| throw new Error('Unsupported Node Type: ' + el.nodeType); | ||
| }; | ||
| const processItems = (item, ctx, parentStyles = {}) => { | ||
| const el = item[META]?.[NODE]; | ||
| if (typeof item !== 'string' && el) { | ||
| const { cssStyles, props } = computeProps(el, item, ctx.styles, parentStyles); | ||
| Object.assign(item, props); | ||
| if ('stack' in item && typeof item.stack === 'function') { | ||
| const children = parseChildren(el, ctx, cssStyles); | ||
| item.stack = item.stack(children, ctx); | ||
| } | ||
| else if ('text' in item && typeof item.text === 'function') { | ||
| const children = parseChildren(el, ctx, cssStyles); | ||
| item.text = item.text(children.filter(isTextOrLeaf), ctx); | ||
| } | ||
| else if ('ul' in item && typeof item.ul === 'function') { | ||
| const children = parseChildren(el, ctx, cssStyles); | ||
| item.ul = item.ul(children, ctx); | ||
| } | ||
| else if ('ol' in item && typeof item.ol === 'function') { | ||
| const children = parseChildren(el, ctx, cssStyles); | ||
| item.ol = item.ol(children, ctx); | ||
| } | ||
| else if ('table' in item && typeof item.table.body === 'function') { | ||
| const children = parseChildren(el, ctx, cssStyles); | ||
| item.table.body = item.table.body(children, ctx); | ||
| } | ||
| } | ||
| return handleItem(item); | ||
| }; | ||
| const parseByRule = (el, ctx, parentStyles = {}) => { | ||
| const item = getItemByRule(el, ctx); | ||
| if (item === null) { | ||
| return null; | ||
| } | ||
| // Add ref to NODE | ||
| const meta = item[META] || {}; | ||
| meta[NODE] = el; | ||
| item[META] = meta; | ||
| return processItems(item, ctx, parentStyles); | ||
| }; | ||
| const parseElement = (el) => { | ||
| if (isStackItem(el)) { | ||
| return { | ||
| stack: (items) => items | ||
| }; | ||
| } | ||
| return { | ||
| text: (items) => { | ||
| // Return flat | ||
| if (items.length === 1 && 'text' in items[0] && Array.isArray(items[0].text)) { | ||
| return items[0].text; | ||
| } | ||
| return items; | ||
| } | ||
| }; | ||
| }; | ||
| const htmlToDom = (html) => { | ||
| if (typeof DOMParser !== 'undefined') { | ||
| const parser = new DOMParser(); | ||
| const doc = parser.parseFromString(html, 'text/html'); | ||
| return doc.body; | ||
| } | ||
| else if (typeof document !== 'undefined' && typeof document.createDocumentFragment === 'function') { | ||
| const fragment = document.createDocumentFragment(); | ||
| const doc = document.createElement('div'); | ||
| doc.innerHTML = html; | ||
| fragment.append(doc); | ||
| return fragment.children[0]; | ||
| } | ||
| throw new Error('Could not parse html to DOM. Please use external parser like jsdom.'); | ||
| }; | ||
| const parse = (input, _config = defaultConfig()) => { | ||
| const config = { | ||
| ...defaultConfig, | ||
| ..._config | ||
| }; | ||
| const ctx = new Context(config, Object.assign({}, config.globalStyles, config.styles)); | ||
| const body = typeof input === 'string' ? htmlToDom(input) : input; | ||
| const content = body !== null ? parseChildren(body, ctx) : []; | ||
| return { | ||
| content, | ||
| images: ctx.images, | ||
| patterns: getPatterns() | ||
| }; | ||
| }; | ||
| export { parse }; |
+1615
| (function (global, factory) { | ||
| typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : | ||
| typeof define === 'function' && define.amd ? define(['exports'], factory) : | ||
| (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.html2pdfmake = {})); | ||
| })(this, (function (exports) { 'use strict'; | ||
| const META = Symbol('__HTML2PDFMAKE'); | ||
| const NODE = 'NODE'; | ||
| const UID = 'UID'; | ||
| const END_WITH_NEWLINE = 'END_WITH_NEWLINE'; | ||
| const START_WITH_NEWLINE = 'START_WITH_NEW_LINE'; | ||
| const IS_NEWLINE = 'IS_NEWLINE'; | ||
| const START_WITH_WHITESPACE = 'START_WITH_WHITESPACE'; | ||
| const END_WITH_WHITESPACE = 'END_WITH_WHITESPACE'; | ||
| const IS_WHITESPACE = 'IS_WHITESPACE'; | ||
| const IS_INPUT = 'IS_INPUT'; | ||
| const MARGIN = 'MARGIN'; | ||
| const PADDING = 'PADDING'; | ||
| const POSITION = 'POSITION'; | ||
| const HANDLER = 'HANDLER'; | ||
| const PDFMAKE = 'PDFMAKE'; | ||
| const ITEMS = 'ITEMS'; // meta items | ||
| const STYLE = 'STYLE'; | ||
| const POS_TOP = 1; // CSS 0 | ||
| const POS_RIGHT = 2; // CSS 1 | ||
| const POS_BOTTOM = 3; // CSS 2 | ||
| const POS_LEFT = 0; // CSS 3 | ||
| const getPatterns = () => ({ | ||
| fill: { | ||
| boundingBox: [1, 1, 4, 4], | ||
| xStep: 1, | ||
| yStep: 1, | ||
| pattern: '1 w 0 1 m 4 5 l s 2 0 m 5 3 l s' | ||
| } | ||
| }); | ||
| class Context { | ||
| config; | ||
| styles; | ||
| images = {}; | ||
| constructor(config, styles) { | ||
| this.config = config; | ||
| this.styles = styles; | ||
| } | ||
| } | ||
| const globalStyles = () => ({ | ||
| ':root': { | ||
| 'font-size': '16px' | ||
| }, | ||
| h1: { | ||
| 'font-size': '32px', | ||
| 'margin-top': '21.44px', | ||
| 'margin-bottom': '21.44px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| h2: { | ||
| 'font-size': '24px', | ||
| 'margin-top': '19.92px', | ||
| 'margin-bottom': '19.92px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| h3: { | ||
| 'font-size': '18.72px', | ||
| 'margin-top': '18.72px', | ||
| 'margin-bottom': '18.72px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| h4: { | ||
| 'font-size': '16px', | ||
| 'margin-top': '21.28px', | ||
| 'margin-bottom': '21.28px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| h5: { | ||
| 'font-size': '13.28px', | ||
| 'margin-top': '22.17px', | ||
| 'margin-bottom': '22.17px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| h6: { | ||
| 'font-size': '10.72px', | ||
| 'margin-top': '24.97px', | ||
| 'margin-bottom': '24.97px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| b: { | ||
| 'font-weight': 'bold' | ||
| }, | ||
| strong: { | ||
| 'font-weight': 'bold' | ||
| }, | ||
| i: { | ||
| 'font-style': 'italic', | ||
| }, | ||
| em: { | ||
| 'font-style': 'italic' | ||
| }, | ||
| s: { | ||
| 'text-decoration': 'line-through' | ||
| }, | ||
| del: { | ||
| 'text-decoration': 'line-through' | ||
| }, | ||
| sub: { | ||
| 'font-size': '22px', | ||
| 'vertical-align': 'sub' | ||
| }, | ||
| small: { | ||
| 'font-size': '13px' | ||
| }, | ||
| u: { | ||
| 'text-decoration': 'underline' | ||
| }, | ||
| ul: { | ||
| 'margin-top': '16px', | ||
| 'margin-bottom': '16px', | ||
| 'padding-left': '20px' | ||
| }, | ||
| ol: { | ||
| 'margin-top': '16px', | ||
| 'margin-bottom': '16px', | ||
| 'padding-left': '20px' | ||
| }, | ||
| p: { | ||
| 'margin-top': '16px', | ||
| 'margin-bottom': '16px' | ||
| }, | ||
| table: { | ||
| border: 'none', | ||
| padding: '3px' | ||
| }, | ||
| td: { | ||
| border: 'none' | ||
| }, | ||
| tr: { | ||
| margin: '4px 0' | ||
| }, | ||
| th: { | ||
| 'font-weight': 'bold', | ||
| border: 'none', | ||
| 'text-align': 'center' | ||
| }, | ||
| a: { | ||
| color: '#0000ee', | ||
| 'text-decoration': 'underline' | ||
| }, | ||
| hr: { | ||
| 'border-top': '2px solid #9a9a9a', | ||
| 'border-bottom': '0', | ||
| 'border-left': '0px solid black', | ||
| 'border-right': '0', | ||
| margin: '8px 0' | ||
| } | ||
| }); | ||
| const defaultConfig = () => ({ | ||
| globalStyles: globalStyles(), | ||
| styles: {}, | ||
| collapseMargin: true, | ||
| collapseWhitespace: true, | ||
| }); | ||
| /** | ||
| * @description Method to check if an item is an object. Date and Function are considered | ||
| * an object, so if you need to exclude those, please update the method accordingly. | ||
| * @param item - The item that needs to be checked | ||
| * @return {Boolean} Whether or not @item is an object | ||
| */ | ||
| const isObject = (item) => { | ||
| return (item === Object(item) && !Array.isArray(item)); | ||
| }; | ||
| /** | ||
| * @description Method to perform a deep merge of objects | ||
| * @param {Object} target - The targeted object that needs to be merged with the supplied @sources | ||
| * @param {Array<Object>} sources - The source(s) that will be used to update the @target object | ||
| * @return {Object} The final merged object | ||
| */ | ||
| const merge = (target, ...sources) => { | ||
| // return the target if no sources passed | ||
| if (!sources.length) { | ||
| return target; | ||
| } | ||
| const result = target; | ||
| if (isObject(result)) { | ||
| for (let i = 0; i < sources.length; i += 1) { | ||
| if (isObject(sources[i])) { | ||
| const elm = sources[i]; | ||
| Object.keys(elm).forEach(key => { | ||
| if (isObject(elm[key])) { | ||
| if (!result[key] || !isObject(result[key])) { | ||
| result[key] = {}; | ||
| } | ||
| merge(result[key], elm[key]); | ||
| } | ||
| else { | ||
| result[key] = elm[key]; | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| return result; | ||
| }; | ||
| const isNotText = (item) => typeof item !== 'string'; | ||
| const isColgroup = (item) => item?.[META]?.[NODE]?.nodeName === 'COLGROUP'; | ||
| const isImage = (item) => 'image' in item; | ||
| const isTable = (item) => 'table' in item; | ||
| const isTextArray = (item) => !!item && typeof item !== 'string' && 'text' in item && Array.isArray(item.text); | ||
| const isTextSimple = (item) => typeof item !== 'string' && 'text' in item && typeof item.text === 'string'; | ||
| const isTextOrLeaf = (item) => 'text' in item || typeof item === 'string'; | ||
| const isList = (item) => 'ul' in item || 'ol' in item; | ||
| const isTdOrTh = (item) => item[META]?.[NODE] && (item[META]?.[NODE]?.nodeName === 'TD' || item[META]?.[NODE]?.nodeName === 'TH'); | ||
| const isHeadline = (item) => item[META]?.[NODE] && (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(item[META]?.[NODE]?.nodeName || '')); | ||
| const isElement = (el) => el.nodeType === 1; | ||
| const isNode = (el) => el.nodeType === 3 || el.nodeType === 8; | ||
| const isCollapsable = (item) => typeof item !== 'undefined' && typeof item !== 'string' && ('stack' in item || 'ul' in item || 'ol' in item) && 'margin' in item; | ||
| const getChildItems = (item) => { | ||
| if (typeof item === 'string') { | ||
| return []; | ||
| } | ||
| if ('stack' in item) { | ||
| return item.stack; | ||
| } | ||
| if ('text' in item && typeof item.text !== 'string') { | ||
| return item.text; | ||
| } | ||
| if ('table' in item) { | ||
| return item.table.body | ||
| .flatMap(tr => tr) | ||
| .filter(isNotText); | ||
| } | ||
| if ('ul' in item) { | ||
| return item.ul; | ||
| } | ||
| if ('ol' in item) { | ||
| return item.ol; | ||
| } | ||
| return []; | ||
| }; | ||
| const toUnit = (value, rootPt = 12) => { | ||
| // if it's just a number, then return it | ||
| if (typeof value === 'number') { | ||
| return isFinite(value) ? value : 0; | ||
| } | ||
| const val = Number(parseFloat(value)); | ||
| if (isNaN(val)) { | ||
| return 0; | ||
| } | ||
| const match = ('' + value).trim().match(/(pt|px|r?em|cm)$/); | ||
| if (!match) { | ||
| return val; | ||
| } | ||
| switch (match[1]) { | ||
| case 'em': | ||
| case 'rem': | ||
| return val * rootPt; | ||
| case 'px': | ||
| // 1px = 0.75 Point | ||
| return Number((val * 0.75).toFixed(2)); | ||
| case 'cm': | ||
| return Number((val * 28.34).toFixed(2)); | ||
| case 'mm': | ||
| return Number((val * 10 * 28.34).toFixed(2)); | ||
| default: | ||
| return val; | ||
| } | ||
| }; | ||
| const getUnitOrValue = (value) => typeof value === 'string' && (value.indexOf('%') > -1 || value.indexOf('auto') > -1) | ||
| ? value | ||
| : toUnit(value); | ||
| const toUnitOrValue = (value) => getUnitOrValue(value); | ||
| const toUnitsOrValues = (value) => value.map(v => getUnitOrValue(v)); | ||
| const expandValueToUnits = (value) => { | ||
| const values = toUnitsOrValues(value.split(' ') | ||
| .map(v => v.trim()) | ||
| .filter(v => v)); | ||
| if (values === null || !Array.isArray(values)) { | ||
| return null; | ||
| } | ||
| // values[0] = top | ||
| // values[1] = right | ||
| // values[2] = bottom | ||
| // values[3] = left | ||
| // pdfmake use left, top, right, bottom, || [horizontal, vertical] | ||
| // css use top, right, bottom, left | ||
| if (values.length === 1 && values[0] !== null) { | ||
| return [values[0], values[0], values[0], values[0]]; | ||
| } | ||
| else if (values.length === 2) { | ||
| // topbottom leftright | ||
| return [values[1], values[0], values[1], values[0]]; | ||
| } | ||
| else if (values.length === 3) { | ||
| // top bottom leftright | ||
| return [values[2], values[0], values[2], values[1]]; | ||
| } | ||
| else if (values.length === 4) { | ||
| return [values[3], values[0], values[1], values[2]]; | ||
| } | ||
| return null; | ||
| }; | ||
| const handleColumns = (item) => { | ||
| const childItems = getChildItems(item); | ||
| return { | ||
| columns: childItems | ||
| .flatMap((subItem) => { | ||
| if ('text' in subItem && Array.isArray(subItem.text)) { | ||
| return subItem.text | ||
| .filter(childItem => !childItem[META]?.[IS_WHITESPACE]) | ||
| .map(text => { | ||
| const width = toUnitOrValue(text[META]?.[STYLE]?.width || 'auto') || 'auto'; | ||
| return (typeof text === 'string') ? { | ||
| text, | ||
| width | ||
| } : { | ||
| ...text, | ||
| width | ||
| }; | ||
| }); | ||
| } | ||
| return { | ||
| stack: [].concat(subItem), | ||
| width: toUnitOrValue(subItem[META]?.[STYLE]?.width || 'auto') || 'auto' | ||
| }; | ||
| }), | ||
| columnGap: 'columnGap' in item ? item.columnGap : 0 | ||
| }; | ||
| }; | ||
| const handleImg = (image) => { | ||
| if (isImage(image) && typeof image.width === 'number' && typeof image.height === 'number') { | ||
| image.fit = [image.width, image.height]; | ||
| } | ||
| return image; | ||
| }; | ||
| const handleTable = (item) => { | ||
| if (isTable(item)) { | ||
| const bodyItem = item.table.body[0]?.[0]; | ||
| const tableItem = bodyItem && typeof bodyItem !== 'string' && 'table' in bodyItem ? bodyItem : null; | ||
| if (!tableItem) { | ||
| return item; | ||
| } | ||
| const innerTable = tableItem.table; | ||
| const colgroup = bodyItem[META]?.[ITEMS]?.colgroup; | ||
| if (colgroup && Array.isArray(colgroup)) { | ||
| innerTable.widths = innerTable.widths || []; | ||
| colgroup.forEach((col, i) => { | ||
| if (col[META]?.[STYLE]?.width && innerTable.widths) { | ||
| innerTable.widths[i] = getUnitOrValue(col[META]?.[STYLE]?.width || 'auto'); | ||
| } | ||
| }); | ||
| } | ||
| const trs = bodyItem[META]?.[ITEMS]?.trs; | ||
| if (Array.isArray(trs)) { | ||
| trs.forEach((tr, i) => { | ||
| if (tr[META]?.[STYLE]?.height && innerTable.heights) { | ||
| innerTable.heights[i] = getUnitOrValue(tr[META]?.[STYLE]?.height || 'auto'); | ||
| } | ||
| }); | ||
| } | ||
| const paddingsTopBottom = {}; | ||
| const paddingsLeftRight = {}; | ||
| innerTable.body | ||
| .forEach((row, trIndex) => { | ||
| row.forEach((column, tdIndex) => { | ||
| if (typeof column !== 'string') { | ||
| if (column[META]?.[PADDING]) { | ||
| paddingsTopBottom[trIndex] = paddingsTopBottom[trIndex] || [0, 0]; | ||
| paddingsLeftRight[tdIndex] = paddingsLeftRight[tdIndex] || [0, 0]; | ||
| paddingsTopBottom[trIndex] = [ | ||
| Math.max(paddingsTopBottom[trIndex][0], column[META]?.[PADDING]?.[POS_TOP] || 0), | ||
| Math.max(paddingsTopBottom[trIndex][1], column[META]?.[PADDING]?.[POS_BOTTOM] || 0) | ||
| ]; | ||
| paddingsLeftRight[tdIndex] = [ | ||
| Math.max(paddingsLeftRight[tdIndex][0], column[META]?.[PADDING]?.[POS_LEFT] || 0), | ||
| Math.max(paddingsLeftRight[tdIndex][1], column[META]?.[PADDING]?.[POS_RIGHT] || 0) | ||
| ]; | ||
| } | ||
| column.style = column.style || []; | ||
| column.style.push(tdIndex % 2 === 0 ? 'td:nth-child(even)' : 'td:nth-child(odd)'); | ||
| column.style.push(trIndex % 2 === 0 ? 'tr:nth-child(even)' : 'tr:nth-child(odd)'); | ||
| } | ||
| }); | ||
| }); | ||
| const tableLayout = {}; | ||
| const hasPaddingTopBottom = Object.keys(paddingsTopBottom).length > 0; | ||
| const hasPaddingLeftRight = Object.keys(paddingsLeftRight).length > 0; | ||
| if (hasPaddingTopBottom) { | ||
| tableLayout.paddingTop = (i) => { | ||
| if (paddingsTopBottom[i]) { | ||
| return paddingsTopBottom[i][0]; | ||
| } | ||
| return 0; | ||
| }; | ||
| tableLayout.paddingBottom = (i) => { | ||
| if (paddingsTopBottom[i]) { | ||
| return paddingsTopBottom[i][1]; | ||
| } | ||
| return 0; | ||
| }; | ||
| } | ||
| if (hasPaddingLeftRight) { | ||
| tableLayout.paddingRight = (i) => { | ||
| if (paddingsLeftRight[i]) { | ||
| return paddingsLeftRight[i][1]; | ||
| } | ||
| return 0; | ||
| }; | ||
| tableLayout.paddingLeft = (i) => { | ||
| if (paddingsLeftRight[i]) { | ||
| return paddingsLeftRight[i][0]; | ||
| } | ||
| return 0; | ||
| }; | ||
| } | ||
| if (hasPaddingLeftRight || hasPaddingTopBottom) { | ||
| tableItem.layout = tableLayout; | ||
| } | ||
| } | ||
| return item; | ||
| }; | ||
| const addTocItem = (item, tocStyle = {}) => { | ||
| if ('text' in item && typeof item.text === 'string') { | ||
| item.tocItem = true; | ||
| merge(item, tocStyle); | ||
| } | ||
| else if ('stack' in item) { | ||
| const text = item.stack.find(s => 'text' in s); | ||
| if (text && typeof text !== 'string') { | ||
| text.tocItem = true; | ||
| merge(text, tocStyle); | ||
| } | ||
| } | ||
| }; | ||
| const handleHeadlineToc = (item) => { | ||
| const tocStyle = {}; | ||
| if (item[META]?.[NODE]?.nodeName === 'H1') { | ||
| Object.assign(tocStyle, { | ||
| tocNumberStyle: { bold: true } | ||
| }); | ||
| } | ||
| else { | ||
| Object.assign(tocStyle, { | ||
| tocMargin: [10, 0, 0, 0] | ||
| }); | ||
| } | ||
| addTocItem(item, tocStyle); | ||
| return item; | ||
| }; | ||
| const handleItem = (item) => { | ||
| if (typeof item !== 'string' && item[META]?.[PDFMAKE]) { | ||
| merge(item, item[META]?.[PDFMAKE] || {}); | ||
| } | ||
| if (typeof item[META]?.[HANDLER] === 'function') { | ||
| return item[META]?.[HANDLER]?.(item) || null; | ||
| } | ||
| return item; | ||
| }; | ||
| let _uid = 0; | ||
| const getUniqueId = (item) => { | ||
| const meta = item[META] || {}; | ||
| const __uid = meta[UID]; | ||
| const el = item[META]?.[NODE]; | ||
| if (__uid) { | ||
| return __uid; | ||
| } | ||
| if (!el) { | ||
| return '#' + (_uid++); | ||
| } | ||
| if (isElement(el)) { | ||
| const id = el.getAttribute('id'); | ||
| if (id) { | ||
| return id; | ||
| } | ||
| } | ||
| const nodeName = el.nodeName.toLowerCase(); | ||
| // TODO add parent? Or name something else? | ||
| const uid = '#' + nodeName + '-' + (_uid++); | ||
| meta[UID] = uid; | ||
| item[META] = meta; | ||
| return uid; | ||
| }; | ||
| const attrToProps = (item) => { | ||
| const el = item[META]?.[NODE]; | ||
| if (!el || !('getAttribute' in el)) | ||
| return { [META]: { [STYLE]: {} } }; | ||
| const cssClass = el.getAttribute('class') || ''; | ||
| const cssClasses = [...new Set(cssClass.split(' ') | ||
| .filter((value) => value) | ||
| .map((value) => '.' + value.trim()))]; | ||
| const nodeName = el.nodeName.toLowerCase(); | ||
| const parentNodeName = el.parentNode ? el.parentNode.nodeName.toLowerCase() : null; | ||
| const styleNames = [ | ||
| nodeName, | ||
| ].concat(cssClasses); | ||
| if (cssClasses.length > 2) { | ||
| styleNames.push(cssClasses.join('')); // .a.b.c | ||
| } | ||
| if (parentNodeName) { | ||
| styleNames.push(parentNodeName + '>' + nodeName); | ||
| } | ||
| const uniqueId = getUniqueId(item); | ||
| styleNames.push(uniqueId); // Should be the last one | ||
| const props = { | ||
| [META]: { | ||
| [STYLE]: item[META]?.[STYLE] || {}, | ||
| ...(item[META] || {}) | ||
| }, | ||
| style: [...new Set((item.style || []).concat(styleNames))] | ||
| }; | ||
| for (let i = 0; i < el.attributes.length; i++) { | ||
| const name = el.attributes[i].name; | ||
| const value = el.getAttribute(name)?.trim() || null; | ||
| if (value == null) { | ||
| continue; | ||
| } | ||
| props[META][STYLE][name] = value; | ||
| switch (name) { | ||
| case 'rowspan': | ||
| props.rowSpan = parseInt(value, 10); | ||
| break; | ||
| case 'colspan': | ||
| props.colSpan = parseInt(value, 10); | ||
| break; | ||
| case 'value': | ||
| if (nodeName === 'li') { | ||
| props.counter = parseInt(value, 10); | ||
| } | ||
| break; | ||
| case 'start': // ol | ||
| if (nodeName === 'ol') { | ||
| props.start = parseInt(value, 10); | ||
| } | ||
| break; | ||
| case 'width': | ||
| if ('image' in item) { | ||
| props.width = toUnit(value); | ||
| } | ||
| break; | ||
| case 'height': | ||
| if ('image' in item) { | ||
| props.height = toUnit(value); | ||
| } | ||
| break; | ||
| case 'data-fit': | ||
| if (value === 'true') { | ||
| const width = el.getAttribute('width'); | ||
| const height = el.getAttribute('height'); | ||
| if (width && height) { | ||
| props.fit = [toUnit(width), toUnit(height)]; | ||
| } | ||
| } | ||
| break; | ||
| case 'data-toc-item': | ||
| if (value !== 'false') { | ||
| let toc = {}; | ||
| if (value) { | ||
| try { | ||
| toc = JSON.parse(value); | ||
| } | ||
| catch (e) { | ||
| console.warn('Not valid JSON format.', value); | ||
| } | ||
| } | ||
| props[META][HANDLER] = (item) => { | ||
| addTocItem(item, toc); | ||
| return item; | ||
| }; | ||
| } | ||
| break; | ||
| case 'data-pdfmake': | ||
| if (value) { | ||
| try { | ||
| props[META][PDFMAKE] = JSON.parse(value); | ||
| } | ||
| catch (e) { | ||
| console.warn('Not valid JSON format.', value); | ||
| } | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| return props; | ||
| }; | ||
| const inheritStyle = (styles) => { | ||
| // TODO what do we want to exclude ? | ||
| const pick = { | ||
| color: true, | ||
| 'font-family': true, | ||
| 'font-size': true, | ||
| 'font-weight': true, | ||
| 'font': true, | ||
| 'line-height': true, | ||
| 'list-style-type': true, | ||
| 'list-style': true, | ||
| 'text-align': true, | ||
| // TODO only if parent is text: [] | ||
| background: true, | ||
| 'font-style': true, | ||
| 'background-color': true, | ||
| 'font-feature-settings': true, | ||
| 'white-space': true, | ||
| 'vertical-align': true, | ||
| 'opacity': true, | ||
| 'text-decoration': true, | ||
| }; | ||
| return Object.keys(styles).reduce((p, c) => { | ||
| if (pick[c] || styles[c] === 'inherit') { | ||
| p[c] = styles[c]; | ||
| } | ||
| return p; | ||
| }, {}); | ||
| }; | ||
| const getBorderStyle = (value) => { | ||
| const border = value.split(' '); | ||
| const color = border[2] || 'black'; | ||
| const borderStyle = border[1] || 'solid'; | ||
| const width = toUnit(border[0]); | ||
| return { color, width, borderStyle }; | ||
| }; | ||
| const computeBorder = (item, props, directive, value) => { | ||
| const { color, width, borderStyle } = getBorderStyle(value); | ||
| const tdOrTh = isTdOrTh(item); | ||
| const setBorder = (index) => { | ||
| props.border = item.border || props.border || [false, false, false, false]; | ||
| props.borderColor = item.borderColor || props.borderColor || ['black', 'black', 'black', 'black']; | ||
| if (value === 'none') { | ||
| props.border[index] = false; | ||
| } | ||
| else { | ||
| props.border[index] = true; | ||
| props.borderColor[index] = color; | ||
| } | ||
| }; | ||
| switch (directive) { | ||
| case 'border': | ||
| if (isTable(item)) { | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| if (value === 'none') { | ||
| props.layout.hLineWidth = () => 0; | ||
| props.layout.vLineWidth = () => 0; | ||
| break; | ||
| } | ||
| props.layout.vLineColor = () => color; | ||
| props.layout.hLineColor = () => color; | ||
| props.layout.hLineWidth = (i, node) => (i === 0 || i === node.table.body.length) ? width : 0; | ||
| props.layout.vLineWidth = (i, node) => (i === 0 || i === node.table.widths?.length) ? width : 0; | ||
| if (borderStyle === 'dashed') { | ||
| props.layout.hLineStyle = () => ({ dash: { length: 2, space: 2 } }); | ||
| props.layout.vLineStyle = () => ({ dash: { length: 2, space: 2 } }); | ||
| } | ||
| } | ||
| else if (tdOrTh) { | ||
| setBorder(0); | ||
| setBorder(1); | ||
| setBorder(2); | ||
| setBorder(3); | ||
| } | ||
| break; | ||
| case 'border-bottom': | ||
| if (isTable(item)) { | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| const hLineWidth = props.layout.hLineWidth || (() => 0); | ||
| const hLineColor = props.layout.hLineColor || (() => 'black'); | ||
| props.layout.hLineWidth = (i, node) => (i === node.table.body.length) ? width : hLineWidth(i, node); | ||
| props.layout.hLineColor = (i, node) => (i === node.table.body.length) ? color : hLineColor(i, node); | ||
| } | ||
| else if (tdOrTh) { | ||
| setBorder(3); | ||
| } | ||
| break; | ||
| case 'border-top': | ||
| if (isTable(item)) { | ||
| const { color, width } = getBorderStyle(value); | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| const hLineWidth = props.layout.hLineWidth || (() => 1); | ||
| const hLineColor = props.layout.hLineColor || (() => 'black'); | ||
| props.layout.hLineWidth = (i, node) => (i === 0) ? width : hLineWidth(i, node); | ||
| props.layout.hLineColor = (i, node) => (i === 0) ? color : hLineColor(i, node); | ||
| } | ||
| else if (tdOrTh) { | ||
| setBorder(1); | ||
| } | ||
| break; | ||
| case 'border-right': | ||
| if (isTable(item)) { | ||
| const { color, width } = getBorderStyle(value); | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| const vLineWidth = props.layout.vLineWidth || (() => 1); | ||
| const vLineColor = props.layout.vLineColor || (() => 'black'); | ||
| props.layout.vLineWidth = (i, node) => (node.table.body.length === 1 ? i === node.table.body.length : i % node.table.body.length !== 0) ? width : vLineWidth(i, node); | ||
| props.layout.vLineColor = (i, node) => (node.table.body.length === 1 ? i === node.table.body.length : i % node.table.body.length !== 0) ? color : vLineColor(i, node); | ||
| } | ||
| else if (tdOrTh) { | ||
| setBorder(2); | ||
| } | ||
| break; | ||
| case 'border-left': | ||
| if (isTable(item)) { | ||
| const { color, width } = getBorderStyle(value); | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| const vLineWidth = props.layout.vLineWidth || (() => 1); | ||
| const vLineColor = props.layout.vLineColor || (() => 'black'); | ||
| props.layout.vLineWidth = (i, node) => (node.table.body.length === 1 ? i === 0 : i % node.table.body.length === 0) ? width : vLineWidth(i, node); | ||
| props.layout.vLineColor = (i, node) => (node.table.body.length === 1 ? i === 0 : i % node.table.body.length === 0) ? color : vLineColor(i, node); | ||
| } | ||
| else if (tdOrTh) { | ||
| setBorder(0); | ||
| } | ||
| break; | ||
| } | ||
| }; | ||
| const computeMargin = (itemProps, item, value, index) => { | ||
| const margin = itemProps[META][MARGIN] || item[META]?.[MARGIN] || [0, 0, 0, 0]; | ||
| margin[index] = value; | ||
| itemProps[META][MARGIN] = [...margin]; | ||
| itemProps.margin = margin; | ||
| const padding = itemProps[META][PADDING] || item[META]?.[PADDING] || [0, 0, 0, 0]; | ||
| const paddingValue = padding[index] || 0; | ||
| itemProps.margin[index] = value + paddingValue; | ||
| }; | ||
| const computePadding = (props, item, value, index) => { | ||
| if (isTable(item)) { | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| switch (index) { | ||
| case POS_LEFT: | ||
| props.layout.paddingLeft = () => toUnit(value); | ||
| break; | ||
| case POS_TOP: | ||
| props.layout.paddingTop = () => toUnit(value); | ||
| break; | ||
| case POS_RIGHT: | ||
| props.layout.paddingRight = () => toUnit(value); | ||
| break; | ||
| case POS_BOTTOM: | ||
| props.layout.paddingBottom = () => toUnit(value); | ||
| break; | ||
| default: | ||
| throw new Error('Unsupported index for padding: ' + index); | ||
| } | ||
| } | ||
| else { | ||
| const padding = props[META][PADDING] || item[META]?.[PADDING] || [0, 0, 0, 0]; | ||
| padding[index] = value; | ||
| props[META][PADDING] = [...padding]; | ||
| if (!isTdOrTh(item)) { | ||
| const margin = props[META][MARGIN] || item[META]?.[MARGIN] || [0, 0, 0, 0]; | ||
| props.margin = margin; | ||
| const marginValue = margin[index]; | ||
| props.margin[index] = value + marginValue; | ||
| } | ||
| } | ||
| }; | ||
| const styleToProps = (item, styles, parentStyles = {}) => { | ||
| const props = { | ||
| [META]: { | ||
| [STYLE]: {}, | ||
| ...(item[META] || {}), | ||
| } | ||
| }; | ||
| const meta = props[META]; | ||
| const image = isImage(item); | ||
| const table = isTable(item); | ||
| const text = isTextSimple(item); | ||
| const list = isList(item); | ||
| const rootFontSize = toUnit(parentStyles['font-size'] || '16px'); | ||
| if (isHeadline(item)) { | ||
| meta[HANDLER] = handleHeadlineToc; | ||
| } | ||
| Object.keys(styles).forEach((key) => { | ||
| const directive = key; | ||
| const value = ('' + styles[key]).trim(); | ||
| props[META][STYLE][directive] = value; | ||
| switch (directive) { | ||
| case 'padding': { | ||
| const paddings = expandValueToUnits(value); | ||
| if (table && paddings !== null) { | ||
| let layout = props.layout || item.layout || {}; | ||
| if (typeof layout === 'string') { | ||
| layout = {}; | ||
| } | ||
| layout.paddingLeft = () => Number(paddings[POS_LEFT]); | ||
| layout.paddingRight = () => Number(paddings[POS_RIGHT]); | ||
| layout.paddingTop = (i) => (i === 0) ? Number(paddings[POS_TOP]) : 0; | ||
| layout.paddingBottom = (i, node) => (i === node.table.body.length - 1) ? Number(paddings[POS_BOTTOM]) : 0; | ||
| props.layout = layout; | ||
| } | ||
| else if (paddings !== null) { | ||
| computePadding(props, item, Number(paddings[POS_TOP]), POS_TOP); | ||
| computePadding(props, item, Number(paddings[POS_LEFT]), POS_LEFT); | ||
| computePadding(props, item, Number(paddings[POS_RIGHT]), POS_RIGHT); | ||
| computePadding(props, item, Number(paddings[POS_BOTTOM]), POS_BOTTOM); | ||
| } | ||
| break; | ||
| } | ||
| case 'border': | ||
| case 'border-bottom': | ||
| case 'border-top': | ||
| case 'border-right': | ||
| case 'border-left': | ||
| computeBorder(item, props, directive, value); | ||
| break; | ||
| case 'font-size': { | ||
| props.fontSize = toUnit(value, rootFontSize); | ||
| break; | ||
| } | ||
| case 'line-height': | ||
| props.lineHeight = toUnit(value, rootFontSize); | ||
| break; | ||
| case 'letter-spacing': | ||
| props.characterSpacing = toUnit(value); | ||
| break; | ||
| case 'text-align': | ||
| props.alignment = value; | ||
| break; | ||
| case 'font-feature-settings': { | ||
| const settings = value.split(',').filter(s => s).map(s => s.replace(/['"]/g, '')); | ||
| const fontFeatures = item.fontFeatures || props.fontFeatures || []; | ||
| fontFeatures.push(...settings); | ||
| props.fontFeatures = fontFeatures; | ||
| break; | ||
| } | ||
| case 'font-weight': | ||
| switch (value) { | ||
| case 'bold': | ||
| props.bold = true; | ||
| break; | ||
| case 'normal': | ||
| props.bold = false; | ||
| break; | ||
| } | ||
| break; | ||
| case 'text-decoration': | ||
| switch (value) { | ||
| case 'underline': | ||
| props.decoration = 'underline'; | ||
| break; | ||
| case 'line-through': | ||
| props.decoration = 'lineThrough'; | ||
| break; | ||
| case 'overline': | ||
| props.decoration = 'overline'; | ||
| break; | ||
| } | ||
| break; | ||
| case 'text-decoration-color': | ||
| props.decorationColor = value; | ||
| break; | ||
| case 'text-decoration-style': | ||
| props.decorationStyle = value; | ||
| break; | ||
| case 'vertical-align': | ||
| if (value === 'sub') { | ||
| props.sub = true; | ||
| } | ||
| break; | ||
| case 'font-style': | ||
| switch (value) { | ||
| case 'italic': | ||
| props.italics = true; | ||
| break; | ||
| } | ||
| break; | ||
| case 'font-family': | ||
| props.font = value; | ||
| break; | ||
| case 'color': | ||
| props.color = value; | ||
| break; | ||
| case 'background': | ||
| case 'background-color': | ||
| if (table) { | ||
| let layout = item.layout || {}; | ||
| if (typeof layout === 'string') { | ||
| layout = {}; | ||
| } | ||
| layout.fillColor = () => value; | ||
| props.layout = layout; | ||
| } | ||
| else if (isTdOrTh(item)) { | ||
| props.fillColor = value; | ||
| } | ||
| else { | ||
| props.background = ['fill', value]; | ||
| } | ||
| break; | ||
| case 'margin': { | ||
| const margin = expandValueToUnits(value)?.map(value => typeof value === 'string' ? 0 : value); | ||
| if (margin) { | ||
| computeMargin(props, item, margin[POS_TOP], POS_TOP); | ||
| computeMargin(props, item, margin[POS_LEFT], POS_LEFT); | ||
| computeMargin(props, item, margin[POS_RIGHT], POS_RIGHT); | ||
| computeMargin(props, item, margin[POS_BOTTOM], POS_BOTTOM); | ||
| } | ||
| break; | ||
| } | ||
| case 'margin-left': | ||
| computeMargin(props, item, toUnit(value), POS_LEFT); | ||
| break; | ||
| case 'margin-top': | ||
| computeMargin(props, item, toUnit(value), POS_TOP); | ||
| break; | ||
| case 'margin-right': | ||
| computeMargin(props, item, toUnit(value), POS_RIGHT); | ||
| break; | ||
| case 'margin-bottom': | ||
| computeMargin(props, item, toUnit(value), POS_BOTTOM); | ||
| break; | ||
| case 'padding-left': | ||
| computePadding(props, item, toUnit(value), POS_LEFT); | ||
| break; | ||
| case 'padding-top': | ||
| computePadding(props, item, toUnit(value), POS_TOP); | ||
| break; | ||
| case 'padding-right': | ||
| computePadding(props, item, toUnit(value), POS_RIGHT); | ||
| break; | ||
| case 'padding-bottom': | ||
| computePadding(props, item, toUnit(value), POS_BOTTOM); | ||
| break; | ||
| case 'page-break-before': | ||
| if (value === 'always') { | ||
| props.pageBreak = 'before'; | ||
| } | ||
| break; | ||
| case 'page-break-after': | ||
| if (value === 'always') { | ||
| props.pageBreak = 'after'; | ||
| } | ||
| break; | ||
| case 'position': | ||
| if (value === 'absolute') { | ||
| meta[POSITION] = 'absolute'; | ||
| props.absolutePosition = {}; | ||
| } | ||
| else if (value === 'relative') { | ||
| meta[POSITION] = 'relative'; | ||
| props.relativePosition = {}; | ||
| } | ||
| break; | ||
| case 'left': | ||
| case 'top': | ||
| // TODO can be set before postion:absolute! | ||
| if (!props.absolutePosition && !props.relativePosition) { | ||
| console.error(directive + ' is set, but no absolute/relative position.'); | ||
| break; | ||
| } | ||
| if (props.absolutePosition) { | ||
| if (directive === 'left') { | ||
| props.absolutePosition.x = toUnit(value); | ||
| } | ||
| else if (directive === 'top') { | ||
| props.absolutePosition.y = toUnit(value); | ||
| } | ||
| } | ||
| else if (props.relativePosition) { | ||
| if (directive === 'left') { | ||
| props.relativePosition.x = toUnit(value); | ||
| } | ||
| else if (directive === 'top') { | ||
| props.relativePosition.y = toUnit(value); | ||
| } | ||
| } | ||
| else { | ||
| console.error(directive + ' is set, but no absolute/relative position.'); | ||
| break; | ||
| } | ||
| break; | ||
| case 'white-space': | ||
| if (value === 'pre' && meta[NODE]) { | ||
| if (text) { | ||
| props.text = meta[NODE]?.textContent || ''; | ||
| } | ||
| props.preserveLeadingSpaces = true; | ||
| } | ||
| break; | ||
| case 'display': | ||
| if (value === 'flex') { | ||
| props[META][HANDLER] = handleColumns; | ||
| } | ||
| else if (value === 'none') { | ||
| props[META][HANDLER] = () => null; | ||
| } | ||
| break; | ||
| case 'opacity': | ||
| props.opacity = Number(parseFloat(value)); | ||
| break; | ||
| case 'gap': | ||
| props.columnGap = toUnit(value); | ||
| break; | ||
| case 'list-style-type': | ||
| case 'list-style': | ||
| if (list) { | ||
| props.type = value; | ||
| } | ||
| else { | ||
| props.listType = value; | ||
| } | ||
| break; | ||
| case 'width': | ||
| if (table) { | ||
| if (value === '100%') { | ||
| item.table.widths = ['*']; | ||
| } | ||
| else { | ||
| const width = toUnitOrValue(value); | ||
| if (width !== null) { | ||
| item.table.widths = [width]; | ||
| } | ||
| } | ||
| } | ||
| else if (image) { | ||
| props.width = toUnit(value); | ||
| } | ||
| break; | ||
| case 'height': | ||
| if (image) { | ||
| props.height = toUnit(value); | ||
| } | ||
| break; | ||
| case 'max-height': | ||
| if (image) { | ||
| props.maxHeight = toUnit(value); | ||
| } | ||
| break; | ||
| case 'max-width': | ||
| if (image) { | ||
| props.maxWidth = toUnit(value); | ||
| } | ||
| break; | ||
| case 'min-height': | ||
| if (image) { | ||
| props.minHeight = toUnit(value); | ||
| } | ||
| break; | ||
| case 'min-width': | ||
| if (image) { | ||
| props.minWidth = toUnit(value); | ||
| } | ||
| break; | ||
| case 'object-fit': | ||
| if (value === 'contain' && image) { | ||
| meta[HANDLER] = handleImg; | ||
| } | ||
| break; | ||
| } | ||
| }); | ||
| return props; | ||
| }; | ||
| /** | ||
| * @param el DOM Element | ||
| */ | ||
| const getInlineStyles = (el) => ('getAttribute' in el ? el.getAttribute('style') || '' : '').split(';') | ||
| .map(style => style.trim().toLowerCase().split(':')) | ||
| .filter(style => style.length === 2) | ||
| .reduce((style, value) => { | ||
| style[value[0].trim()] = value[1].trim(); | ||
| return style; | ||
| }, {}); | ||
| const getDefaultStyles = (el, item, styles) => (item.style || []).concat(el.nodeName.toLowerCase()) | ||
| .filter((selector) => styles && styles[selector]) | ||
| .reduce((style, selector) => { | ||
| return { | ||
| ...style, | ||
| ...styles[selector] | ||
| }; | ||
| }, {}); | ||
| /** | ||
| * | ||
| * @param el DOM Element | ||
| * @param item | ||
| * @param styles additional styles | ||
| * @param parentStyles pick styles | ||
| */ | ||
| const computeProps = (el, item, styles, parentStyles = {}) => { | ||
| const defaultStyles = getDefaultStyles(el, item, styles); | ||
| const rootStyles = styles[':root'] || globalStyles()[':root']; | ||
| const inheritedStyles = inheritStyle(parentStyles); | ||
| const cssStyles = Object.assign({}, defaultStyles, inheritedStyles, getInlineStyles(el)); | ||
| const styleProps = styleToProps(item, cssStyles, Object.assign({}, rootStyles, inheritedStyles)); | ||
| const attrProps = attrToProps(item); | ||
| const props = { | ||
| ...styleProps, | ||
| ...attrProps, | ||
| [META]: { | ||
| ...(styleProps[META] || {}), | ||
| ...(attrProps[META] || {}), | ||
| [STYLE]: { | ||
| ...(styleProps[META][STYLE] || {}), | ||
| ...(attrProps[META][STYLE] || {}), | ||
| } | ||
| } | ||
| }; | ||
| return { | ||
| cssStyles, | ||
| props | ||
| }; | ||
| }; | ||
| const collapseMargin = (item, prevItem) => { | ||
| if (isCollapsable(item) && isCollapsable(prevItem)) { | ||
| const prevMargin = prevItem[META]?.[MARGIN] || [0, 0, 0, 0]; | ||
| prevItem[META] = { ...(prevItem[META] || {}), [MARGIN]: prevMargin }; | ||
| prevItem.margin[POS_BOTTOM] = prevItem[META]?.[PADDING]?.[POS_BOTTOM] || 0; | ||
| const itemMargin = item[META]?.[MARGIN] || [0, 0, 0, 0]; | ||
| const marginTop = Math.max(itemMargin[POS_TOP], prevMargin[POS_BOTTOM]); | ||
| itemMargin[POS_TOP] = marginTop; | ||
| prevMargin[POS_BOTTOM] = 0; | ||
| item[META] = { ...(item[META] || {}), [MARGIN]: itemMargin }; | ||
| item.margin[POS_TOP] = marginTop + (item[META]?.[PADDING]?.[POS_TOP] || 0); | ||
| } | ||
| }; | ||
| const findLastDeep = (ta) => { | ||
| const last = ta.text.at(-1); | ||
| if (isTextArray(last)) { | ||
| return findLastDeep(last); | ||
| } | ||
| return last; | ||
| }; | ||
| const findFirstArrayDeep = (ta) => { | ||
| const first = ta.text.at(0); | ||
| if (isTextArray(first)) { | ||
| return findFirstArrayDeep(first); | ||
| } | ||
| return ta.text; | ||
| }; | ||
| const collapseWhitespace = (item, nextText) => { | ||
| const prevLastText = findLastDeep(item); | ||
| const nextFirstTextArray = findFirstArrayDeep(nextText); | ||
| if (prevLastText && prevLastText[META]?.[IS_WHITESPACE] && nextFirstTextArray[0][META]?.[IS_WHITESPACE]) { | ||
| nextFirstTextArray.shift(); | ||
| } | ||
| }; | ||
| function isBase64(str) { | ||
| return /^data:image\/(jpeg|png|jpg);base64,/.test(str); | ||
| } | ||
| const parseImg = (el, ctx) => { | ||
| const src = el.getAttribute('src'); | ||
| if (!src) { | ||
| return null; | ||
| } | ||
| const name = el.getAttribute('name') || src; | ||
| let image; | ||
| if (isBase64(src)) { | ||
| image = src; | ||
| } | ||
| else if (ctx.images[name]) { | ||
| image = name; | ||
| } | ||
| else { | ||
| ctx.images[src] = name; | ||
| image = name; | ||
| } | ||
| return { | ||
| image, | ||
| [META]: {} | ||
| }; | ||
| }; | ||
| const parseSvg = (el) => { | ||
| // TODO is this okay? | ||
| const svgEl = el.cloneNode(true); | ||
| const width = el.getAttribute('width'); | ||
| const height = el.getAttribute('height'); | ||
| if (width) { | ||
| svgEl.setAttribute('width', '' + getUnitOrValue(width)); | ||
| } | ||
| if (height) { | ||
| svgEl.setAttribute('height', '' + getUnitOrValue(height)); | ||
| } | ||
| return { | ||
| svg: svgEl.outerHTML.replace(/\n(\s+)?/g, ''), | ||
| }; | ||
| }; | ||
| const parseTable = () => { | ||
| // TODO table in table? | ||
| return { | ||
| table: { | ||
| body: (items) => { | ||
| // tbody -> tr | ||
| const colgroup = items.find(isColgroup)?.stack[0] || []; | ||
| const tbody = items.filter(item => !isColgroup(item)); | ||
| const trs = tbody.flatMap((item) => 'stack' in item ? item.stack : []); | ||
| const body = trs.map((item) => getChildItems(item)); | ||
| if (body.length === 0) { | ||
| return []; | ||
| } | ||
| const longestRow = body.reduce((a, b) => a.length <= b.length ? b : a); | ||
| const table = { | ||
| body, | ||
| widths: new Array(longestRow.length).fill('auto'), | ||
| heights: new Array(trs.length).fill('auto') | ||
| }; | ||
| return [[{ | ||
| table, | ||
| layout: { | ||
| defaultBorder: false | ||
| }, | ||
| [META]: { | ||
| [ITEMS]: { | ||
| colgroup: 'text' in colgroup && Array.isArray(colgroup.text) ? colgroup.text : [], | ||
| trs | ||
| }, | ||
| } | ||
| }]]; | ||
| }, | ||
| // widths: ['*'], | ||
| }, | ||
| [META]: { | ||
| [HANDLER]: handleTable, | ||
| }, | ||
| layout: {} | ||
| }; | ||
| }; | ||
| const parseText = (el) => { | ||
| const text = el.textContent; | ||
| if (text === null) { | ||
| return null; | ||
| } | ||
| const keepNewLines = text.replace(/[^\S\r\n]+/, ''); | ||
| const trimmedText = text.replace(/\n|\t| +/g, ' ') | ||
| .replace(/^ +/, '') | ||
| .replace(/ +$/, ''); | ||
| //.trim() removes also | ||
| const endWithNL = keepNewLines[keepNewLines.length - 1] === '\n'; | ||
| const startWithNL = keepNewLines[0] === '\n'; | ||
| const startWithWhitespace = text[0] === ' '; | ||
| const endWithWhitespace = text[text.length - 1] === ' '; | ||
| return { | ||
| text: trimmedText, | ||
| [META]: { | ||
| [START_WITH_NEWLINE]: startWithNL, | ||
| [END_WITH_NEWLINE]: endWithNL, | ||
| [IS_NEWLINE]: startWithNL && endWithNL && trimmedText.length === 0, | ||
| [START_WITH_WHITESPACE]: startWithWhitespace, | ||
| [END_WITH_WHITESPACE]: endWithWhitespace, | ||
| [IS_WHITESPACE]: startWithWhitespace && endWithWhitespace && text.length === 1, | ||
| }, | ||
| }; | ||
| }; | ||
| const WHITESPACE = ' '; | ||
| const addWhitespace = (type) => ({ | ||
| text: WHITESPACE, | ||
| [META]: { | ||
| [IS_WHITESPACE]: type | ||
| } | ||
| }); | ||
| const parseAsHTMLCollection = (el) => ['TABLE', 'TBODY', 'TR', 'COLGROUP', 'COL', 'UL', 'OL', 'SELECT'].includes(el.nodeName) && 'children' in el; | ||
| const stackRegex = /^(address|blockquote|body|center|colgroup|dir|div|dl|fieldset|form|h[1-6]|hr|isindex|menu|noframes|noscript|ol|p|pre|table|ul|dd|dt|frameset|li|tbody|td|tfoot|th|thead|tr|html)$/i; | ||
| const isStackItem = (el) => stackRegex.test(el.nodeName); | ||
| const parseChildren = (el, ctx, parentStyles = {}) => { | ||
| const items = []; | ||
| const children = parseAsHTMLCollection(el) ? el.children : el.childNodes; | ||
| for (let i = 0; i < children.length; i++) { | ||
| const item = parseByRule(children[i], ctx, parentStyles); | ||
| if (item === null) { | ||
| continue; | ||
| } | ||
| const isNewline = !!item[META]?.[IS_NEWLINE]; | ||
| const prevItem = items[items.length - 1]; | ||
| if (ctx.config.collapseMargin && prevItem) { | ||
| collapseMargin(item, prevItem); | ||
| } | ||
| if (isNewline && (items.length === 0 || !children[i + 1] || prevItem && 'stack' in prevItem)) { | ||
| continue; | ||
| } | ||
| // Stack item | ||
| if (!('text' in item)) { | ||
| items.push(item); | ||
| continue; | ||
| } | ||
| const endWithNewLine = !!item[META]?.[END_WITH_NEWLINE]; | ||
| const startWithNewLine = !!item[META]?.[START_WITH_NEWLINE]; | ||
| const endWithWhiteSpace = !!item[META]?.[END_WITH_WHITESPACE]; | ||
| const startWithWhitespace = !!item[META]?.[START_WITH_WHITESPACE]; | ||
| const isWhitespace = !!item[META]?.[IS_WHITESPACE]; | ||
| const textItem = Array.isArray(item.text) | ||
| ? item : { text: [isWhitespace ? addWhitespace('newLine') : item] }; | ||
| if (!isNewline && !isWhitespace) { | ||
| // https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace | ||
| // add whitespace before | ||
| if (startWithNewLine || startWithWhitespace) { | ||
| textItem.text.unshift(addWhitespace(startWithNewLine ? 'startWithNewLine' : 'startWithWhitespace')); | ||
| } | ||
| // add whitespace after | ||
| if (endWithNewLine || endWithWhiteSpace) { | ||
| textItem.text.push(addWhitespace(endWithNewLine ? 'endWithNewLine' : 'endWithWhiteSpace')); | ||
| } | ||
| } | ||
| // Append text to last text element otherwise a new line is created | ||
| if (isTextArray(prevItem)) { | ||
| if (ctx.config.collapseWhitespace) { | ||
| collapseWhitespace(prevItem, textItem); | ||
| } | ||
| prevItem.text.push(textItem); | ||
| } | ||
| else { | ||
| // wrap so the next text items will be appended to it | ||
| items.push({ | ||
| text: [textItem] | ||
| }); | ||
| } | ||
| } | ||
| return items; | ||
| }; | ||
| const getNodeRule = (node) => { | ||
| const nodeName = node.nodeName.toLowerCase(); | ||
| switch (nodeName) { | ||
| case '#comment': | ||
| return () => null; | ||
| case '#text': | ||
| return parseText; | ||
| default: | ||
| return () => null; | ||
| } | ||
| }; | ||
| const getElementRule = (el) => { | ||
| const nodeName = el.nodeName.toLowerCase(); | ||
| switch (nodeName) { | ||
| case '#comment': | ||
| case 'option': // see <select> | ||
| case 'script': | ||
| case 'style': | ||
| case 'iframe': | ||
| case 'object': | ||
| return () => null; | ||
| case '#text': | ||
| return parseText; | ||
| case 'a': | ||
| return (el) => { | ||
| const href = el.getAttribute('href'); | ||
| if (!href) { | ||
| return parseElement(el); | ||
| } | ||
| const linkify = (item) => { | ||
| const children = getChildItems(item); | ||
| [].concat(children) | ||
| .forEach((link) => { | ||
| if (typeof link !== 'string') { | ||
| if (href[0] === '#') { | ||
| link.linkToDestination = href.slice(1); | ||
| } | ||
| else { | ||
| link.link = href; | ||
| } | ||
| } | ||
| linkify(link); | ||
| }); | ||
| }; | ||
| return { | ||
| text: items => { | ||
| items.forEach((link) => { | ||
| if (typeof link !== 'string') { | ||
| if (href[0] === '#') { | ||
| link.linkToDestination = href.slice(1); | ||
| } | ||
| else { | ||
| link.link = href; | ||
| } | ||
| } | ||
| linkify(link); | ||
| }); | ||
| return items; | ||
| } | ||
| }; | ||
| }; | ||
| case 'br': | ||
| return () => ({ | ||
| text: '\n', | ||
| [META]: { | ||
| [IS_NEWLINE]: true | ||
| } | ||
| }); | ||
| case 'qr-code': // CUSTOM | ||
| return (el) => { | ||
| const content = el.getAttribute('value'); | ||
| if (!content) { | ||
| return null; | ||
| } | ||
| const sizeAttr = el.getAttribute('data-size'); | ||
| const size = sizeAttr ? toUnit(sizeAttr) : toUnit('128px'); | ||
| return { | ||
| qr: content, | ||
| fit: size, | ||
| }; | ||
| }; | ||
| case 'toc': // CUSTOM | ||
| return (el) => { | ||
| const content = el.textContent; | ||
| if (!content) { | ||
| return null; | ||
| } | ||
| return { | ||
| toc: { | ||
| title: { | ||
| text: content, | ||
| bold: true, | ||
| fontSize: toUnit('22px'), | ||
| margin: [0, 10, 0, 10] | ||
| }, | ||
| }, | ||
| }; | ||
| }; | ||
| case 'table': | ||
| return parseTable; | ||
| case 'ul': | ||
| return () => { | ||
| return { | ||
| ul: (items) => items | ||
| }; | ||
| }; | ||
| case 'ol': | ||
| return () => { | ||
| return { | ||
| ol: (items) => items | ||
| }; | ||
| }; | ||
| case 'img': | ||
| return parseImg; | ||
| case 'svg': | ||
| return parseSvg; | ||
| case 'hr': | ||
| // TODO find better <hr> alternative? | ||
| return () => { | ||
| return { | ||
| table: { | ||
| widths: ['*'], | ||
| body: [ | ||
| [''], | ||
| ] | ||
| }, | ||
| style: ['hr'], | ||
| }; | ||
| }; | ||
| case 'input': | ||
| // TODO acro-form | ||
| return (el) => { | ||
| if (el instanceof HTMLInputElement) { | ||
| return { | ||
| text: 'value' in el ? el.value : '', | ||
| [META]: { | ||
| [IS_INPUT]: true | ||
| } | ||
| }; | ||
| } | ||
| return null; | ||
| }; | ||
| case 'select': | ||
| // TODO acro-form | ||
| return (el) => { | ||
| if (el instanceof HTMLSelectElement) { | ||
| const value = el.options[el.selectedIndex].value; | ||
| return { | ||
| text: value, | ||
| [META]: { | ||
| [IS_INPUT]: true | ||
| } | ||
| }; | ||
| } | ||
| return null; | ||
| }; | ||
| default: | ||
| return parseElement; | ||
| } | ||
| }; | ||
| const getItemByRule = (el, ctx) => { | ||
| if (typeof ctx.config.customRule === 'function') { | ||
| const result = ctx.config.customRule(el, ctx); | ||
| if (result === null) { | ||
| return null; | ||
| } | ||
| else if (result !== undefined) { | ||
| return result; | ||
| } | ||
| } | ||
| if (isElement(el)) { // ELEMENT_NODE | ||
| return getElementRule(el)(el, ctx); | ||
| } | ||
| else if (isNode(el)) { // TEXT_NODE || COMMENT_NODE | ||
| return getNodeRule(el)(el, ctx); | ||
| } | ||
| throw new Error('Unsupported Node Type: ' + el.nodeType); | ||
| }; | ||
| const processItems = (item, ctx, parentStyles = {}) => { | ||
| const el = item[META]?.[NODE]; | ||
| if (typeof item !== 'string' && el) { | ||
| const { cssStyles, props } = computeProps(el, item, ctx.styles, parentStyles); | ||
| Object.assign(item, props); | ||
| if ('stack' in item && typeof item.stack === 'function') { | ||
| const children = parseChildren(el, ctx, cssStyles); | ||
| item.stack = item.stack(children, ctx); | ||
| } | ||
| else if ('text' in item && typeof item.text === 'function') { | ||
| const children = parseChildren(el, ctx, cssStyles); | ||
| item.text = item.text(children.filter(isTextOrLeaf), ctx); | ||
| } | ||
| else if ('ul' in item && typeof item.ul === 'function') { | ||
| const children = parseChildren(el, ctx, cssStyles); | ||
| item.ul = item.ul(children, ctx); | ||
| } | ||
| else if ('ol' in item && typeof item.ol === 'function') { | ||
| const children = parseChildren(el, ctx, cssStyles); | ||
| item.ol = item.ol(children, ctx); | ||
| } | ||
| else if ('table' in item && typeof item.table.body === 'function') { | ||
| const children = parseChildren(el, ctx, cssStyles); | ||
| item.table.body = item.table.body(children, ctx); | ||
| } | ||
| } | ||
| return handleItem(item); | ||
| }; | ||
| const parseByRule = (el, ctx, parentStyles = {}) => { | ||
| const item = getItemByRule(el, ctx); | ||
| if (item === null) { | ||
| return null; | ||
| } | ||
| // Add ref to NODE | ||
| const meta = item[META] || {}; | ||
| meta[NODE] = el; | ||
| item[META] = meta; | ||
| return processItems(item, ctx, parentStyles); | ||
| }; | ||
| const parseElement = (el) => { | ||
| if (isStackItem(el)) { | ||
| return { | ||
| stack: (items) => items | ||
| }; | ||
| } | ||
| return { | ||
| text: (items) => { | ||
| // Return flat | ||
| if (items.length === 1 && 'text' in items[0] && Array.isArray(items[0].text)) { | ||
| return items[0].text; | ||
| } | ||
| return items; | ||
| } | ||
| }; | ||
| }; | ||
| const htmlToDom = (html) => { | ||
| if (typeof DOMParser !== 'undefined') { | ||
| const parser = new DOMParser(); | ||
| const doc = parser.parseFromString(html, 'text/html'); | ||
| return doc.body; | ||
| } | ||
| else if (typeof document !== 'undefined' && typeof document.createDocumentFragment === 'function') { | ||
| const fragment = document.createDocumentFragment(); | ||
| const doc = document.createElement('div'); | ||
| doc.innerHTML = html; | ||
| fragment.append(doc); | ||
| return fragment.children[0]; | ||
| } | ||
| throw new Error('Could not parse html to DOM. Please use external parser like jsdom.'); | ||
| }; | ||
| const parse = (input, _config = defaultConfig()) => { | ||
| const config = { | ||
| ...defaultConfig, | ||
| ..._config | ||
| }; | ||
| const ctx = new Context(config, Object.assign({}, config.globalStyles, config.styles)); | ||
| const body = typeof input === 'string' ? htmlToDom(input) : input; | ||
| const content = body !== null ? parseChildren(body, ctx) : []; | ||
| return { | ||
| content, | ||
| images: ctx.images, | ||
| patterns: getPatterns() | ||
| }; | ||
| }; | ||
| exports.parse = parse; | ||
| Object.defineProperty(exports, '__esModule', { value: true }); | ||
| })); |
+2
-0
| # html2pdfmake Changelog | ||
| ### [0.0.4](https://github.com/dantio/html2pdfmake/compare/v0.0.3...v0.0.4) (2022-05-20) | ||
| ### [0.0.3](https://github.com/dantio/html2pdfmake/compare/v0.0.2...v0.0.3) (2022-05-20) | ||
@@ -3,0 +5,0 @@ |
+4
-7
| { | ||
| "name": "html2pdfmake", | ||
| "version": "0.0.3", | ||
| "version": "0.0.4", | ||
| "description": "HTML/DOM to pdfmake", | ||
@@ -45,9 +45,7 @@ "type": "module", | ||
| }, | ||
| "module": "./dist/index.js", | ||
| "main": "./lib/index.js", | ||
| "types": "./lib/index.d.ts", | ||
| "module": "./dist/index.js", | ||
| "browser": "./lib/html2pdfmake.min.js", | ||
| "browser:esm": "./dist/html2pdfmake.min.mjs", | ||
| "jsdelivr": "./lib/html2pdfmake.min.js", | ||
| "unpkg": "./lib/html2pdfmake.min.js", | ||
| "browser": "./lib/html2pdfmake.js", | ||
| "browser:esm": "./dist/html2pdfmake.mjs", | ||
| "browserslist": "> 0.5%, last 2 versions, not dead", | ||
@@ -90,3 +88,2 @@ "scripts": { | ||
| "rollup": "^2.70.2", | ||
| "rollup-plugin-terser": "^7.0.2", | ||
| "standard-version": "^9.3.2", | ||
@@ -93,0 +90,0 @@ "ts-node": "^10.7.0", |
+5
-3
@@ -11,4 +11,6 @@ # html2pdfmake | ||
| ## Usage | ||
| ```js | ||
| import {parse} from 'html2pdfmake'; | ||
| ```html | ||
| <script type="module" src="https://cdn.jsdelivr.net/npm/html2pdfmake/dist/html2pdfmake.min.mjs"></script> | ||
| <script> | ||
| import {parse} from 'https://cdn.jsdelivr.net/npm/html2pdfmake/dist/html2pdfmake.min.mjs'; | ||
| const {content, images, patterns} = parse(document.getElementById('template')); | ||
@@ -22,3 +24,3 @@ | ||
| }) | ||
| </script> | ||
| ``` | ||
@@ -25,0 +27,0 @@ |
| const META = Symbol('__HTML2PDFMAKE'); | ||
| const NODE = 'NODE'; | ||
| const UID = 'UID'; | ||
| const END_WITH_NEWLINE = 'END_WITH_NEWLINE'; | ||
| const START_WITH_NEWLINE = 'START_WITH_NEW_LINE'; | ||
| const IS_NEWLINE = 'IS_NEWLINE'; | ||
| const START_WITH_WHITESPACE = 'START_WITH_WHITESPACE'; | ||
| const END_WITH_WHITESPACE = 'END_WITH_WHITESPACE'; | ||
| const IS_WHITESPACE = 'IS_WHITESPACE'; | ||
| const IS_INPUT = 'IS_INPUT'; | ||
| const MARGIN = 'MARGIN'; | ||
| const PADDING = 'PADDING'; | ||
| const POSITION = 'POSITION'; | ||
| const HANDLER = 'HANDLER'; | ||
| const PDFMAKE = 'PDFMAKE'; | ||
| const ITEMS = 'ITEMS'; // meta items | ||
| const STYLE = 'STYLE'; | ||
| const POS_TOP = 1; // CSS 0 | ||
| const POS_RIGHT = 2; // CSS 1 | ||
| const POS_BOTTOM = 3; // CSS 2 | ||
| const POS_LEFT = 0; // CSS 3 | ||
| const getPatterns = () => ({ | ||
| fill: { | ||
| boundingBox: [1, 1, 4, 4], | ||
| xStep: 1, | ||
| yStep: 1, | ||
| pattern: '1 w 0 1 m 4 5 l s 2 0 m 5 3 l s' | ||
| } | ||
| }); | ||
| class Context { | ||
| config; | ||
| styles; | ||
| images = {}; | ||
| constructor(config, styles) { | ||
| this.config = config; | ||
| this.styles = styles; | ||
| } | ||
| } | ||
| const globalStyles = () => ({ | ||
| ':root': { | ||
| 'font-size': '16px' | ||
| }, | ||
| h1: { | ||
| 'font-size': '32px', | ||
| 'margin-top': '21.44px', | ||
| 'margin-bottom': '21.44px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| h2: { | ||
| 'font-size': '24px', | ||
| 'margin-top': '19.92px', | ||
| 'margin-bottom': '19.92px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| h3: { | ||
| 'font-size': '18.72px', | ||
| 'margin-top': '18.72px', | ||
| 'margin-bottom': '18.72px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| h4: { | ||
| 'font-size': '16px', | ||
| 'margin-top': '21.28px', | ||
| 'margin-bottom': '21.28px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| h5: { | ||
| 'font-size': '13.28px', | ||
| 'margin-top': '22.17px', | ||
| 'margin-bottom': '22.17px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| h6: { | ||
| 'font-size': '10.72px', | ||
| 'margin-top': '24.97px', | ||
| 'margin-bottom': '24.97px', | ||
| 'font-weight': 'bold' | ||
| }, | ||
| b: { | ||
| 'font-weight': 'bold' | ||
| }, | ||
| strong: { | ||
| 'font-weight': 'bold' | ||
| }, | ||
| i: { | ||
| 'font-style': 'italic', | ||
| }, | ||
| em: { | ||
| 'font-style': 'italic' | ||
| }, | ||
| s: { | ||
| 'text-decoration': 'line-through' | ||
| }, | ||
| del: { | ||
| 'text-decoration': 'line-through' | ||
| }, | ||
| sub: { | ||
| 'font-size': '22px', | ||
| 'vertical-align': 'sub' | ||
| }, | ||
| small: { | ||
| 'font-size': '13px' | ||
| }, | ||
| u: { | ||
| 'text-decoration': 'underline' | ||
| }, | ||
| ul: { | ||
| 'margin-top': '16px', | ||
| 'margin-bottom': '16px', | ||
| 'padding-left': '20px' | ||
| }, | ||
| ol: { | ||
| 'margin-top': '16px', | ||
| 'margin-bottom': '16px', | ||
| 'padding-left': '20px' | ||
| }, | ||
| p: { | ||
| 'margin-top': '16px', | ||
| 'margin-bottom': '16px' | ||
| }, | ||
| table: { | ||
| border: 'none', | ||
| padding: '3px' | ||
| }, | ||
| td: { | ||
| border: 'none' | ||
| }, | ||
| tr: { | ||
| margin: '4px 0' | ||
| }, | ||
| th: { | ||
| 'font-weight': 'bold', | ||
| border: 'none', | ||
| 'text-align': 'center' | ||
| }, | ||
| a: { | ||
| color: '#0000ee', | ||
| 'text-decoration': 'underline' | ||
| }, | ||
| hr: { | ||
| 'border-top': '2px solid #9a9a9a', | ||
| 'border-bottom': '0', | ||
| 'border-left': '0px solid black', | ||
| 'border-right': '0', | ||
| margin: '8px 0' | ||
| } | ||
| }); | ||
| const defaultConfig = () => ({ | ||
| globalStyles: globalStyles(), | ||
| styles: {}, | ||
| collapseMargin: true, | ||
| collapseWhitespace: true, | ||
| }); | ||
| /** | ||
| * @description Method to check if an item is an object. Date and Function are considered | ||
| * an object, so if you need to exclude those, please update the method accordingly. | ||
| * @param item - The item that needs to be checked | ||
| * @return {Boolean} Whether or not @item is an object | ||
| */ | ||
| const isObject = (item) => { | ||
| return (item === Object(item) && !Array.isArray(item)); | ||
| }; | ||
| /** | ||
| * @description Method to perform a deep merge of objects | ||
| * @param {Object} target - The targeted object that needs to be merged with the supplied @sources | ||
| * @param {Array<Object>} sources - The source(s) that will be used to update the @target object | ||
| * @return {Object} The final merged object | ||
| */ | ||
| const merge = (target, ...sources) => { | ||
| // return the target if no sources passed | ||
| if (!sources.length) { | ||
| return target; | ||
| } | ||
| const result = target; | ||
| if (isObject(result)) { | ||
| for (let i = 0; i < sources.length; i += 1) { | ||
| if (isObject(sources[i])) { | ||
| const elm = sources[i]; | ||
| Object.keys(elm).forEach(key => { | ||
| if (isObject(elm[key])) { | ||
| if (!result[key] || !isObject(result[key])) { | ||
| result[key] = {}; | ||
| } | ||
| merge(result[key], elm[key]); | ||
| } | ||
| else { | ||
| result[key] = elm[key]; | ||
| } | ||
| }); | ||
| } | ||
| } | ||
| } | ||
| return result; | ||
| }; | ||
| const isNotText = (item) => typeof item !== 'string'; | ||
| const isColgroup = (item) => item?.[META]?.[NODE]?.nodeName === 'COLGROUP'; | ||
| const isImage = (item) => 'image' in item; | ||
| const isTable = (item) => 'table' in item; | ||
| const isTextArray = (item) => !!item && typeof item !== 'string' && 'text' in item && Array.isArray(item.text); | ||
| const isTextSimple = (item) => typeof item !== 'string' && 'text' in item && typeof item.text === 'string'; | ||
| const isTextOrLeaf = (item) => 'text' in item || typeof item === 'string'; | ||
| const isList = (item) => 'ul' in item || 'ol' in item; | ||
| const isTdOrTh = (item) => item[META]?.[NODE] && (item[META]?.[NODE]?.nodeName === 'TD' || item[META]?.[NODE]?.nodeName === 'TH'); | ||
| const isHeadline = (item) => item[META]?.[NODE] && (['H1', 'H2', 'H3', 'H4', 'H5', 'H6'].includes(item[META]?.[NODE]?.nodeName || '')); | ||
| const isElement = (el) => el.nodeType === 1; | ||
| const isNode = (el) => el.nodeType === 3 || el.nodeType === 8; | ||
| const isCollapsable = (item) => typeof item !== 'undefined' && typeof item !== 'string' && ('stack' in item || 'ul' in item || 'ol' in item) && 'margin' in item; | ||
| const getChildItems = (item) => { | ||
| if (typeof item === 'string') { | ||
| return []; | ||
| } | ||
| if ('stack' in item) { | ||
| return item.stack; | ||
| } | ||
| if ('text' in item && typeof item.text !== 'string') { | ||
| return item.text; | ||
| } | ||
| if ('table' in item) { | ||
| return item.table.body | ||
| .flatMap(tr => tr) | ||
| .filter(isNotText); | ||
| } | ||
| if ('ul' in item) { | ||
| return item.ul; | ||
| } | ||
| if ('ol' in item) { | ||
| return item.ol; | ||
| } | ||
| return []; | ||
| }; | ||
| const toUnit = (value, rootPt = 12) => { | ||
| // if it's just a number, then return it | ||
| if (typeof value === 'number') { | ||
| return isFinite(value) ? value : 0; | ||
| } | ||
| const val = Number(parseFloat(value)); | ||
| if (isNaN(val)) { | ||
| return 0; | ||
| } | ||
| const match = ('' + value).trim().match(/(pt|px|r?em|cm)$/); | ||
| if (!match) { | ||
| return val; | ||
| } | ||
| switch (match[1]) { | ||
| case 'em': | ||
| case 'rem': | ||
| return val * rootPt; | ||
| case 'px': | ||
| // 1px = 0.75 Point | ||
| return Number((val * 0.75).toFixed(2)); | ||
| case 'cm': | ||
| return Number((val * 28.34).toFixed(2)); | ||
| case 'mm': | ||
| return Number((val * 10 * 28.34).toFixed(2)); | ||
| default: | ||
| return val; | ||
| } | ||
| }; | ||
| const getUnitOrValue = (value) => typeof value === 'string' && (value.indexOf('%') > -1 || value.indexOf('auto') > -1) | ||
| ? value | ||
| : toUnit(value); | ||
| const toUnitOrValue = (value) => getUnitOrValue(value); | ||
| const toUnitsOrValues = (value) => value.map(v => getUnitOrValue(v)); | ||
| const expandValueToUnits = (value) => { | ||
| const values = toUnitsOrValues(value.split(' ') | ||
| .map(v => v.trim()) | ||
| .filter(v => v)); | ||
| if (values === null || !Array.isArray(values)) { | ||
| return null; | ||
| } | ||
| // values[0] = top | ||
| // values[1] = right | ||
| // values[2] = bottom | ||
| // values[3] = left | ||
| // pdfmake use left, top, right, bottom, || [horizontal, vertical] | ||
| // css use top, right, bottom, left | ||
| if (values.length === 1 && values[0] !== null) { | ||
| return [values[0], values[0], values[0], values[0]]; | ||
| } | ||
| else if (values.length === 2) { | ||
| // topbottom leftright | ||
| return [values[1], values[0], values[1], values[0]]; | ||
| } | ||
| else if (values.length === 3) { | ||
| // top bottom leftright | ||
| return [values[2], values[0], values[2], values[1]]; | ||
| } | ||
| else if (values.length === 4) { | ||
| return [values[3], values[0], values[1], values[2]]; | ||
| } | ||
| return null; | ||
| }; | ||
| const handleColumns = (item) => { | ||
| const childItems = getChildItems(item); | ||
| return { | ||
| columns: childItems | ||
| .flatMap((subItem) => { | ||
| if ('text' in subItem && Array.isArray(subItem.text)) { | ||
| return subItem.text | ||
| .filter(childItem => !childItem[META]?.[IS_WHITESPACE]) | ||
| .map(text => { | ||
| const width = toUnitOrValue(text[META]?.[STYLE]?.width || 'auto') || 'auto'; | ||
| return (typeof text === 'string') ? { | ||
| text, | ||
| width | ||
| } : { | ||
| ...text, | ||
| width | ||
| }; | ||
| }); | ||
| } | ||
| return { | ||
| stack: [].concat(subItem), | ||
| width: toUnitOrValue(subItem[META]?.[STYLE]?.width || 'auto') || 'auto' | ||
| }; | ||
| }), | ||
| columnGap: 'columnGap' in item ? item.columnGap : 0 | ||
| }; | ||
| }; | ||
| const handleImg = (image) => { | ||
| if (isImage(image) && typeof image.width === 'number' && typeof image.height === 'number') { | ||
| image.fit = [image.width, image.height]; | ||
| } | ||
| return image; | ||
| }; | ||
| const handleTable = (item) => { | ||
| if (isTable(item)) { | ||
| const bodyItem = item.table.body[0]?.[0]; | ||
| const tableItem = bodyItem && typeof bodyItem !== 'string' && 'table' in bodyItem ? bodyItem : null; | ||
| if (!tableItem) { | ||
| return item; | ||
| } | ||
| const innerTable = tableItem.table; | ||
| const colgroup = bodyItem[META]?.[ITEMS]?.colgroup; | ||
| if (colgroup && Array.isArray(colgroup)) { | ||
| innerTable.widths = innerTable.widths || []; | ||
| colgroup.forEach((col, i) => { | ||
| if (col[META]?.[STYLE]?.width && innerTable.widths) { | ||
| innerTable.widths[i] = getUnitOrValue(col[META]?.[STYLE]?.width || 'auto'); | ||
| } | ||
| }); | ||
| } | ||
| const trs = bodyItem[META]?.[ITEMS]?.trs; | ||
| if (Array.isArray(trs)) { | ||
| trs.forEach((tr, i) => { | ||
| if (tr[META]?.[STYLE]?.height && innerTable.heights) { | ||
| innerTable.heights[i] = getUnitOrValue(tr[META]?.[STYLE]?.height || 'auto'); | ||
| } | ||
| }); | ||
| } | ||
| const paddingsTopBottom = {}; | ||
| const paddingsLeftRight = {}; | ||
| innerTable.body | ||
| .forEach((row, trIndex) => { | ||
| row.forEach((column, tdIndex) => { | ||
| if (typeof column !== 'string') { | ||
| if (column[META]?.[PADDING]) { | ||
| paddingsTopBottom[trIndex] = paddingsTopBottom[trIndex] || [0, 0]; | ||
| paddingsLeftRight[tdIndex] = paddingsLeftRight[tdIndex] || [0, 0]; | ||
| paddingsTopBottom[trIndex] = [ | ||
| Math.max(paddingsTopBottom[trIndex][0], column[META]?.[PADDING]?.[POS_TOP] || 0), | ||
| Math.max(paddingsTopBottom[trIndex][1], column[META]?.[PADDING]?.[POS_BOTTOM] || 0) | ||
| ]; | ||
| paddingsLeftRight[tdIndex] = [ | ||
| Math.max(paddingsLeftRight[tdIndex][0], column[META]?.[PADDING]?.[POS_LEFT] || 0), | ||
| Math.max(paddingsLeftRight[tdIndex][1], column[META]?.[PADDING]?.[POS_RIGHT] || 0) | ||
| ]; | ||
| } | ||
| column.style = column.style || []; | ||
| column.style.push(tdIndex % 2 === 0 ? 'td:nth-child(even)' : 'td:nth-child(odd)'); | ||
| column.style.push(trIndex % 2 === 0 ? 'tr:nth-child(even)' : 'tr:nth-child(odd)'); | ||
| } | ||
| }); | ||
| }); | ||
| const tableLayout = {}; | ||
| const hasPaddingTopBottom = Object.keys(paddingsTopBottom).length > 0; | ||
| const hasPaddingLeftRight = Object.keys(paddingsLeftRight).length > 0; | ||
| if (hasPaddingTopBottom) { | ||
| tableLayout.paddingTop = (i) => { | ||
| if (paddingsTopBottom[i]) { | ||
| return paddingsTopBottom[i][0]; | ||
| } | ||
| return 0; | ||
| }; | ||
| tableLayout.paddingBottom = (i) => { | ||
| if (paddingsTopBottom[i]) { | ||
| return paddingsTopBottom[i][1]; | ||
| } | ||
| return 0; | ||
| }; | ||
| } | ||
| if (hasPaddingLeftRight) { | ||
| tableLayout.paddingRight = (i) => { | ||
| if (paddingsLeftRight[i]) { | ||
| return paddingsLeftRight[i][1]; | ||
| } | ||
| return 0; | ||
| }; | ||
| tableLayout.paddingLeft = (i) => { | ||
| if (paddingsLeftRight[i]) { | ||
| return paddingsLeftRight[i][0]; | ||
| } | ||
| return 0; | ||
| }; | ||
| } | ||
| if (hasPaddingLeftRight || hasPaddingTopBottom) { | ||
| tableItem.layout = tableLayout; | ||
| } | ||
| } | ||
| return item; | ||
| }; | ||
| const addTocItem = (item, tocStyle = {}) => { | ||
| if ('text' in item && typeof item.text === 'string') { | ||
| item.tocItem = true; | ||
| merge(item, tocStyle); | ||
| } | ||
| else if ('stack' in item) { | ||
| const text = item.stack.find(s => 'text' in s); | ||
| if (text && typeof text !== 'string') { | ||
| text.tocItem = true; | ||
| merge(text, tocStyle); | ||
| } | ||
| } | ||
| }; | ||
| const handleHeadlineToc = (item) => { | ||
| const tocStyle = {}; | ||
| if (item[META]?.[NODE]?.nodeName === 'H1') { | ||
| Object.assign(tocStyle, { | ||
| tocNumberStyle: { bold: true } | ||
| }); | ||
| } | ||
| else { | ||
| Object.assign(tocStyle, { | ||
| tocMargin: [10, 0, 0, 0] | ||
| }); | ||
| } | ||
| addTocItem(item, tocStyle); | ||
| return item; | ||
| }; | ||
| const handleItem = (item) => { | ||
| if (typeof item !== 'string' && item[META]?.[PDFMAKE]) { | ||
| merge(item, item[META]?.[PDFMAKE] || {}); | ||
| } | ||
| if (typeof item[META]?.[HANDLER] === 'function') { | ||
| return item[META]?.[HANDLER]?.(item) || null; | ||
| } | ||
| return item; | ||
| }; | ||
| let _uid = 0; | ||
| const getUniqueId = (item) => { | ||
| const meta = item[META] || {}; | ||
| const __uid = meta[UID]; | ||
| const el = item[META]?.[NODE]; | ||
| if (__uid) { | ||
| return __uid; | ||
| } | ||
| if (!el) { | ||
| return '#' + (_uid++); | ||
| } | ||
| if (isElement(el)) { | ||
| const id = el.getAttribute('id'); | ||
| if (id) { | ||
| return id; | ||
| } | ||
| } | ||
| const nodeName = el.nodeName.toLowerCase(); | ||
| // TODO add parent? Or name something else? | ||
| const uid = '#' + nodeName + '-' + (_uid++); | ||
| meta[UID] = uid; | ||
| item[META] = meta; | ||
| return uid; | ||
| }; | ||
| const attrToProps = (item) => { | ||
| const el = item[META]?.[NODE]; | ||
| if (!el || !('getAttribute' in el)) | ||
| return { [META]: { [STYLE]: {} } }; | ||
| const cssClass = el.getAttribute('class') || ''; | ||
| const cssClasses = [...new Set(cssClass.split(' ') | ||
| .filter((value) => value) | ||
| .map((value) => '.' + value.trim()))]; | ||
| const nodeName = el.nodeName.toLowerCase(); | ||
| const parentNodeName = el.parentNode ? el.parentNode.nodeName.toLowerCase() : null; | ||
| const styleNames = [ | ||
| nodeName, | ||
| ].concat(cssClasses); | ||
| if (cssClasses.length > 2) { | ||
| styleNames.push(cssClasses.join('')); // .a.b.c | ||
| } | ||
| if (parentNodeName) { | ||
| styleNames.push(parentNodeName + '>' + nodeName); | ||
| } | ||
| const uniqueId = getUniqueId(item); | ||
| styleNames.push(uniqueId); // Should be the last one | ||
| const props = { | ||
| [META]: { | ||
| [STYLE]: item[META]?.[STYLE] || {}, | ||
| ...(item[META] || {}) | ||
| }, | ||
| style: [...new Set((item.style || []).concat(styleNames))] | ||
| }; | ||
| for (let i = 0; i < el.attributes.length; i++) { | ||
| const name = el.attributes[i].name; | ||
| const value = el.getAttribute(name)?.trim() || null; | ||
| if (value == null) { | ||
| continue; | ||
| } | ||
| props[META][STYLE][name] = value; | ||
| switch (name) { | ||
| case 'rowspan': | ||
| props.rowSpan = parseInt(value, 10); | ||
| break; | ||
| case 'colspan': | ||
| props.colSpan = parseInt(value, 10); | ||
| break; | ||
| case 'value': | ||
| if (nodeName === 'li') { | ||
| props.counter = parseInt(value, 10); | ||
| } | ||
| break; | ||
| case 'start': // ol | ||
| if (nodeName === 'ol') { | ||
| props.start = parseInt(value, 10); | ||
| } | ||
| break; | ||
| case 'width': | ||
| if ('image' in item) { | ||
| props.width = toUnit(value); | ||
| } | ||
| break; | ||
| case 'height': | ||
| if ('image' in item) { | ||
| props.height = toUnit(value); | ||
| } | ||
| break; | ||
| case 'data-fit': | ||
| if (value === 'true') { | ||
| const width = el.getAttribute('width'); | ||
| const height = el.getAttribute('height'); | ||
| if (width && height) { | ||
| props.fit = [toUnit(width), toUnit(height)]; | ||
| } | ||
| } | ||
| break; | ||
| case 'data-toc-item': | ||
| if (value !== 'false') { | ||
| let toc = {}; | ||
| if (value) { | ||
| try { | ||
| toc = JSON.parse(value); | ||
| } | ||
| catch (e) { | ||
| console.warn('Not valid JSON format.', value); | ||
| } | ||
| } | ||
| props[META][HANDLER] = (item) => { | ||
| addTocItem(item, toc); | ||
| return item; | ||
| }; | ||
| } | ||
| break; | ||
| case 'data-pdfmake': | ||
| if (value) { | ||
| try { | ||
| props[META][PDFMAKE] = JSON.parse(value); | ||
| } | ||
| catch (e) { | ||
| console.warn('Not valid JSON format.', value); | ||
| } | ||
| } | ||
| break; | ||
| } | ||
| } | ||
| return props; | ||
| }; | ||
| const inheritStyle = (styles) => { | ||
| // TODO what do we want to exclude ? | ||
| const pick = { | ||
| color: true, | ||
| 'font-family': true, | ||
| 'font-size': true, | ||
| 'font-weight': true, | ||
| 'font': true, | ||
| 'line-height': true, | ||
| 'list-style-type': true, | ||
| 'list-style': true, | ||
| 'text-align': true, | ||
| // TODO only if parent is text: [] | ||
| background: true, | ||
| 'font-style': true, | ||
| 'background-color': true, | ||
| 'font-feature-settings': true, | ||
| 'white-space': true, | ||
| 'vertical-align': true, | ||
| 'opacity': true, | ||
| 'text-decoration': true, | ||
| }; | ||
| return Object.keys(styles).reduce((p, c) => { | ||
| if (pick[c] || styles[c] === 'inherit') { | ||
| p[c] = styles[c]; | ||
| } | ||
| return p; | ||
| }, {}); | ||
| }; | ||
| const getBorderStyle = (value) => { | ||
| const border = value.split(' '); | ||
| const color = border[2] || 'black'; | ||
| const borderStyle = border[1] || 'solid'; | ||
| const width = toUnit(border[0]); | ||
| return { color, width, borderStyle }; | ||
| }; | ||
| const computeBorder = (item, props, directive, value) => { | ||
| const { color, width, borderStyle } = getBorderStyle(value); | ||
| const tdOrTh = isTdOrTh(item); | ||
| const setBorder = (index) => { | ||
| props.border = item.border || props.border || [false, false, false, false]; | ||
| props.borderColor = item.borderColor || props.borderColor || ['black', 'black', 'black', 'black']; | ||
| if (value === 'none') { | ||
| props.border[index] = false; | ||
| } | ||
| else { | ||
| props.border[index] = true; | ||
| props.borderColor[index] = color; | ||
| } | ||
| }; | ||
| switch (directive) { | ||
| case 'border': | ||
| if (isTable(item)) { | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| if (value === 'none') { | ||
| props.layout.hLineWidth = () => 0; | ||
| props.layout.vLineWidth = () => 0; | ||
| break; | ||
| } | ||
| props.layout.vLineColor = () => color; | ||
| props.layout.hLineColor = () => color; | ||
| props.layout.hLineWidth = (i, node) => (i === 0 || i === node.table.body.length) ? width : 0; | ||
| props.layout.vLineWidth = (i, node) => (i === 0 || i === node.table.widths?.length) ? width : 0; | ||
| if (borderStyle === 'dashed') { | ||
| props.layout.hLineStyle = () => ({ dash: { length: 2, space: 2 } }); | ||
| props.layout.vLineStyle = () => ({ dash: { length: 2, space: 2 } }); | ||
| } | ||
| } | ||
| else if (tdOrTh) { | ||
| setBorder(0); | ||
| setBorder(1); | ||
| setBorder(2); | ||
| setBorder(3); | ||
| } | ||
| break; | ||
| case 'border-bottom': | ||
| if (isTable(item)) { | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| const hLineWidth = props.layout.hLineWidth || (() => 0); | ||
| const hLineColor = props.layout.hLineColor || (() => 'black'); | ||
| props.layout.hLineWidth = (i, node) => (i === node.table.body.length) ? width : hLineWidth(i, node); | ||
| props.layout.hLineColor = (i, node) => (i === node.table.body.length) ? color : hLineColor(i, node); | ||
| } | ||
| else if (tdOrTh) { | ||
| setBorder(3); | ||
| } | ||
| break; | ||
| case 'border-top': | ||
| if (isTable(item)) { | ||
| const { color, width } = getBorderStyle(value); | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| const hLineWidth = props.layout.hLineWidth || (() => 1); | ||
| const hLineColor = props.layout.hLineColor || (() => 'black'); | ||
| props.layout.hLineWidth = (i, node) => (i === 0) ? width : hLineWidth(i, node); | ||
| props.layout.hLineColor = (i, node) => (i === 0) ? color : hLineColor(i, node); | ||
| } | ||
| else if (tdOrTh) { | ||
| setBorder(1); | ||
| } | ||
| break; | ||
| case 'border-right': | ||
| if (isTable(item)) { | ||
| const { color, width } = getBorderStyle(value); | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| const vLineWidth = props.layout.vLineWidth || (() => 1); | ||
| const vLineColor = props.layout.vLineColor || (() => 'black'); | ||
| props.layout.vLineWidth = (i, node) => (node.table.body.length === 1 ? i === node.table.body.length : i % node.table.body.length !== 0) ? width : vLineWidth(i, node); | ||
| props.layout.vLineColor = (i, node) => (node.table.body.length === 1 ? i === node.table.body.length : i % node.table.body.length !== 0) ? color : vLineColor(i, node); | ||
| } | ||
| else if (tdOrTh) { | ||
| setBorder(2); | ||
| } | ||
| break; | ||
| case 'border-left': | ||
| if (isTable(item)) { | ||
| const { color, width } = getBorderStyle(value); | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| const vLineWidth = props.layout.vLineWidth || (() => 1); | ||
| const vLineColor = props.layout.vLineColor || (() => 'black'); | ||
| props.layout.vLineWidth = (i, node) => (node.table.body.length === 1 ? i === 0 : i % node.table.body.length === 0) ? width : vLineWidth(i, node); | ||
| props.layout.vLineColor = (i, node) => (node.table.body.length === 1 ? i === 0 : i % node.table.body.length === 0) ? color : vLineColor(i, node); | ||
| } | ||
| else if (tdOrTh) { | ||
| setBorder(0); | ||
| } | ||
| break; | ||
| } | ||
| }; | ||
| const computeMargin = (itemProps, item, value, index) => { | ||
| const margin = itemProps[META][MARGIN] || item[META]?.[MARGIN] || [0, 0, 0, 0]; | ||
| margin[index] = value; | ||
| itemProps[META][MARGIN] = [...margin]; | ||
| itemProps.margin = margin; | ||
| const padding = itemProps[META][PADDING] || item[META]?.[PADDING] || [0, 0, 0, 0]; | ||
| const paddingValue = padding[index] || 0; | ||
| itemProps.margin[index] = value + paddingValue; | ||
| }; | ||
| const computePadding = (props, item, value, index) => { | ||
| if (isTable(item)) { | ||
| props.layout = item.layout || props.layout || {}; | ||
| if (typeof props.layout === 'string') { | ||
| props.layout = {}; | ||
| } | ||
| switch (index) { | ||
| case POS_LEFT: | ||
| props.layout.paddingLeft = () => toUnit(value); | ||
| break; | ||
| case POS_TOP: | ||
| props.layout.paddingTop = () => toUnit(value); | ||
| break; | ||
| case POS_RIGHT: | ||
| props.layout.paddingRight = () => toUnit(value); | ||
| break; | ||
| case POS_BOTTOM: | ||
| props.layout.paddingBottom = () => toUnit(value); | ||
| break; | ||
| default: | ||
| throw new Error('Unsupported index for padding: ' + index); | ||
| } | ||
| } | ||
| else { | ||
| const padding = props[META][PADDING] || item[META]?.[PADDING] || [0, 0, 0, 0]; | ||
| padding[index] = value; | ||
| props[META][PADDING] = [...padding]; | ||
| if (!isTdOrTh(item)) { | ||
| const margin = props[META][MARGIN] || item[META]?.[MARGIN] || [0, 0, 0, 0]; | ||
| props.margin = margin; | ||
| const marginValue = margin[index]; | ||
| props.margin[index] = value + marginValue; | ||
| } | ||
| } | ||
| }; | ||
| const styleToProps = (item, styles, parentStyles = {}) => { | ||
| const props = { | ||
| [META]: { | ||
| [STYLE]: {}, | ||
| ...(item[META] || {}), | ||
| } | ||
| }; | ||
| const meta = props[META]; | ||
| const image = isImage(item); | ||
| const table = isTable(item); | ||
| const text = isTextSimple(item); | ||
| const list = isList(item); | ||
| const rootFontSize = toUnit(parentStyles['font-size'] || '16px'); | ||
| if (isHeadline(item)) { | ||
| meta[HANDLER] = handleHeadlineToc; | ||
| } | ||
| Object.keys(styles).forEach((key) => { | ||
| const directive = key; | ||
| const value = ('' + styles[key]).trim(); | ||
| props[META][STYLE][directive] = value; | ||
| switch (directive) { | ||
| case 'padding': { | ||
| const paddings = expandValueToUnits(value); | ||
| if (table && paddings !== null) { | ||
| let layout = props.layout || item.layout || {}; | ||
| if (typeof layout === 'string') { | ||
| layout = {}; | ||
| } | ||
| layout.paddingLeft = () => Number(paddings[POS_LEFT]); | ||
| layout.paddingRight = () => Number(paddings[POS_RIGHT]); | ||
| layout.paddingTop = (i) => (i === 0) ? Number(paddings[POS_TOP]) : 0; | ||
| layout.paddingBottom = (i, node) => (i === node.table.body.length - 1) ? Number(paddings[POS_BOTTOM]) : 0; | ||
| props.layout = layout; | ||
| } | ||
| else if (paddings !== null) { | ||
| computePadding(props, item, Number(paddings[POS_TOP]), POS_TOP); | ||
| computePadding(props, item, Number(paddings[POS_LEFT]), POS_LEFT); | ||
| computePadding(props, item, Number(paddings[POS_RIGHT]), POS_RIGHT); | ||
| computePadding(props, item, Number(paddings[POS_BOTTOM]), POS_BOTTOM); | ||
| } | ||
| break; | ||
| } | ||
| case 'border': | ||
| case 'border-bottom': | ||
| case 'border-top': | ||
| case 'border-right': | ||
| case 'border-left': | ||
| computeBorder(item, props, directive, value); | ||
| break; | ||
| case 'font-size': { | ||
| props.fontSize = toUnit(value, rootFontSize); | ||
| break; | ||
| } | ||
| case 'line-height': | ||
| props.lineHeight = toUnit(value, rootFontSize); | ||
| break; | ||
| case 'letter-spacing': | ||
| props.characterSpacing = toUnit(value); | ||
| break; | ||
| case 'text-align': | ||
| props.alignment = value; | ||
| break; | ||
| case 'font-feature-settings': { | ||
| const settings = value.split(',').filter(s => s).map(s => s.replace(/['"]/g, '')); | ||
| const fontFeatures = item.fontFeatures || props.fontFeatures || []; | ||
| fontFeatures.push(...settings); | ||
| props.fontFeatures = fontFeatures; | ||
| break; | ||
| } | ||
| case 'font-weight': | ||
| switch (value) { | ||
| case 'bold': | ||
| props.bold = true; | ||
| break; | ||
| case 'normal': | ||
| props.bold = false; | ||
| break; | ||
| } | ||
| break; | ||
| case 'text-decoration': | ||
| switch (value) { | ||
| case 'underline': | ||
| props.decoration = 'underline'; | ||
| break; | ||
| case 'line-through': | ||
| props.decoration = 'lineThrough'; | ||
| break; | ||
| case 'overline': | ||
| props.decoration = 'overline'; | ||
| break; | ||
| } | ||
| break; | ||
| case 'text-decoration-color': | ||
| props.decorationColor = value; | ||
| break; | ||
| case 'text-decoration-style': | ||
| props.decorationStyle = value; | ||
| break; | ||
| case 'vertical-align': | ||
| if (value === 'sub') { | ||
| props.sub = true; | ||
| } | ||
| break; | ||
| case 'font-style': | ||
| switch (value) { | ||
| case 'italic': | ||
| props.italics = true; | ||
| break; | ||
| } | ||
| break; | ||
| case 'font-family': | ||
| props.font = value; | ||
| break; | ||
| case 'color': | ||
| props.color = value; | ||
| break; | ||
| case 'background': | ||
| case 'background-color': | ||
| if (table) { | ||
| let layout = item.layout || {}; | ||
| if (typeof layout === 'string') { | ||
| layout = {}; | ||
| } | ||
| layout.fillColor = () => value; | ||
| props.layout = layout; | ||
| } | ||
| else if (isTdOrTh(item)) { | ||
| props.fillColor = value; | ||
| } | ||
| else { | ||
| props.background = ['fill', value]; | ||
| } | ||
| break; | ||
| case 'margin': { | ||
| const margin = expandValueToUnits(value)?.map(value => typeof value === 'string' ? 0 : value); | ||
| if (margin) { | ||
| computeMargin(props, item, margin[POS_TOP], POS_TOP); | ||
| computeMargin(props, item, margin[POS_LEFT], POS_LEFT); | ||
| computeMargin(props, item, margin[POS_RIGHT], POS_RIGHT); | ||
| computeMargin(props, item, margin[POS_BOTTOM], POS_BOTTOM); | ||
| } | ||
| break; | ||
| } | ||
| case 'margin-left': | ||
| computeMargin(props, item, toUnit(value), POS_LEFT); | ||
| break; | ||
| case 'margin-top': | ||
| computeMargin(props, item, toUnit(value), POS_TOP); | ||
| break; | ||
| case 'margin-right': | ||
| computeMargin(props, item, toUnit(value), POS_RIGHT); | ||
| break; | ||
| case 'margin-bottom': | ||
| computeMargin(props, item, toUnit(value), POS_BOTTOM); | ||
| break; | ||
| case 'padding-left': | ||
| computePadding(props, item, toUnit(value), POS_LEFT); | ||
| break; | ||
| case 'padding-top': | ||
| computePadding(props, item, toUnit(value), POS_TOP); | ||
| break; | ||
| case 'padding-right': | ||
| computePadding(props, item, toUnit(value), POS_RIGHT); | ||
| break; | ||
| case 'padding-bottom': | ||
| computePadding(props, item, toUnit(value), POS_BOTTOM); | ||
| break; | ||
| case 'page-break-before': | ||
| if (value === 'always') { | ||
| props.pageBreak = 'before'; | ||
| } | ||
| break; | ||
| case 'page-break-after': | ||
| if (value === 'always') { | ||
| props.pageBreak = 'after'; | ||
| } | ||
| break; | ||
| case 'position': | ||
| if (value === 'absolute') { | ||
| meta[POSITION] = 'absolute'; | ||
| props.absolutePosition = {}; | ||
| } | ||
| else if (value === 'relative') { | ||
| meta[POSITION] = 'relative'; | ||
| props.relativePosition = {}; | ||
| } | ||
| break; | ||
| case 'left': | ||
| case 'top': | ||
| // TODO can be set before postion:absolute! | ||
| if (!props.absolutePosition && !props.relativePosition) { | ||
| console.error(directive + ' is set, but no absolute/relative position.'); | ||
| break; | ||
| } | ||
| if (props.absolutePosition) { | ||
| if (directive === 'left') { | ||
| props.absolutePosition.x = toUnit(value); | ||
| } | ||
| else if (directive === 'top') { | ||
| props.absolutePosition.y = toUnit(value); | ||
| } | ||
| } | ||
| else if (props.relativePosition) { | ||
| if (directive === 'left') { | ||
| props.relativePosition.x = toUnit(value); | ||
| } | ||
| else if (directive === 'top') { | ||
| props.relativePosition.y = toUnit(value); | ||
| } | ||
| } | ||
| else { | ||
| console.error(directive + ' is set, but no absolute/relative position.'); | ||
| break; | ||
| } | ||
| break; | ||
| case 'white-space': | ||
| if (value === 'pre' && meta[NODE]) { | ||
| if (text) { | ||
| props.text = meta[NODE]?.textContent || ''; | ||
| } | ||
| props.preserveLeadingSpaces = true; | ||
| } | ||
| break; | ||
| case 'display': | ||
| if (value === 'flex') { | ||
| props[META][HANDLER] = handleColumns; | ||
| } | ||
| else if (value === 'none') { | ||
| props[META][HANDLER] = () => null; | ||
| } | ||
| break; | ||
| case 'opacity': | ||
| props.opacity = Number(parseFloat(value)); | ||
| break; | ||
| case 'gap': | ||
| props.columnGap = toUnit(value); | ||
| break; | ||
| case 'list-style-type': | ||
| case 'list-style': | ||
| if (list) { | ||
| props.type = value; | ||
| } | ||
| else { | ||
| props.listType = value; | ||
| } | ||
| break; | ||
| case 'width': | ||
| if (table) { | ||
| if (value === '100%') { | ||
| item.table.widths = ['*']; | ||
| } | ||
| else { | ||
| const width = toUnitOrValue(value); | ||
| if (width !== null) { | ||
| item.table.widths = [width]; | ||
| } | ||
| } | ||
| } | ||
| else if (image) { | ||
| props.width = toUnit(value); | ||
| } | ||
| break; | ||
| case 'height': | ||
| if (image) { | ||
| props.height = toUnit(value); | ||
| } | ||
| break; | ||
| case 'max-height': | ||
| if (image) { | ||
| props.maxHeight = toUnit(value); | ||
| } | ||
| break; | ||
| case 'max-width': | ||
| if (image) { | ||
| props.maxWidth = toUnit(value); | ||
| } | ||
| break; | ||
| case 'min-height': | ||
| if (image) { | ||
| props.minHeight = toUnit(value); | ||
| } | ||
| break; | ||
| case 'min-width': | ||
| if (image) { | ||
| props.minWidth = toUnit(value); | ||
| } | ||
| break; | ||
| case 'object-fit': | ||
| if (value === 'contain' && image) { | ||
| meta[HANDLER] = handleImg; | ||
| } | ||
| break; | ||
| } | ||
| }); | ||
| return props; | ||
| }; | ||
| /** | ||
| * @param el DOM Element | ||
| */ | ||
| const getInlineStyles = (el) => ('getAttribute' in el ? el.getAttribute('style') || '' : '').split(';') | ||
| .map(style => style.trim().toLowerCase().split(':')) | ||
| .filter(style => style.length === 2) | ||
| .reduce((style, value) => { | ||
| style[value[0].trim()] = value[1].trim(); | ||
| return style; | ||
| }, {}); | ||
| const getDefaultStyles = (el, item, styles) => (item.style || []).concat(el.nodeName.toLowerCase()) | ||
| .filter((selector) => styles && styles[selector]) | ||
| .reduce((style, selector) => { | ||
| return { | ||
| ...style, | ||
| ...styles[selector] | ||
| }; | ||
| }, {}); | ||
| /** | ||
| * | ||
| * @param el DOM Element | ||
| * @param item | ||
| * @param styles additional styles | ||
| * @param parentStyles pick styles | ||
| */ | ||
| const computeProps = (el, item, styles, parentStyles = {}) => { | ||
| const defaultStyles = getDefaultStyles(el, item, styles); | ||
| const rootStyles = styles[':root'] || globalStyles()[':root']; | ||
| const inheritedStyles = inheritStyle(parentStyles); | ||
| const cssStyles = Object.assign({}, defaultStyles, inheritedStyles, getInlineStyles(el)); | ||
| const styleProps = styleToProps(item, cssStyles, Object.assign({}, rootStyles, inheritedStyles)); | ||
| const attrProps = attrToProps(item); | ||
| const props = { | ||
| ...styleProps, | ||
| ...attrProps, | ||
| [META]: { | ||
| ...(styleProps[META] || {}), | ||
| ...(attrProps[META] || {}), | ||
| [STYLE]: { | ||
| ...(styleProps[META][STYLE] || {}), | ||
| ...(attrProps[META][STYLE] || {}), | ||
| } | ||
| } | ||
| }; | ||
| return { | ||
| cssStyles, | ||
| props | ||
| }; | ||
| }; | ||
| const collapseMargin = (item, prevItem) => { | ||
| if (isCollapsable(item) && isCollapsable(prevItem)) { | ||
| const prevMargin = prevItem[META]?.[MARGIN] || [0, 0, 0, 0]; | ||
| prevItem[META] = { ...(prevItem[META] || {}), [MARGIN]: prevMargin }; | ||
| prevItem.margin[POS_BOTTOM] = prevItem[META]?.[PADDING]?.[POS_BOTTOM] || 0; | ||
| const itemMargin = item[META]?.[MARGIN] || [0, 0, 0, 0]; | ||
| const marginTop = Math.max(itemMargin[POS_TOP], prevMargin[POS_BOTTOM]); | ||
| itemMargin[POS_TOP] = marginTop; | ||
| prevMargin[POS_BOTTOM] = 0; | ||
| item[META] = { ...(item[META] || {}), [MARGIN]: itemMargin }; | ||
| item.margin[POS_TOP] = marginTop + (item[META]?.[PADDING]?.[POS_TOP] || 0); | ||
| } | ||
| }; | ||
| const findLastDeep = (ta) => { | ||
| const last = ta.text.at(-1); | ||
| if (isTextArray(last)) { | ||
| return findLastDeep(last); | ||
| } | ||
| return last; | ||
| }; | ||
| const findFirstArrayDeep = (ta) => { | ||
| const first = ta.text.at(0); | ||
| if (isTextArray(first)) { | ||
| return findFirstArrayDeep(first); | ||
| } | ||
| return ta.text; | ||
| }; | ||
| const collapseWhitespace = (item, nextText) => { | ||
| const prevLastText = findLastDeep(item); | ||
| const nextFirstTextArray = findFirstArrayDeep(nextText); | ||
| if (prevLastText && prevLastText[META]?.[IS_WHITESPACE] && nextFirstTextArray[0][META]?.[IS_WHITESPACE]) { | ||
| nextFirstTextArray.shift(); | ||
| } | ||
| }; | ||
| function isBase64(str) { | ||
| return /^data:image\/(jpeg|png|jpg);base64,/.test(str); | ||
| } | ||
| const parseImg = (el, ctx) => { | ||
| const src = el.getAttribute('src'); | ||
| if (!src) { | ||
| return null; | ||
| } | ||
| const name = el.getAttribute('name') || src; | ||
| let image; | ||
| if (isBase64(src)) { | ||
| image = src; | ||
| } | ||
| else if (ctx.images[name]) { | ||
| image = name; | ||
| } | ||
| else { | ||
| ctx.images[src] = name; | ||
| image = name; | ||
| } | ||
| return { | ||
| image, | ||
| [META]: {} | ||
| }; | ||
| }; | ||
| const parseSvg = (el) => { | ||
| // TODO is this okay? | ||
| const svgEl = el.cloneNode(true); | ||
| const width = el.getAttribute('width'); | ||
| const height = el.getAttribute('height'); | ||
| if (width) { | ||
| svgEl.setAttribute('width', '' + getUnitOrValue(width)); | ||
| } | ||
| if (height) { | ||
| svgEl.setAttribute('height', '' + getUnitOrValue(height)); | ||
| } | ||
| return { | ||
| svg: svgEl.outerHTML.replace(/\n(\s+)?/g, ''), | ||
| }; | ||
| }; | ||
| const parseTable = () => { | ||
| // TODO table in table? | ||
| return { | ||
| table: { | ||
| body: (items) => { | ||
| // tbody -> tr | ||
| const colgroup = items.find(isColgroup)?.stack[0] || []; | ||
| const tbody = items.filter(item => !isColgroup(item)); | ||
| const trs = tbody.flatMap((item) => 'stack' in item ? item.stack : []); | ||
| const body = trs.map((item) => getChildItems(item)); | ||
| if (body.length === 0) { | ||
| return []; | ||
| } | ||
| const longestRow = body.reduce((a, b) => a.length <= b.length ? b : a); | ||
| const table = { | ||
| body, | ||
| widths: new Array(longestRow.length).fill('auto'), | ||
| heights: new Array(trs.length).fill('auto') | ||
| }; | ||
| return [[{ | ||
| table, | ||
| layout: { | ||
| defaultBorder: false | ||
| }, | ||
| [META]: { | ||
| [ITEMS]: { | ||
| colgroup: 'text' in colgroup && Array.isArray(colgroup.text) ? colgroup.text : [], | ||
| trs | ||
| }, | ||
| } | ||
| }]]; | ||
| }, | ||
| // widths: ['*'], | ||
| }, | ||
| [META]: { | ||
| [HANDLER]: handleTable, | ||
| }, | ||
| layout: {} | ||
| }; | ||
| }; | ||
| const parseText = (el) => { | ||
| const text = el.textContent; | ||
| if (text === null) { | ||
| return null; | ||
| } | ||
| const keepNewLines = text.replace(/[^\S\r\n]+/, ''); | ||
| const trimmedText = text.replace(/\n|\t| +/g, ' ') | ||
| .replace(/^ +/, '') | ||
| .replace(/ +$/, ''); | ||
| //.trim() removes also | ||
| const endWithNL = keepNewLines[keepNewLines.length - 1] === '\n'; | ||
| const startWithNL = keepNewLines[0] === '\n'; | ||
| const startWithWhitespace = text[0] === ' '; | ||
| const endWithWhitespace = text[text.length - 1] === ' '; | ||
| return { | ||
| text: trimmedText, | ||
| [META]: { | ||
| [START_WITH_NEWLINE]: startWithNL, | ||
| [END_WITH_NEWLINE]: endWithNL, | ||
| [IS_NEWLINE]: startWithNL && endWithNL && trimmedText.length === 0, | ||
| [START_WITH_WHITESPACE]: startWithWhitespace, | ||
| [END_WITH_WHITESPACE]: endWithWhitespace, | ||
| [IS_WHITESPACE]: startWithWhitespace && endWithWhitespace && text.length === 1, | ||
| }, | ||
| }; | ||
| }; | ||
| const WHITESPACE = ' '; | ||
| const addWhitespace = (type) => ({ | ||
| text: WHITESPACE, | ||
| [META]: { | ||
| [IS_WHITESPACE]: type | ||
| } | ||
| }); | ||
| const parseAsHTMLCollection = (el) => ['TABLE', 'TBODY', 'TR', 'COLGROUP', 'COL', 'UL', 'OL', 'SELECT'].includes(el.nodeName) && 'children' in el; | ||
| const stackRegex = /^(address|blockquote|body|center|colgroup|dir|div|dl|fieldset|form|h[1-6]|hr|isindex|menu|noframes|noscript|ol|p|pre|table|ul|dd|dt|frameset|li|tbody|td|tfoot|th|thead|tr|html)$/i; | ||
| const isStackItem = (el) => stackRegex.test(el.nodeName); | ||
| const parseChildren = (el, ctx, parentStyles = {}) => { | ||
| const items = []; | ||
| const children = parseAsHTMLCollection(el) ? el.children : el.childNodes; | ||
| for (let i = 0; i < children.length; i++) { | ||
| const item = parseByRule(children[i], ctx, parentStyles); | ||
| if (item === null) { | ||
| continue; | ||
| } | ||
| const isNewline = !!item[META]?.[IS_NEWLINE]; | ||
| const prevItem = items[items.length - 1]; | ||
| if (ctx.config.collapseMargin && prevItem) { | ||
| collapseMargin(item, prevItem); | ||
| } | ||
| if (isNewline && (items.length === 0 || !children[i + 1] || prevItem && 'stack' in prevItem)) { | ||
| continue; | ||
| } | ||
| // Stack item | ||
| if (!('text' in item)) { | ||
| items.push(item); | ||
| continue; | ||
| } | ||
| const endWithNewLine = !!item[META]?.[END_WITH_NEWLINE]; | ||
| const startWithNewLine = !!item[META]?.[START_WITH_NEWLINE]; | ||
| const endWithWhiteSpace = !!item[META]?.[END_WITH_WHITESPACE]; | ||
| const startWithWhitespace = !!item[META]?.[START_WITH_WHITESPACE]; | ||
| const isWhitespace = !!item[META]?.[IS_WHITESPACE]; | ||
| const textItem = Array.isArray(item.text) | ||
| ? item : { text: [isWhitespace ? addWhitespace('newLine') : item] }; | ||
| if (!isNewline && !isWhitespace) { | ||
| // https://developer.mozilla.org/en-US/docs/Web/API/Document_Object_Model/Whitespace | ||
| // add whitespace before | ||
| if (startWithNewLine || startWithWhitespace) { | ||
| textItem.text.unshift(addWhitespace(startWithNewLine ? 'startWithNewLine' : 'startWithWhitespace')); | ||
| } | ||
| // add whitespace after | ||
| if (endWithNewLine || endWithWhiteSpace) { | ||
| textItem.text.push(addWhitespace(endWithNewLine ? 'endWithNewLine' : 'endWithWhiteSpace')); | ||
| } | ||
| } | ||
| // Append text to last text element otherwise a new line is created | ||
| if (isTextArray(prevItem)) { | ||
| if (ctx.config.collapseWhitespace) { | ||
| collapseWhitespace(prevItem, textItem); | ||
| } | ||
| prevItem.text.push(textItem); | ||
| } | ||
| else { | ||
| // wrap so the next text items will be appended to it | ||
| items.push({ | ||
| text: [textItem] | ||
| }); | ||
| } | ||
| } | ||
| return items; | ||
| }; | ||
| const getNodeRule = (node) => { | ||
| const nodeName = node.nodeName.toLowerCase(); | ||
| switch (nodeName) { | ||
| case '#comment': | ||
| return () => null; | ||
| case '#text': | ||
| return parseText; | ||
| default: | ||
| return () => null; | ||
| } | ||
| }; | ||
| const getElementRule = (el) => { | ||
| const nodeName = el.nodeName.toLowerCase(); | ||
| switch (nodeName) { | ||
| case '#comment': | ||
| case 'option': // see <select> | ||
| case 'script': | ||
| case 'style': | ||
| case 'iframe': | ||
| case 'object': | ||
| return () => null; | ||
| case '#text': | ||
| return parseText; | ||
| case 'a': | ||
| return (el) => { | ||
| const href = el.getAttribute('href'); | ||
| if (!href) { | ||
| return parseElement(el); | ||
| } | ||
| const linkify = (item) => { | ||
| const children = getChildItems(item); | ||
| [].concat(children) | ||
| .forEach((link) => { | ||
| if (typeof link !== 'string') { | ||
| if (href[0] === '#') { | ||
| link.linkToDestination = href.slice(1); | ||
| } | ||
| else { | ||
| link.link = href; | ||
| } | ||
| } | ||
| linkify(link); | ||
| }); | ||
| }; | ||
| return { | ||
| text: items => { | ||
| items.forEach((link) => { | ||
| if (typeof link !== 'string') { | ||
| if (href[0] === '#') { | ||
| link.linkToDestination = href.slice(1); | ||
| } | ||
| else { | ||
| link.link = href; | ||
| } | ||
| } | ||
| linkify(link); | ||
| }); | ||
| return items; | ||
| } | ||
| }; | ||
| }; | ||
| case 'br': | ||
| return () => ({ | ||
| text: '\n', | ||
| [META]: { | ||
| [IS_NEWLINE]: true | ||
| } | ||
| }); | ||
| case 'qr-code': // CUSTOM | ||
| return (el) => { | ||
| const content = el.getAttribute('value'); | ||
| if (!content) { | ||
| return null; | ||
| } | ||
| const sizeAttr = el.getAttribute('data-size'); | ||
| const size = sizeAttr ? toUnit(sizeAttr) : toUnit('128px'); | ||
| return { | ||
| qr: content, | ||
| fit: size, | ||
| }; | ||
| }; | ||
| case 'toc': // CUSTOM | ||
| return (el) => { | ||
| const content = el.textContent; | ||
| if (!content) { | ||
| return null; | ||
| } | ||
| return { | ||
| toc: { | ||
| title: { | ||
| text: content, | ||
| bold: true, | ||
| fontSize: toUnit('22px'), | ||
| margin: [0, 10, 0, 10] | ||
| }, | ||
| }, | ||
| }; | ||
| }; | ||
| case 'table': | ||
| return parseTable; | ||
| case 'ul': | ||
| return () => { | ||
| return { | ||
| ul: (items) => items | ||
| }; | ||
| }; | ||
| case 'ol': | ||
| return () => { | ||
| return { | ||
| ol: (items) => items | ||
| }; | ||
| }; | ||
| case 'img': | ||
| return parseImg; | ||
| case 'svg': | ||
| return parseSvg; | ||
| case 'hr': | ||
| // TODO find better <hr> alternative? | ||
| return () => { | ||
| return { | ||
| table: { | ||
| widths: ['*'], | ||
| body: [ | ||
| [''], | ||
| ] | ||
| }, | ||
| style: ['hr'], | ||
| }; | ||
| }; | ||
| case 'input': | ||
| // TODO acro-form | ||
| return (el) => { | ||
| if (el instanceof HTMLInputElement) { | ||
| return { | ||
| text: 'value' in el ? el.value : '', | ||
| [META]: { | ||
| [IS_INPUT]: true | ||
| } | ||
| }; | ||
| } | ||
| return null; | ||
| }; | ||
| case 'select': | ||
| // TODO acro-form | ||
| return (el) => { | ||
| if (el instanceof HTMLSelectElement) { | ||
| const value = el.options[el.selectedIndex].value; | ||
| return { | ||
| text: value, | ||
| [META]: { | ||
| [IS_INPUT]: true | ||
| } | ||
| }; | ||
| } | ||
| return null; | ||
| }; | ||
| default: | ||
| return parseElement; | ||
| } | ||
| }; | ||
| const getItemByRule = (el, ctx) => { | ||
| if (typeof ctx.config.customRule === 'function') { | ||
| const result = ctx.config.customRule(el, ctx); | ||
| if (result === null) { | ||
| return null; | ||
| } | ||
| else if (result !== undefined) { | ||
| return result; | ||
| } | ||
| } | ||
| if (isElement(el)) { // ELEMENT_NODE | ||
| return getElementRule(el)(el, ctx); | ||
| } | ||
| else if (isNode(el)) { // TEXT_NODE || COMMENT_NODE | ||
| return getNodeRule(el)(el, ctx); | ||
| } | ||
| throw new Error('Unsupported Node Type: ' + el.nodeType); | ||
| }; | ||
| const processItems = (item, ctx, parentStyles = {}) => { | ||
| const el = item[META]?.[NODE]; | ||
| if (typeof item !== 'string' && el) { | ||
| const { cssStyles, props } = computeProps(el, item, ctx.styles, parentStyles); | ||
| Object.assign(item, props); | ||
| if ('stack' in item && typeof item.stack === 'function') { | ||
| const children = parseChildren(el, ctx, cssStyles); | ||
| item.stack = item.stack(children, ctx); | ||
| } | ||
| else if ('text' in item && typeof item.text === 'function') { | ||
| const children = parseChildren(el, ctx, cssStyles); | ||
| item.text = item.text(children.filter(isTextOrLeaf), ctx); | ||
| } | ||
| else if ('ul' in item && typeof item.ul === 'function') { | ||
| const children = parseChildren(el, ctx, cssStyles); | ||
| item.ul = item.ul(children, ctx); | ||
| } | ||
| else if ('ol' in item && typeof item.ol === 'function') { | ||
| const children = parseChildren(el, ctx, cssStyles); | ||
| item.ol = item.ol(children, ctx); | ||
| } | ||
| else if ('table' in item && typeof item.table.body === 'function') { | ||
| const children = parseChildren(el, ctx, cssStyles); | ||
| item.table.body = item.table.body(children, ctx); | ||
| } | ||
| } | ||
| return handleItem(item); | ||
| }; | ||
| const parseByRule = (el, ctx, parentStyles = {}) => { | ||
| const item = getItemByRule(el, ctx); | ||
| if (item === null) { | ||
| return null; | ||
| } | ||
| // Add ref to NODE | ||
| const meta = item[META] || {}; | ||
| meta[NODE] = el; | ||
| item[META] = meta; | ||
| return processItems(item, ctx, parentStyles); | ||
| }; | ||
| const parseElement = (el) => { | ||
| if (isStackItem(el)) { | ||
| return { | ||
| stack: (items) => items | ||
| }; | ||
| } | ||
| return { | ||
| text: (items) => { | ||
| // Return flat | ||
| if (items.length === 1 && 'text' in items[0] && Array.isArray(items[0].text)) { | ||
| return items[0].text; | ||
| } | ||
| return items; | ||
| } | ||
| }; | ||
| }; | ||
| const htmlToDom = (html) => { | ||
| if (typeof DOMParser !== 'undefined') { | ||
| const parser = new DOMParser(); | ||
| const doc = parser.parseFromString(html, 'text/html'); | ||
| return doc.body; | ||
| } | ||
| else if (typeof document !== 'undefined' && typeof document.createDocumentFragment === 'function') { | ||
| const fragment = document.createDocumentFragment(); | ||
| const doc = document.createElement('div'); | ||
| doc.innerHTML = html; | ||
| fragment.append(doc); | ||
| return fragment.children[0]; | ||
| } | ||
| throw new Error('Could not parse html to DOM. Please use external parser like jsdom.'); | ||
| }; | ||
| const parse = (input, _config = defaultConfig()) => { | ||
| const config = { | ||
| ...defaultConfig, | ||
| ..._config | ||
| }; | ||
| const ctx = new Context(config, Object.assign({}, config.globalStyles, config.styles)); | ||
| const body = typeof input === 'string' ? htmlToDom(input) : input; | ||
| const content = body !== null ? parseChildren(body, ctx) : []; | ||
| return { | ||
| content, | ||
| images: ctx.images, | ||
| patterns: getPatterns() | ||
| }; | ||
| }; | ||
| export { parse }; |
| !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).html2pdfmake={})}(this,(function(t){"use strict";const e=Symbol("__HTML2PDFMAKE"),o="END_WITH_NEWLINE",n="START_WITH_NEW_LINE",r="IS_NEWLINE",a="START_WITH_WHITESPACE",i="END_WITH_WHITESPACE",l="IS_WHITESPACE",s="IS_INPUT",c="MARGIN",u="HANDLER",d="ITEMS",p="STYLE";class b{config;styles;images={};constructor(t,e){this.config=t,this.styles=e}}const g=()=>({globalStyles:{":root":{"font-size":"16px"},h1:{"font-size":"32px","margin-top":"21.44px","margin-bottom":"21.44px","font-weight":"bold"},h2:{"font-size":"24px","margin-top":"19.92px","margin-bottom":"19.92px","font-weight":"bold"},h3:{"font-size":"18.72px","margin-top":"18.72px","margin-bottom":"18.72px","font-weight":"bold"},h4:{"font-size":"16px","margin-top":"21.28px","margin-bottom":"21.28px","font-weight":"bold"},h5:{"font-size":"13.28px","margin-top":"22.17px","margin-bottom":"22.17px","font-weight":"bold"},h6:{"font-size":"10.72px","margin-top":"24.97px","margin-bottom":"24.97px","font-weight":"bold"},b:{"font-weight":"bold"},strong:{"font-weight":"bold"},i:{"font-style":"italic"},em:{"font-style":"italic"},s:{"text-decoration":"line-through"},del:{"text-decoration":"line-through"},sub:{"font-size":"22px","vertical-align":"sub"},small:{"font-size":"13px"},u:{"text-decoration":"underline"},ul:{"margin-top":"16px","margin-bottom":"16px","padding-left":"20px"},ol:{"margin-top":"16px","margin-bottom":"16px","padding-left":"20px"},p:{"margin-top":"16px","margin-bottom":"16px"},table:{border:"none",padding:"3px"},td:{border:"none"},tr:{margin:"4px 0"},th:{"font-weight":"bold",border:"none","text-align":"center"},a:{color:"#0000ee","text-decoration":"underline"},hr:{"border-top":"2px solid #9a9a9a","border-bottom":"0","border-left":"0px solid black","border-right":"0",margin:"8px 0"}},styles:{},collapseMargin:!0,collapseWhitespace:!0}),h=t=>t===Object(t)&&!Array.isArray(t),f=(t,...e)=>{if(!e.length)return t;const o=t;if(h(o))for(let t=0;t<e.length;t+=1)if(h(e[t])){const n=e[t];Object.keys(n).forEach((t=>{h(n[t])?(o[t]&&h(o[t])||(o[t]={}),f(o[t],n[t])):o[t]=n[t]}))}return o},y=t=>"string"!=typeof t,m=t=>"COLGROUP"===t?.[e]?.NODE?.nodeName,x=t=>"image"in t,k=t=>"table"in t,N=t=>!!t&&"string"!=typeof t&&"text"in t&&Array.isArray(t.text),w=t=>"text"in t||"string"==typeof t,E=t=>t[e]?.NODE&&("TD"===t[e]?.NODE?.nodeName||"TH"===t[e]?.NODE?.nodeName),A=t=>1===t.nodeType,L=t=>void 0!==t&&"string"!=typeof t&&("stack"in t||"ul"in t||"ol"in t)&&"margin"in t,I=t=>"string"==typeof t?[]:"stack"in t?t.stack:"text"in t&&"string"!=typeof t.text?t.text:"table"in t?t.table.body.flatMap((t=>t)).filter(y):"ul"in t?t.ul:"ol"in t?t.ol:[],S=(t,e=12)=>{if("number"==typeof t)return isFinite(t)?t:0;const o=Number(parseFloat(t));if(isNaN(o))return 0;const n=(""+t).trim().match(/(pt|px|r?em|cm)$/);if(!n)return o;switch(n[1]){case"em":case"rem":return o*e;case"px":return Number((.75*o).toFixed(2));case"cm":return Number((28.34*o).toFixed(2));case"mm":return Number((10*o*28.34).toFixed(2));default:return o}},T=t=>"string"==typeof t&&(t.indexOf("%")>-1||t.indexOf("auto")>-1)?t:S(t),D=t=>T(t),O=t=>{const e=(t=>t.map((t=>T(t))))(t.split(" ").map((t=>t.trim())).filter((t=>t)));return null!==e&&Array.isArray(e)?1===e.length&&null!==e[0]?[e[0],e[0],e[0],e[0]]:2===e.length?[e[1],e[0],e[1],e[0]]:3===e.length?[e[2],e[0],e[2],e[1]]:4===e.length?[e[3],e[0],e[1],e[2]]:null:null},W=t=>({columns:I(t).flatMap((t=>"text"in t&&Array.isArray(t.text)?t.text.filter((t=>!t[e]?.IS_WHITESPACE)).map((t=>{const o=D(t[e]?.STYLE?.width||"auto")||"auto";return"string"==typeof t?{text:t,width:o}:{...t,width:o}})):{stack:[].concat(t),width:D(t[e]?.STYLE?.width||"auto")||"auto"})),columnGap:"columnGap"in t?t.columnGap:0}),v=t=>(x(t)&&"number"==typeof t.width&&"number"==typeof t.height&&(t.fit=[t.width,t.height]),t),P=t=>{if(k(t)){const o=t.table.body[0]?.[0],n=o&&"string"!=typeof o&&"table"in o?o:null;if(!n)return t;const r=n.table,a=o[e]?.ITEMS?.colgroup;a&&Array.isArray(a)&&(r.widths=r.widths||[],a.forEach(((t,o)=>{t[e]?.STYLE?.width&&r.widths&&(r.widths[o]=T(t[e]?.STYLE?.width||"auto"))})));const i=o[e]?.ITEMS?.trs;Array.isArray(i)&&i.forEach(((t,o)=>{t[e]?.STYLE?.height&&r.heights&&(r.heights[o]=T(t[e]?.STYLE?.height||"auto"))}));const l={},s={};r.body.forEach(((t,o)=>{t.forEach(((t,n)=>{"string"!=typeof t&&(t[e]?.PADDING&&(l[o]=l[o]||[0,0],s[n]=s[n]||[0,0],l[o]=[Math.max(l[o][0],t[e]?.PADDING?.[1]||0),Math.max(l[o][1],t[e]?.PADDING?.[3]||0)],s[n]=[Math.max(s[n][0],t[e]?.PADDING?.[0]||0),Math.max(s[n][1],t[e]?.PADDING?.[2]||0)]),t.style=t.style||[],t.style.push(n%2==0?"td:nth-child(even)":"td:nth-child(odd)"),t.style.push(o%2==0?"tr:nth-child(even)":"tr:nth-child(odd)"))}))}));const c={},u=Object.keys(l).length>0,d=Object.keys(s).length>0;u&&(c.paddingTop=t=>l[t]?l[t][0]:0,c.paddingBottom=t=>l[t]?l[t][1]:0),d&&(c.paddingRight=t=>s[t]?s[t][1]:0,c.paddingLeft=t=>s[t]?s[t][0]:0),(d||u)&&(n.layout=c)}return t},C=(t,e={})=>{if("text"in t&&"string"==typeof t.text)t.tocItem=!0,f(t,e);else if("stack"in t){const o=t.stack.find((t=>"text"in t));o&&"string"!=typeof o&&(o.tocItem=!0,f(o,e))}},H=t=>{const o={};return"H1"===t[e]?.NODE?.nodeName?Object.assign(o,{tocNumberStyle:{bold:!0}}):Object.assign(o,{tocMargin:[10,0,0,0]}),C(t,o),t};let M=0;const _=t=>{const o=t[e]?.NODE;if(!o||!("getAttribute"in o))return{[e]:{[p]:{}}};const n=o.getAttribute("class")||"",r=[...new Set(n.split(" ").filter((t=>t)).map((t=>"."+t.trim())))],a=o.nodeName.toLowerCase(),i=o.parentNode?o.parentNode.nodeName.toLowerCase():null,l=[a].concat(r);r.length>2&&l.push(r.join("")),i&&l.push(i+">"+a);const s=(t=>{const o=t[e]||{},n=o.UID,r=t[e]?.NODE;if(n)return n;if(!r)return"#"+M++;if(A(r)){const t=r.getAttribute("id");if(t)return t}const a="#"+r.nodeName.toLowerCase()+"-"+M++;return o.UID=a,t[e]=o,a})(t);l.push(s);const c={[e]:{[p]:t[e]?.STYLE||{},...t[e]||{}},style:[...new Set((t.style||[]).concat(l))]};for(let n=0;n<o.attributes.length;n++){const r=o.attributes[n].name,i=o.getAttribute(r)?.trim()||null;if(null!=i)switch(c[e].STYLE[r]=i,r){case"rowspan":c.rowSpan=parseInt(i,10);break;case"colspan":c.colSpan=parseInt(i,10);break;case"value":"li"===a&&(c.counter=parseInt(i,10));break;case"start":"ol"===a&&(c.start=parseInt(i,10));break;case"width":"image"in t&&(c.width=S(i));break;case"height":"image"in t&&(c.height=S(i));break;case"data-fit":if("true"===i){const t=o.getAttribute("width"),e=o.getAttribute("height");t&&e&&(c.fit=[S(t),S(e)])}break;case"data-toc-item":if("false"!==i){let t={};if(i)try{t=JSON.parse(i)}catch(t){console.warn("Not valid JSON format.",i)}c[e].HANDLER=e=>(C(e,t),e)}break;case"data-pdfmake":if(i)try{c[e].PDFMAKE=JSON.parse(i)}catch(t){console.warn("Not valid JSON format.",i)}}}return c},R=t=>{const e=t.split(" "),o=e[2]||"black",n=e[1]||"solid";return{color:o,width:S(e[0]),borderStyle:n}},G=(t,o,n,r)=>{const a=t[e].MARGIN||o[e]?.MARGIN||[0,0,0,0];a[r]=n,t[e].MARGIN=[...a],t.margin=a;const i=(t[e].PADDING||o[e]?.PADDING||[0,0,0,0])[r]||0;t.margin[r]=n+i},z=(t,o,n,r)=>{if(k(o))switch(t.layout=o.layout||t.layout||{},"string"==typeof t.layout&&(t.layout={}),r){case 0:t.layout.paddingLeft=()=>S(n);break;case 1:t.layout.paddingTop=()=>S(n);break;case 2:t.layout.paddingRight=()=>S(n);break;case 3:t.layout.paddingBottom=()=>S(n);break;default:throw new Error("Unsupported index for padding: "+r)}else{const a=t[e].PADDING||o[e]?.PADDING||[0,0,0,0];if(a[r]=n,t[e].PADDING=[...a],!E(o)){const a=t[e].MARGIN||o[e]?.MARGIN||[0,0,0,0];t.margin=a;const i=a[r];t.margin[r]=n+i}}},j=(t,o,n={})=>{const r={[e]:{[p]:{},...t[e]||{}}},a=r[e],i=x(t),l=k(t),s=(t=>"string"!=typeof t&&"text"in t&&"string"==typeof t.text)(t),c=(t=>"ul"in t||"ol"in t)(t),u=S(n["font-size"]||"16px");return(t=>t[e]?.NODE&&["H1","H2","H3","H4","H5","H6"].includes(t[e]?.NODE?.nodeName||""))(t)&&(a.HANDLER=H),Object.keys(o).forEach((n=>{const d=n,p=(""+o[n]).trim();switch(r[e].STYLE[d]=p,d){case"padding":{const e=O(p);if(l&&null!==e){let o=r.layout||t.layout||{};"string"==typeof o&&(o={}),o.paddingLeft=()=>Number(e[0]),o.paddingRight=()=>Number(e[2]),o.paddingTop=t=>0===t?Number(e[1]):0,o.paddingBottom=(t,o)=>t===o.table.body.length-1?Number(e[3]):0,r.layout=o}else null!==e&&(z(r,t,Number(e[1]),1),z(r,t,Number(e[0]),0),z(r,t,Number(e[2]),2),z(r,t,Number(e[3]),3));break}case"border":case"border-bottom":case"border-top":case"border-right":case"border-left":((t,e,o,n)=>{const{color:r,width:a,borderStyle:i}=R(n),l=E(t),s=o=>{e.border=t.border||e.border||[!1,!1,!1,!1],e.borderColor=t.borderColor||e.borderColor||["black","black","black","black"],"none"===n?e.border[o]=!1:(e.border[o]=!0,e.borderColor[o]=r)};switch(o){case"border":if(k(t)){if(e.layout=t.layout||e.layout||{},"string"==typeof e.layout&&(e.layout={}),"none"===n){e.layout.hLineWidth=()=>0,e.layout.vLineWidth=()=>0;break}e.layout.vLineColor=()=>r,e.layout.hLineColor=()=>r,e.layout.hLineWidth=(t,e)=>0===t||t===e.table.body.length?a:0,e.layout.vLineWidth=(t,e)=>0===t||t===e.table.widths?.length?a:0,"dashed"===i&&(e.layout.hLineStyle=()=>({dash:{length:2,space:2}}),e.layout.vLineStyle=()=>({dash:{length:2,space:2}}))}else l&&(s(0),s(1),s(2),s(3));break;case"border-bottom":if(k(t)){e.layout=t.layout||e.layout||{},"string"==typeof e.layout&&(e.layout={});const o=e.layout.hLineWidth||(()=>0),n=e.layout.hLineColor||(()=>"black");e.layout.hLineWidth=(t,e)=>t===e.table.body.length?a:o(t,e),e.layout.hLineColor=(t,e)=>t===e.table.body.length?r:n(t,e)}else l&&s(3);break;case"border-top":if(k(t)){const{color:o,width:r}=R(n);e.layout=t.layout||e.layout||{},"string"==typeof e.layout&&(e.layout={});const a=e.layout.hLineWidth||(()=>1),i=e.layout.hLineColor||(()=>"black");e.layout.hLineWidth=(t,e)=>0===t?r:a(t,e),e.layout.hLineColor=(t,e)=>0===t?o:i(t,e)}else l&&s(1);break;case"border-right":if(k(t)){const{color:o,width:r}=R(n);e.layout=t.layout||e.layout||{},"string"==typeof e.layout&&(e.layout={});const a=e.layout.vLineWidth||(()=>1),i=e.layout.vLineColor||(()=>"black");e.layout.vLineWidth=(t,e)=>(1===e.table.body.length?t===e.table.body.length:t%e.table.body.length!=0)?r:a(t,e),e.layout.vLineColor=(t,e)=>(1===e.table.body.length?t===e.table.body.length:t%e.table.body.length!=0)?o:i(t,e)}else l&&s(2);break;case"border-left":if(k(t)){const{color:o,width:r}=R(n);e.layout=t.layout||e.layout||{},"string"==typeof e.layout&&(e.layout={});const a=e.layout.vLineWidth||(()=>1),i=e.layout.vLineColor||(()=>"black");e.layout.vLineWidth=(t,e)=>(1===e.table.body.length?0===t:t%e.table.body.length==0)?r:a(t,e),e.layout.vLineColor=(t,e)=>(1===e.table.body.length?0===t:t%e.table.body.length==0)?o:i(t,e)}else l&&s(0)}})(t,r,d,p);break;case"font-size":r.fontSize=S(p,u);break;case"line-height":r.lineHeight=S(p,u);break;case"letter-spacing":r.characterSpacing=S(p);break;case"text-align":r.alignment=p;break;case"font-feature-settings":{const e=p.split(",").filter((t=>t)).map((t=>t.replace(/['"]/g,""))),o=t.fontFeatures||r.fontFeatures||[];o.push(...e),r.fontFeatures=o;break}case"font-weight":switch(p){case"bold":r.bold=!0;break;case"normal":r.bold=!1}break;case"text-decoration":switch(p){case"underline":r.decoration="underline";break;case"line-through":r.decoration="lineThrough";break;case"overline":r.decoration="overline"}break;case"text-decoration-color":r.decorationColor=p;break;case"text-decoration-style":r.decorationStyle=p;break;case"vertical-align":"sub"===p&&(r.sub=!0);break;case"font-style":if("italic"===p)r.italics=!0;break;case"font-family":r.font=p;break;case"color":r.color=p;break;case"background":case"background-color":if(l){let e=t.layout||{};"string"==typeof e&&(e={}),e.fillColor=()=>p,r.layout=e}else E(t)?r.fillColor=p:r.background=["fill",p];break;case"margin":{const e=O(p)?.map((t=>"string"==typeof t?0:t));e&&(G(r,t,e[1],1),G(r,t,e[0],0),G(r,t,e[2],2),G(r,t,e[3],3));break}case"margin-left":G(r,t,S(p),0);break;case"margin-top":G(r,t,S(p),1);break;case"margin-right":G(r,t,S(p),2);break;case"margin-bottom":G(r,t,S(p),3);break;case"padding-left":z(r,t,S(p),0);break;case"padding-top":z(r,t,S(p),1);break;case"padding-right":z(r,t,S(p),2);break;case"padding-bottom":z(r,t,S(p),3);break;case"page-break-before":"always"===p&&(r.pageBreak="before");break;case"page-break-after":"always"===p&&(r.pageBreak="after");break;case"position":"absolute"===p?(a.POSITION="absolute",r.absolutePosition={}):"relative"===p&&(a.POSITION="relative",r.relativePosition={});break;case"left":case"top":if(!r.absolutePosition&&!r.relativePosition){console.error(d+" is set, but no absolute/relative position.");break}if(r.absolutePosition)"left"===d?r.absolutePosition.x=S(p):"top"===d&&(r.absolutePosition.y=S(p));else{if(!r.relativePosition){console.error(d+" is set, but no absolute/relative position.");break}"left"===d?r.relativePosition.x=S(p):"top"===d&&(r.relativePosition.y=S(p))}break;case"white-space":"pre"===p&&a.NODE&&(s&&(r.text=a.NODE?.textContent||""),r.preserveLeadingSpaces=!0);break;case"display":"flex"===p?r[e].HANDLER=W:"none"===p&&(r[e].HANDLER=()=>null);break;case"opacity":r.opacity=Number(parseFloat(p));break;case"gap":r.columnGap=S(p);break;case"list-style-type":case"list-style":c?r.type=p:r.listType=p;break;case"width":if(l)if("100%"===p)t.table.widths=["*"];else{const e=D(p);null!==e&&(t.table.widths=[e])}else i&&(r.width=S(p));break;case"height":i&&(r.height=S(p));break;case"max-height":i&&(r.maxHeight=S(p));break;case"max-width":i&&(r.maxWidth=S(p));break;case"min-height":i&&(r.minHeight=S(p));break;case"min-width":i&&(r.minWidth=S(p));break;case"object-fit":"contain"===p&&i&&(a.HANDLER=v)}})),r},F=(t,o,n,r={})=>{const a=((t,e,o)=>(e.style||[]).concat(t.nodeName.toLowerCase()).filter((t=>o&&o[t])).reduce(((t,e)=>({...t,...o[e]})),{}))(t,o,n),i=n[":root"]||{"font-size":"16px"},l=(t=>{const e={color:!0,"font-family":!0,"font-size":!0,"font-weight":!0,font:!0,"line-height":!0,"list-style-type":!0,"list-style":!0,"text-align":!0,background:!0,"font-style":!0,"background-color":!0,"font-feature-settings":!0,"white-space":!0,"vertical-align":!0,opacity:!0,"text-decoration":!0};return Object.keys(t).reduce(((o,n)=>((e[n]||"inherit"===t[n])&&(o[n]=t[n]),o)),{})})(r),s=Object.assign({},a,l,(t=>("getAttribute"in t&&t.getAttribute("style")||"").split(";").map((t=>t.trim().toLowerCase().split(":"))).filter((t=>2===t.length)).reduce(((t,e)=>(t[e[0].trim()]=e[1].trim(),t)),{}))(t)),c=j(o,s,Object.assign({},i,l)),u=_(o);return{cssStyles:s,props:{...c,...u,[e]:{...c[e]||{},...u[e]||{},[p]:{...c[e].STYLE||{},...u[e].STYLE||{}}}}}},Y=(t,o)=>{if(L(t)&&L(o)){const n=o[e]?.MARGIN||[0,0,0,0];o[e]={...o[e]||{},[c]:n},o.margin[3]=o[e]?.PADDING?.[3]||0;const r=t[e]?.MARGIN||[0,0,0,0],a=Math.max(r[1],n[3]);r[1]=a,n[3]=0,t[e]={...t[e]||{},[c]:r},t.margin[1]=a+(t[e]?.PADDING?.[1]||0)}},B=t=>{const e=t.text.at(-1);return N(e)?B(e):e},U=t=>{const e=t.text.at(0);return N(e)?U(e):t.text},J=(t,o)=>{const n=B(t),r=U(o);n&&n[e]?.IS_WHITESPACE&&r[0][e]?.IS_WHITESPACE&&r.shift()};const K=(t,o)=>{const n=t.getAttribute("src");if(!n)return null;const r=t.getAttribute("name")||n;let a;return/^data:image\/(jpeg|png|jpg);base64,/.test(n)?a=n:(o.images[r]||(o.images[n]=r),a=r),{image:a,[e]:{}}},q=t=>{const e=t.cloneNode(!0),o=t.getAttribute("width"),n=t.getAttribute("height");return o&&e.setAttribute("width",""+T(o)),n&&e.setAttribute("height",""+T(n)),{svg:e.outerHTML.replace(/\n(\s+)?/g,"")}},$=()=>({table:{body:t=>{const o=t.find(m)?.stack[0]||[],n=t.filter((t=>!m(t))).flatMap((t=>"stack"in t?t.stack:[])),r=n.map((t=>I(t)));if(0===r.length)return[];const a=r.reduce(((t,e)=>t.length<=e.length?e:t));return[[{table:{body:r,widths:new Array(a.length).fill("auto"),heights:new Array(n.length).fill("auto")},layout:{defaultBorder:!1},[e]:{[d]:{colgroup:"text"in o&&Array.isArray(o.text)?o.text:[],trs:n}}}]]}},[e]:{[u]:P},layout:{}}),Q=t=>{const s=t.textContent;if(null===s)return null;const c=s.replace(/[^\S\r\n]+/,""),u=s.replace(/\n|\t| +/g," ").replace(/^ +/,"").replace(/ +$/,""),d="\n"===c[c.length-1],p="\n"===c[0],b=" "===s[0],g=" "===s[s.length-1];return{text:u,[e]:{[n]:p,[o]:d,[r]:p&&d&&0===u.length,[a]:b,[i]:g,[l]:b&&g&&1===s.length}}},V=t=>({text:" ",[e]:{[l]:t}}),X=/^(address|blockquote|body|center|colgroup|dir|div|dl|fieldset|form|h[1-6]|hr|isindex|menu|noframes|noscript|ol|p|pre|table|ul|dd|dt|frameset|li|tbody|td|tfoot|th|thead|tr|html)$/i,Z=(t,o,n={})=>{const r=[],a=(t=>["TABLE","TBODY","TR","COLGROUP","COL","UL","OL","SELECT"].includes(t.nodeName)&&"children"in t)(t)?t.children:t.childNodes;for(let t=0;t<a.length;t++){const i=ot(a[t],o,n);if(null===i)continue;const l=!!i[e]?.IS_NEWLINE,s=r[r.length-1];if(o.config.collapseMargin&&s&&Y(i,s),l&&(0===r.length||!a[t+1]||s&&"stack"in s))continue;if(!("text"in i)){r.push(i);continue}const c=!!i[e]?.END_WITH_NEWLINE,u=!!i[e]?.START_WITH_NEW_LINE,d=!!i[e]?.END_WITH_WHITESPACE,p=!!i[e]?.START_WITH_WHITESPACE,b=!!i[e]?.IS_WHITESPACE,g=Array.isArray(i.text)?i:{text:[b?V("newLine"):i]};l||b||((u||p)&&g.text.unshift(V(u?"startWithNewLine":"startWithWhitespace")),(c||d)&&g.text.push(V(c?"endWithNewLine":"endWithWhiteSpace"))),N(s)?(o.config.collapseWhitespace&&J(s,g),s.text.push(g)):r.push({text:[g]})}return r},tt=(t,o)=>{if("function"==typeof o.config.customRule){const e=o.config.customRule(t,o);if(null===e)return null;if(void 0!==e)return e}if(A(t))return(t=>{switch(t.nodeName.toLowerCase()){case"#comment":case"option":case"script":case"style":case"iframe":case"object":return()=>null;case"#text":return Q;case"a":return t=>{const e=t.getAttribute("href");if(!e)return nt(t);const o=t=>{const n=I(t);[].concat(n).forEach((t=>{"string"!=typeof t&&("#"===e[0]?t.linkToDestination=e.slice(1):t.link=e),o(t)}))};return{text:t=>(t.forEach((t=>{"string"!=typeof t&&("#"===e[0]?t.linkToDestination=e.slice(1):t.link=e),o(t)})),t)}};case"br":return()=>({text:"\n",[e]:{[r]:!0}});case"qr-code":return t=>{const e=t.getAttribute("value");if(!e)return null;const o=t.getAttribute("data-size");return{qr:e,fit:S(o||"128px")}};case"toc":return t=>{const e=t.textContent;return e?{toc:{title:{text:e,bold:!0,fontSize:S("22px"),margin:[0,10,0,10]}}}:null};case"table":return $;case"ul":return()=>({ul:t=>t});case"ol":return()=>({ol:t=>t});case"img":return K;case"svg":return q;case"hr":return()=>({table:{widths:["*"],body:[[""]]},style:["hr"]});case"input":return t=>t instanceof HTMLInputElement?{text:"value"in t?t.value:"",[e]:{[s]:!0}}:null;case"select":return t=>t instanceof HTMLSelectElement?{text:t.options[t.selectedIndex].value,[e]:{[s]:!0}}:null;default:return nt}})(t)(t,o);if((t=>3===t.nodeType||8===t.nodeType)(t))return(t=>{switch(t.nodeName.toLowerCase()){case"#comment":default:return()=>null;case"#text":return Q}})(t)(t,o);throw new Error("Unsupported Node Type: "+t.nodeType)},et=(t,o,n={})=>{const r=t[e]?.NODE;if("string"!=typeof t&&r){const{cssStyles:e,props:a}=F(r,t,o.styles,n);if(Object.assign(t,a),"stack"in t&&"function"==typeof t.stack){const n=Z(r,o,e);t.stack=t.stack(n,o)}else if("text"in t&&"function"==typeof t.text){const n=Z(r,o,e);t.text=t.text(n.filter(w),o)}else if("ul"in t&&"function"==typeof t.ul){const n=Z(r,o,e);t.ul=t.ul(n,o)}else if("ol"in t&&"function"==typeof t.ol){const n=Z(r,o,e);t.ol=t.ol(n,o)}else if("table"in t&&"function"==typeof t.table.body){const n=Z(r,o,e);t.table.body=t.table.body(n,o)}}return(t=>("string"!=typeof t&&t[e]?.PDFMAKE&&f(t,t[e]?.PDFMAKE||{}),"function"==typeof t[e]?.HANDLER?t[e]?.HANDLER?.(t)||null:t))(t)},ot=(t,o,n={})=>{const r=tt(t,o);if(null===r)return null;const a=r[e]||{};return a.NODE=t,r[e]=a,et(r,o,n)},nt=t=>(t=>X.test(t.nodeName))(t)?{stack:t=>t}:{text:t=>1===t.length&&"text"in t[0]&&Array.isArray(t[0].text)?t[0].text:t};t.parse=(t,e={globalStyles:{":root":{"font-size":"16px"},h1:{"font-size":"32px","margin-top":"21.44px","margin-bottom":"21.44px","font-weight":"bold"},h2:{"font-size":"24px","margin-top":"19.92px","margin-bottom":"19.92px","font-weight":"bold"},h3:{"font-size":"18.72px","margin-top":"18.72px","margin-bottom":"18.72px","font-weight":"bold"},h4:{"font-size":"16px","margin-top":"21.28px","margin-bottom":"21.28px","font-weight":"bold"},h5:{"font-size":"13.28px","margin-top":"22.17px","margin-bottom":"22.17px","font-weight":"bold"},h6:{"font-size":"10.72px","margin-top":"24.97px","margin-bottom":"24.97px","font-weight":"bold"},b:{"font-weight":"bold"},strong:{"font-weight":"bold"},i:{"font-style":"italic"},em:{"font-style":"italic"},s:{"text-decoration":"line-through"},del:{"text-decoration":"line-through"},sub:{"font-size":"22px","vertical-align":"sub"},small:{"font-size":"13px"},u:{"text-decoration":"underline"},ul:{"margin-top":"16px","margin-bottom":"16px","padding-left":"20px"},ol:{"margin-top":"16px","margin-bottom":"16px","padding-left":"20px"},p:{"margin-top":"16px","margin-bottom":"16px"},table:{border:"none",padding:"3px"},td:{border:"none"},tr:{margin:"4px 0"},th:{"font-weight":"bold",border:"none","text-align":"center"},a:{color:"#0000ee","text-decoration":"underline"},hr:{"border-top":"2px solid #9a9a9a","border-bottom":"0","border-left":"0px solid black","border-right":"0",margin:"8px 0"}},styles:{},collapseMargin:!0,collapseWhitespace:!0})=>{const o={...g,...e},n=new b(o,Object.assign({},o.globalStyles,o.styles)),r="string"==typeof t?(t=>{if("undefined"!=typeof DOMParser)return(new DOMParser).parseFromString(t,"text/html").body;if("undefined"!=typeof document&&"function"==typeof document.createDocumentFragment){const e=document.createDocumentFragment(),o=document.createElement("div");return o.innerHTML=t,e.append(o),e.children[0]}throw new Error("Could not parse html to DOM. Please use external parser like jsdom.")})(t):t;return{content:null!==r?Z(r,n):[],images:n.images,patterns:{fill:{boundingBox:[1,1,4,4],xStep:1,yStep:1,pattern:"1 w 0 1 m 4 5 l s 2 0 m 5 3 l s"}}}},Object.defineProperty(t,"__esModule",{value:!0})}));//# sourceMappingURL=html2pdfmake.min.js.map |
Sorry, the diff of this file is too big to display
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Minified code
QualityThis package contains minified code. This may be harmless in some cases where minified code is included in packaged libraries, however packages on npm should not minify code.
Found 1 instance in 1 package
17
-5.56%7683
24.18%2
-33.33%37
5.71%302455
-15.45%135
-0.74%8
166.67%