From 7c0a94e51ab0991393a4755647c888564e73c045 Mon Sep 17 00:00:00 2001 From: Jake Holland Date: Fri, 14 Feb 2025 17:21:03 +0000 Subject: [PATCH] Performance improvements to tables --- lib/table/accessibility.js | 30 ++++---- lib/table/normalize.js | 103 +++++++++++++------------ lib/table/render.js | 85 +++++++++++---------- lib/table/size.js | 9 ++- lib/table/style.js | 31 +++----- lib/table/{types.js => utils.js} | 80 +++++++++++++++++++ lib/utils.js | 127 ++----------------------------- tests/unit/table.spec.js | 35 +++++++-- tests/unit/utils.spec.js | 50 +----------- 9 files changed, 250 insertions(+), 300 deletions(-) rename lib/table/{types.js => utils.js} (81%) diff --git a/lib/table/accessibility.js b/lib/table/accessibility.js index 8e6b6276..5da0d1b3 100644 --- a/lib/table/accessibility.js +++ b/lib/table/accessibility.js @@ -9,13 +9,14 @@ import PDFDocument from '../document'; * @private */ export function accommodateTable() { - if (this.opts.structParent) { + const structParent = this.opts.structParent; + if (structParent) { this._tableStruct = this.document.struct('Table'); this._tableStruct.dictionary.data.ID = this._id; - if (this.opts.structParent instanceof PDFStructureElement) { - this.opts.structParent.add(this._tableStruct); - } else if (this.opts.structParent instanceof PDFDocument) { - this.opts.structParent.addStructure(this._tableStruct); + if (structParent instanceof PDFStructureElement) { + structParent.add(this._tableStruct); + } else if (structParent instanceof PDFDocument) { + structParent.addStructure(this._tableStruct); } this._headerRowLookup = {}; this._headerColumnLookup = {}; @@ -62,7 +63,9 @@ export function accessibleRow(row, rowIndex, renderCell) { * @private */ export function accessibleCell(cell, rowStruct, callback) { - const cellStruct = this.document.struct(cell.type, { title: cell.title }); + const doc = this.document; + + const cellStruct = doc.struct(cell.type, { title: cell.title }); cellStruct.dictionary.data.ID = cell.id; rowStruct.add(cellStruct); @@ -116,19 +119,18 @@ export function accessibleCell(cell, rowStruct, callback) { ); if (Headers.size) attributes.Headers = Array.from(Headers); + const normalizeColor = doc._normalizeColor; if (cell.backgroundColor != null) { - attributes.BackgroundColor = this.document._normalizeColor( - cell.backgroundColor, - ); + attributes.BackgroundColor = normalizeColor(cell.backgroundColor); } const hasBorder = [border.top, border.bottom, border.left, border.right]; if (hasBorder.some((x) => x)) { const borderColor = cell.borderColor; attributes.BorderColor = [ - hasBorder[0] ? this.document._normalizeColor(borderColor.top) : null, - hasBorder[1] ? this.document._normalizeColor(borderColor.bottom) : null, - hasBorder[2] ? this.document._normalizeColor(borderColor.left) : null, - hasBorder[3] ? this.document._normalizeColor(borderColor.right) : null, + hasBorder[0] ? normalizeColor(borderColor.top) : null, + hasBorder[1] ? normalizeColor(borderColor.bottom) : null, + hasBorder[2] ? normalizeColor(borderColor.left) : null, + hasBorder[3] ? normalizeColor(borderColor.right) : null, ]; } @@ -136,7 +138,7 @@ export function accessibleCell(cell, rowStruct, callback) { Object.keys(attributes).forEach( (key) => attributes[key] === undefined && delete attributes[key], ); - cellStruct.dictionary.data.A = this.document.ref(attributes); + cellStruct.dictionary.data.A = doc.ref(attributes); cellStruct.add(callback); cellStruct.end(); cellStruct.dictionary.data.A.end(); diff --git a/lib/table/normalize.js b/lib/table/normalize.js index 2dc9d7fe..5557929b 100644 --- a/lib/table/normalize.js +++ b/lib/table/normalize.js @@ -1,9 +1,11 @@ -import { deepMerge, definedProps, normalizeSides } from '../utils'; +import { deepMerge, memoize } from './utils'; import { + normalizeAlignment, normalizedColumnStyle, normalizedDefaultStyle, normalizedRowStyle, } from './style'; +import { normalizeSides } from '../utils'; /** * Normalize a table @@ -13,45 +15,48 @@ import { * @private */ export function normalizeTable() { + const doc = this.document; + const opts = this.opts; + // Normalize config - let index = this.document._tableIndex++; - this._id = new String(this.opts.id ?? `table-${index}`); + let index = doc._tableIndex++; + this._id = new String(opts.id ?? `table-${index}`); this._position = { - x: this.document.sizeToPoint(this.opts.position?.x, this.document.x), - y: this.document.sizeToPoint(this.opts.position?.y, this.document.y), + x: doc.sizeToPoint(opts.position?.x, doc.x), + y: doc.sizeToPoint(opts.position?.y, doc.y), }; - this._maxWidth = this.document.sizeToPoint( - this.opts.maxWidth, - this.document.page.width - - this.document.page.margins.right - - this._position.x, + this._maxWidth = doc.sizeToPoint( + opts.maxWidth, + doc.page.width - doc.page.margins.right - this._position.x, ); const { defaultStyle, defaultColStyle, defaultRowStyle } = - normalizedDefaultStyle(this.opts.defaultStyle); + normalizedDefaultStyle(opts.defaultStyle); this._defaultStyle = defaultStyle; let colStyle; - if (this.opts.columnStyles) { - if (Array.isArray(this.opts.columnStyles)) { - colStyle = (i) => this.opts.columnStyles[i]; - } else if (typeof this.opts.columnStyles === 'function') { - colStyle = this.opts.columnStyles; - } else if (typeof this.opts.columnStyles === 'object') { - colStyle = () => this.opts.columnStyles; + if (opts.columnStyles) { + if (Array.isArray(opts.columnStyles)) { + colStyle = (i) => opts.columnStyles[i]; + } else if (typeof opts.columnStyles === 'function') { + // memoize all columns + colStyle = memoize((i) => opts.columnStyles(i), Infinity); + } else if (typeof opts.columnStyles === 'object') { + colStyle = () => opts.columnStyles; } } if (!colStyle) colStyle = () => ({}); this._colStyle = normalizedColumnStyle.bind(this, defaultColStyle, colStyle); let rowStyle; - if (this.opts.rowStyles) { - if (Array.isArray(this.opts.rowStyles)) { - rowStyle = (i) => this.opts.rowStyles[i]; - } else if (typeof this.opts.rowStyles === 'function') { - rowStyle = this.opts.rowStyles; - } else if (typeof this.opts.rowStyles === 'object') { - rowStyle = () => this.opts.rowStyles; + if (opts.rowStyles) { + if (Array.isArray(opts.rowStyles)) { + rowStyle = (i) => opts.rowStyles[i]; + } else if (typeof opts.rowStyles === 'function') { + // Memoize the row configs in a rolling buffer + rowStyle = memoize((i) => opts.rowStyles(i), 10); + } else if (typeof opts.rowStyles === 'object') { + rowStyle = () => opts.rowStyles; } } if (!rowStyle) rowStyle = () => ({}); @@ -90,12 +95,14 @@ export function normalizeCell(cell, rowIndex, colIndex) { const font = deepMerge({}, colStyle.font, rowStyle.font, cell.font); + const doc = this.document; + // Initialize cell context - const rollbackFont = this.document._fontSource; - const rollbackFontSize = this.document._fontSize; - const rollbackFontFamily = this.document._fontFamily; - if (font.src) this.document.font(font.src, font.family); - if (font.size) this.document.fontSize(font.size); + const rollbackFont = doc._fontSource; + const rollbackFontSize = doc._fontSize; + const rollbackFontFamily = doc._fontFamily; + if (font.src) doc.font(font.src, font.family); + if (font.size) doc.fontSize(font.size); // Refetch rowStyle to reflect font changes rowStyle = this._rowStyle(rowIndex); @@ -105,34 +112,30 @@ export function normalizeCell(cell, rowIndex, colIndex) { cell.borderColor = normalizeSides(cell.borderColor); // Cell takes highest priority, then row, then column, then defaultConfig - const config = deepMerge(this._defaultStyle, colStyle, rowStyle, cell, { - rowIndex, - colIndex, - font, - }); + const config = deepMerge(this._defaultStyle, colStyle, rowStyle, cell); + config.rowIndex = rowIndex; + config.colIndex = colIndex; + config.font = font; // Normalize config config.text = normalizeText(config.text); config.rowSpan = config.rowSpan ?? 1; config.colSpan = config.colSpan ?? 1; config.padding = normalizeSides(config.padding, '0.25em', (x) => - this.document.sizeToPoint(x, '0.25em'), + doc.sizeToPoint(x, '0.25em'), ); config.border = normalizeSides(config.border, 1, (x) => - this.document.sizeToPoint(x, 1), + doc.sizeToPoint(x, 1), ); config.borderColor = normalizeSides( config.borderColor, 'black', (x) => x ?? 'black', ); - config.align = - config.align == null || typeof config.align === 'string' - ? { x: config.align, y: config.align } - : config.align; + config.align = normalizeAlignment(config.align); config.align.x = config.align.x ?? 'left'; config.align.y = config.align.y ?? 'top'; - config.textStroke = this.document.sizeToPoint(config.textStroke, 0); + config.textStroke = doc.sizeToPoint(config.textStroke, 0); config.textStrokeColor = config.textStrokeColor ?? 'black'; config.textColor = config.textColor ?? 'black'; config.textOptions = config.textOptions ?? {}; @@ -140,14 +143,17 @@ export function normalizeCell(cell, rowIndex, colIndex) { // Accessibility settings config.id = new String(config.id ?? `${this._id}-${rowIndex}-${colIndex}`); config.type = config.type?.toUpperCase() === 'TH' ? 'TH' : 'TD'; - config.scope = config.scope?.toLowerCase(); - if (config.scope === 'row') config.scope = 'Row'; - else if (config.scope === 'both') config.scope = 'Both'; - else if (config.scope === 'column') config.scope = 'Column'; + if (config.scope) { + config.scope = config.scope.toLowerCase(); + if (config.scope === 'row') config.scope = 'Row'; + else if (config.scope === 'both') config.scope = 'Both'; + else if (config.scope === 'column') config.scope = 'Column'; + } - if (this.opts.debug !== undefined) config.debug = this.opts.debug; + if (typeof this.opts.debug === 'boolean') config.debug = this.opts.debug; - this.document.font(rollbackFont, rollbackFontFamily, rollbackFontSize); + // Rollback font + doc.font(rollbackFont, rollbackFontFamily, rollbackFontSize); return config; } @@ -169,7 +175,6 @@ export function normalizeRow(row, rowIndex) { return row.map((cell) => { // Ensure TableCell if (cell == null || typeof cell !== 'object') cell = { text: cell }; - cell = definedProps(cell); // Find the starting column of the cell // Skipping over the claimed cells diff --git a/lib/table/render.js b/lib/table/render.js index 0923b712..f4119afb 100644 --- a/lib/table/render.js +++ b/lib/table/render.js @@ -31,7 +31,7 @@ export function renderRow(row, rowIndex) { function renderCell(cell, rowStruct) { const cellRenderer = () => { // Render cell background - if (cell.backgroundColor !== undefined) { + if (cell.backgroundColor != null) { this.document .save() .rect(cell.x, cell.y, cell.width, cell.height) @@ -77,12 +77,14 @@ function renderCell(cell, rowStruct) { * @param {SizedNormalizedTableCellStyle} cell */ function renderCellText(cell) { + const doc = this.document; + // Configure fonts - const rollbackFont = this.document._fontSource; - const rollbackFontSize = this.document._fontSize; - const rollbackFontFamily = this.document._fontFamily; - if (cell.font?.src) this.document.font(cell.font.src, cell.font?.family); - if (cell.font?.size) this.document.fontSize(cell.font.size); + const rollbackFont = doc._fontSource; + const rollbackFontSize = doc._fontSize; + const rollbackFontFamily = doc._fontFamily; + if (cell.font?.src) doc.font(cell.font.src, cell.font?.family); + if (cell.font?.size) doc.fontSize(cell.font.size); const x = cell.textX; const y = cell.textY; @@ -104,12 +106,12 @@ function renderCellText(cell) { const dy = Py + Oy; if (cell.debug) { - this.document.save(); - this.document.dash(1, { space: 1 }).lineWidth(1).strokeOpacity(0.3); + doc.save(); + doc.dash(1, { space: 1 }).lineWidth(1).strokeOpacity(0.3); // Debug actual text bounds if (cell.text) { - this.document + doc .moveTo(x + Px, y) .lineTo(x + Px, y + Ah) .moveTo(x + Px + Cw, y) @@ -122,24 +124,24 @@ function renderCellText(cell) { .stroke('green'); } // Debug allocated text bounds - this.document.rect(x, y, Aw, Ah).stroke('orange'); + doc.rect(x, y, Aw, Ah).stroke('orange'); - this.document.restore(); + doc.restore(); } // Create text mask to cut off any overflowing text // Mask cuts off at the padding not the actual cell, this is intentional! - this.document.save().rect(x, y, Aw, Ah).clip(); + doc.save().rect(x, y, Aw, Ah).clip(); - this.document.fillColor(cell.textColor).strokeColor(cell.textStrokeColor); - if (cell.textStroke > 0) this.document.lineWidth(cell.textStroke); + doc.fillColor(cell.textColor).strokeColor(cell.textStrokeColor); + if (cell.textStroke > 0) doc.lineWidth(cell.textStroke); // Render the text - this.document.text(cell.text, x + dx, y + dy, cell.textOptions); + doc.text(cell.text, x + dx, y + dy, cell.textOptions); // Cleanup - this.document.restore(); - this.document.font(rollbackFont, rollbackFontFamily, rollbackFontSize); + doc.restore(); + doc.font(rollbackFont, rollbackFontFamily, rollbackFontSize); } /** @@ -155,57 +157,62 @@ function renderCellText(cell) { * @private */ function renderBorder(border, borderColor, x, y, width, height, mask) { - const computedBorder = Object.fromEntries( + border = Object.fromEntries( Object.entries(border).map(([k, v]) => [k, mask && !mask[k] ? 0 : v]), ); + const doc = this.document; if ( - [computedBorder.right, computedBorder.bottom, computedBorder.left].every( - (val) => val === computedBorder.top, + [border.right, border.bottom, border.left].every( + (val) => val === border.top, ) ) { - if (computedBorder.top > 0) { - this.document + if (border.top > 0) { + doc .save() - .lineWidth(computedBorder.top) + .lineWidth(border.top) .rect(x, y, width, height) .stroke(borderColor.top) .restore(); } } else { // Top - if (computedBorder.top > 0) { - this.document + if (border.top > 0) { + doc .save() - .lineWidth(computedBorder.top) - .polygon([x, y], [x + width, y]) + .lineWidth(border.top) + .moveTo(x, y) + .lineTo(x + width, y) .stroke(borderColor.top) .restore(); } // Right - if (computedBorder.right > 0) { - this.document + if (border.right > 0) { + doc .save() - .lineWidth(computedBorder.right) - .polygon([x + width, y], [x + width, y + height]) + .lineWidth(border.right) + .moveTo(x + width, y) + .lineTo(x + width, y + height) .stroke(borderColor.right) .restore(); } // Bottom - if (computedBorder.bottom > 0) { - this.document + if (border.bottom > 0) { + doc .save() - .lineWidth(computedBorder.bottom) - .polygon([x + width, y + height], [x, y + height]) + .lineWidth(border.bottom) + .moveTo(x + width, y + height) + .lineTo(x, y + height) .stroke(borderColor.bottom) .restore(); } // Left - if (computedBorder.left > 0) { - this.document + if (border.left > 0) { + doc .save() - .lineWidth(computedBorder.left) - .polygon([x, y + height], [x, y]) + .lineWidth(border.left) + .moveTo(x, y + height) + .lineTo(x, y) .stroke(borderColor.left) .restore(); } diff --git a/lib/table/size.js b/lib/table/size.js index 9aac5f56..435033c7 100644 --- a/lib/table/size.js +++ b/lib/table/size.js @@ -72,9 +72,12 @@ function ensureColumnWidths(numCols) { }); } - this._columnXPos = this._columnWidths.map((_, i, a) => - a.slice(0, i).reduce((a, b) => a + b, this._position.x), - ); + let tempX = this._position.x; + this._columnXPos = Array.from(this._columnWidths, (v) => { + const t = tempX; + tempX += v; + return t; + }); } /** diff --git a/lib/table/style.js b/lib/table/style.js index 1de522c3..74753a57 100644 --- a/lib/table/style.js +++ b/lib/table/style.js @@ -1,5 +1,5 @@ -import { deepMerge, definedProps, normalizeSides } from '../utils'; -import { COLUMN_FIELDS, ROW_FIELDS } from './types'; +import { COLUMN_FIELDS, deepMerge, ROW_FIELDS } from './utils'; +import { normalizeSides } from '../utils'; /** * Normalize the row config @@ -17,7 +17,6 @@ export function normalizedDefaultStyle(defaultStyleInternal) { let defaultStyle = defaultStyleInternal; // Force object form if (typeof defaultStyle !== 'object') defaultStyle = { text: defaultStyle }; - defaultStyle = definedProps(defaultStyle); const defaultRowStyle = Object.fromEntries( Object.entries(defaultStyle).filter(([k]) => ROW_FIELDS.includes(k)), @@ -29,13 +28,7 @@ export function normalizedDefaultStyle(defaultStyleInternal) { defaultStyle.padding = normalizeSides(defaultStyle.padding); defaultStyle.border = normalizeSides(defaultStyle.border); defaultStyle.borderColor = normalizeSides(defaultStyle.borderColor); - defaultStyle.align = - defaultStyle.align == null || typeof defaultStyle.align === 'string' - ? { x: defaultStyle.align, y: defaultStyle.align } - : defaultStyle.align; - - COLUMN_FIELDS.forEach((f) => delete defaultStyle[f]); - ROW_FIELDS.forEach((f) => delete defaultStyle[f]); + defaultStyle.align = normalizeAlignment(defaultStyle.align); return { defaultStyle, defaultRowStyle, defaultColStyle }; } @@ -63,10 +56,7 @@ export function normalizedRowStyle(defaultRowStyle, rowStyleInternal, i) { rowStyle.padding = normalizeSides(rowStyle.padding); rowStyle.border = normalizeSides(rowStyle.border); rowStyle.borderColor = normalizeSides(rowStyle.borderColor); - rowStyle.align = - rowStyle.align === undefined || typeof rowStyle.align === 'string' - ? { x: rowStyle.align, y: rowStyle.align } - : rowStyle.align; + rowStyle.align = normalizeAlignment(rowStyle.align); // Merge defaults rowStyle = deepMerge(defaultRowStyle, rowStyle); @@ -98,7 +88,7 @@ export function normalizedRowStyle(defaultRowStyle, rowStyleInternal, i) { contentHeight, ); - return definedProps(rowStyle); + return rowStyle; } /** @@ -122,10 +112,7 @@ export function normalizedColumnStyle(defaultColStyle, colStyleInternal, i) { colStyle.padding = normalizeSides(colStyle.padding); colStyle.border = normalizeSides(colStyle.border); colStyle.borderColor = normalizeSides(colStyle.borderColor); - colStyle.align = - colStyle.align == null || typeof colStyle.align === 'string' - ? { x: colStyle.align, y: colStyle.align } - : colStyle.align; + colStyle.align = normalizeAlignment(colStyle.align); // Merge defaults colStyle = deepMerge(defaultColStyle, colStyle); @@ -153,5 +140,9 @@ export function normalizedColumnStyle(defaultColStyle, colStyleInternal, i) { this._maxWidth, // Use table width here for percentage scaling ); - return definedProps(colStyle); + return colStyle; +} + +export function normalizeAlignment(align) { + return align == null || typeof align === 'string' ? { x: align, y: align } : align; } diff --git a/lib/table/types.js b/lib/table/utils.js similarity index 81% rename from lib/table/types.js rename to lib/table/utils.js index b36433a8..70662222 100644 --- a/lib/table/types.js +++ b/lib/table/utils.js @@ -3,6 +3,28 @@ * @typedef {function(number): T} Dynamic */ +/** + * @typedef {Object} Font + * @property {PDFFontSource} [src] + * The name of the font + * + * Defaults to the current document font source `doc._fontSrc` + * @property {string} [family] + * The font family of the font + * + * Defaults to the current document font family `doc._fontFamily` + * @property {Size} [size] + * The size of the font + * + * Defaults to the current document font size `doc._fontSize` + */ + +/** + * Measurement of how wide something is, false means 0 and true means 1 + * + * @typedef {Size | boolean} Wideness + */ + /** * The value of the text of a cell * @typedef {string | null | undefined} TableCellText @@ -285,3 +307,61 @@ export const ROW_FIELDS = ['height', 'minHeight', 'maxHeight']; * @type {string[]} */ export const COLUMN_FIELDS = ['width', 'minWidth', 'maxWidth']; + +export function memoize(fn, maxSize) { + const cache = new Map(); + return function (...args) { + const key = args[0]; + if (!cache.has(key)) { + cache.set(key, fn(...args)); + if (cache.size > maxSize) cache.delete(cache.keys().next()); + } + return cache.get(key); + }; +} + +/** + * Simple object check. + * @param item + * @returns {boolean} + */ +function isObject(item) { + return item && typeof item === "object" && !Array.isArray(item); +} + +/** + * Deep merge two objects. + * + * @template T + * @param {T} target + * @param sources + * @returns {T} + */ +export function deepMerge(target, ...sources) { + if (!isObject(target)) return target; + target = deepClone(target); + + for (const source of sources) { + if (isObject(source)) { + for (const key in source) { + if (isObject(source[key])) { + if (!(key in target)) target[key] = {}; + target[key] = deepMerge(target[key], source[key]); + } else if (source[key] !== undefined) { + target[key] = deepClone(source[key]); + } + } + } + } + + return target; +} + +function deepClone(obj) { + let result = obj; + if (typeof obj == 'object') { + result = Array.isArray(obj) ? [] : {}; + for (const key in obj) result[key] = deepClone(obj[key]); + } + return result; +} diff --git a/lib/utils.js b/lib/utils.js index 5e91cdd1..17b0d538 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -16,42 +16,6 @@ export function PDFNumber(n) { */ /** @typedef {string | Buffer | Uint8Array | ArrayBuffer} PDFFontSource */ - -/** - * @template {Size} [T=Size] - * @typedef {Object} Position - * @property {T} [x] - * The x coordinate - * - * Defaults to the current document position `doc.x` - * @property {T} [y] - * The y coordinate - * - * Defaults to the current document position `doc.y` - */ - -/** - * @typedef {Object} Font - * @property {PDFFontSource} [src] - * The name of the font - * - * Defaults to the current document font source `doc._fontSrc` - * @property {string} [family] - * The font family of the font - * - * Defaults to the current document font family `doc._fontFamily` - * @property {Size} [size] - * The size of the font - * - * Defaults to the current document font size `doc._fontSize` - */ - -/** - * Measurement of how wide something is, false means 0 and true means 1 - * - * @typedef {Size | boolean} Wideness - */ - /** * Side definitions * - To define all sides, use a single value @@ -87,16 +51,14 @@ export function normalizeSides( transformer = (v) => v, ) { if ( - sides === undefined || - sides === null || - (typeof sides === "object" && Object.keys(sides).length === 0) + sides == null || + (typeof sides === 'object' && Object.keys(sides).length === 0) ) { sides = defaultDefinition; } - if (typeof sides !== "object" || sides === null) { - sides = [sides, sides, sides, sides]; - } - if (Array.isArray(sides)) { + if (sides == null || typeof sides !== 'object') { + sides = { top: sides, right: sides, bottom: sides, left: sides }; + } else if (Array.isArray(sides)) { if (sides.length === 2) { sides = { vertical: sides[0], horizontal: sides[1] }; } else { @@ -109,7 +71,7 @@ export function normalizeSides( } } - if ("vertical" in sides || "horizontal" in sides) { + if ('vertical' in sides || 'horizontal' in sides) { sides = { top: sides.vertical, right: sides.horizontal, @@ -144,7 +106,7 @@ export function cosine(a) { if (a === 90) return 0; if (a === 180) return -1; if (a === 270) return 0; - return Math.cos(a * Math.PI / 180); + return Math.cos((a * Math.PI) / 180); } /** @@ -159,78 +121,5 @@ export function sine(a) { if (a === 90) return 1; if (a === 180) return 0; if (a === 270) return -1; - return Math.sin(a * Math.PI / 180); -} - -/** - * @template T - * @param {T} obj - * @returns {T} - */ -export function definedProps(obj) { - return Object.fromEntries( - Object.entries(obj) - .map(([k, v]) => { - if ( - v !== undefined && - v !== null && - typeof v === "object" && - !Array.isArray(v) - ) - return [k, definedProps(v)]; - return [k, v]; - }) - .filter(([, v]) => v !== undefined), - ); -} - -/** - * Simple object check. - * @param item - * @returns {boolean} - */ -function isObject(item) { - return item && typeof item === "object" && !Array.isArray(item); -} - -/** - * Deep merge two objects. - * - * @note Modifies target - * - * @template T - * @param {T} target - * @param sources - * @returns {T} - */ -export function deepMerge(target, ...sources) { - if (!isObject(target)) return target; - target = deepClone(target); - - for (const source of sources) { - if (isObject(source)) { - for (const key in source) { - if (isObject(source[key])) { - if (!(key in target)) target[key] = {}; - target[key] = deepMerge(target[key], source[key]); - } else if (source[key] !== undefined) { - target[key] = deepClone(source[key]); - } - } - } - } - - return target; -} - -/** - * Create a deep copy of a value - * - * @template T - * @param {T} a - * @returns {T} - */ -export function deepClone(a) { - if (a === undefined || a === null || typeof a !== "object") return a; - return JSON.parse(JSON.stringify(a)); + return Math.sin((a * Math.PI) / 180); } diff --git a/tests/unit/table.spec.js b/tests/unit/table.spec.js index 16c5b446..681593ad 100644 --- a/tests/unit/table.spec.js +++ b/tests/unit/table.spec.js @@ -1,16 +1,37 @@ -import PDFDocument from "../../lib/document"; -import PDFTable from "../../lib/table"; +import PDFDocument from '../../lib/document'; +import PDFTable from '../../lib/table'; +import { deepMerge } from '../../lib/table/utils'; -describe("table", () => { - test("created", () => { +describe('table', () => { + test('created', () => { const document = new PDFDocument(); expect(document.table()).toBeInstanceOf(PDFTable); - expect(document.table({data: []})).toBe(document); + expect(document.table({ data: [] })).toBe(document); }); - test("row", () => { + test('row', () => { const document = new PDFDocument(); const table = document.table(); - table.row(["A", "B", "C"]); + table.row(['A', 'B', 'C']); expect(table._columnWidths.length).toBe(3); }); }); + +describe('utils', () => { + describe('deepMerge', () => { + test.each([ + [{ a: 'hello' }, { b: 'world' }, { a: 'hello', b: 'world' }], + [{ a: 'hello' }, { a: 'world' }, { a: 'world' }], + [{}, { a: 'hello' }, { a: 'hello' }], + [{ a: 'hello' }, undefined, { a: 'hello' }], + [undefined, undefined, undefined], + [1, 2, 1], + [1, {}, 1], + [{ a: 'hello' }, { a: {} }, { a: 'hello' }], + [{ a: { b: 'hello' } }, { a: { b: 'world' } }, { a: { b: 'world' } }], + ])('%o -> %o', function () { + const opts = Array.from(arguments); + const expected = opts.splice(-1, 1)[0]; + expect(deepMerge(...opts)).toEqual(expected); + }); + }); +}); diff --git a/tests/unit/utils.spec.js b/tests/unit/utils.spec.js index 28a4e35c..e1181cd6 100644 --- a/tests/unit/utils.spec.js +++ b/tests/unit/utils.spec.js @@ -1,9 +1,4 @@ -import { - deepClone, - deepMerge, - definedProps, - normalizeSides, -} from '../../lib/utils'; +import { normalizeSides } from '../../lib/utils'; describe('normalizeSides', () => { test.each([ @@ -59,46 +54,3 @@ describe('normalizeSides', () => { }); }); }); - -describe('definedProps', () => { - test.each([ - [{}, {}], - [{ a: 'hi' }, { a: 'hi' }], - [{ a: undefined }, {}], - [{ a: undefined, b: 1 }, { b: 1 }], - [{ a: { b: undefined } }, { a: {} }], - [{ a: { b: { c: undefined } } }, { a: { b: {} } }], - ])('%o -> %o', (obj, expected) => { - expect(definedProps(obj)).toEqual(expected); - }); -}); - -describe('deepMerge', () => { - test.each([ - [{ a: 'hello' }, { b: 'world' }, { a: 'hello', b: 'world' }], - [{ a: 'hello' }, { a: 'world' }, { a: 'world' }], - [{}, { a: 'hello' }, { a: 'hello' }], - [{ a: 'hello' }, undefined, { a: 'hello' }], - [undefined, undefined, undefined], - [1, 2, 1], - [1, {}, 1], - [{ a: 'hello' }, { a: {} }, { a: 'hello' }], - [{ a: { b: 'hello' } }, { a: { b: 'world' } }, { a: { b: 'world' } }], - ])('%o -> %o', function () { - const opts = Array.from(arguments); - const expected = opts.splice(-1, 1)[0]; - expect(deepMerge(...opts)).toEqual(expected); - }); -}); - -describe('deepClone', () => { - test.each([ - [1], - [true], - ['hello'], - [{ a: 'hello' }], - [{ a: { b: 'hello' } }], - ])('%s', (a) => { - expect(deepClone(a)).toEqual(a); - }); -});