From 9af23e790f88e15abd80fc7984ff3fd10a093bab Mon Sep 17 00:00:00 2001 From: Diego Muracciole Date: Wed, 19 Feb 2025 03:19:13 +0100 Subject: [PATCH 1/3] feat: type first steps --- packages/fns/rollup.config.js | 2 +- packages/fns/src/asyncCompose.ts | 26 +- packages/layout/package.json | 3 +- packages/layout/rollup.config.js | 32 +- packages/layout/src/{index.js => index.ts} | 0 .../src/node/{getPadding.js => getPadding.ts} | 10 +- packages/layout/src/page/getContentArea.js | 10 - packages/layout/src/page/getContentArea.ts | 12 + .../{getOrientation.js => getOrientation.ts} | 8 +- .../src/page/{getSize.js => getSize.ts} | 77 ++-- packages/layout/src/page/getWrapArea.js | 9 - packages/layout/src/page/getWrapArea.ts | 13 + packages/layout/src/page/isHeightAuto.js | 11 - packages/layout/src/page/isHeightAuto.ts | 12 + packages/layout/src/page/isLandscape.js | 11 - packages/layout/src/page/isLandscape.ts | 12 + packages/layout/src/page/isPortrait.js | 11 - packages/layout/src/page/isPortrait.ts | 12 + ...esolveBookmarks.js => resolveBookmarks.ts} | 17 +- ...titution.js => resolveLinkSubstitution.ts} | 35 +- ...esolvePageSizes.js => resolvePageSizes.ts} | 16 +- packages/layout/src/steps/resolveStyles.js | 83 ----- packages/layout/src/steps/resolveStyles.ts | 88 +++++ .../steps/{resolveYoga.js => resolveYoga.ts} | 3 +- packages/layout/src/types.ts | 311 ++++++++++++++++ .../layout/tests/page/getOrientation.test.js | 29 -- .../layout/tests/page/getOrientation.test.ts | 38 ++ packages/layout/tests/page/getSize.test.js | 87 ----- packages/layout/tests/page/getSize.test.ts | 133 +++++++ .../layout/tests/page/isHeightAuto.test.js | 30 -- .../layout/tests/page/isHeightAuto.test.ts | 55 +++ .../layout/tests/page/isLandscape.test.js | 23 -- .../layout/tests/page/isLandscape.test.ts | 29 ++ packages/layout/tests/page/isPortrait.test.js | 23 -- packages/layout/tests/page/isPortrait.test.ts | 29 ++ .../resolveLinkSubstitution.test.ts.snap | 131 +++++++ .../__snapshots__/resolveStyles.test.ts.snap | 344 ++++++++++++++++++ ...marks.test.js => resolveBookmarks.test.ts} | 36 +- ...est.js => resolveLinkSubstitution.test.ts} | 53 +-- ...veStyles.test.js => resolveStyles.test.ts} | 80 ++-- packages/layout/tsconfig.json | 19 + packages/stylesheet/src/index.ts | 2 +- 42 files changed, 1472 insertions(+), 493 deletions(-) rename packages/layout/src/{index.js => index.ts} (100%) rename packages/layout/src/node/{getPadding.js => getPadding.ts} (83%) delete mode 100644 packages/layout/src/page/getContentArea.js create mode 100644 packages/layout/src/page/getContentArea.ts rename packages/layout/src/page/{getOrientation.js => getOrientation.ts} (65%) rename packages/layout/src/page/{getSize.js => getSize.ts} (65%) delete mode 100644 packages/layout/src/page/getWrapArea.js create mode 100644 packages/layout/src/page/getWrapArea.ts delete mode 100644 packages/layout/src/page/isHeightAuto.js create mode 100644 packages/layout/src/page/isHeightAuto.ts delete mode 100644 packages/layout/src/page/isLandscape.js create mode 100644 packages/layout/src/page/isLandscape.ts delete mode 100644 packages/layout/src/page/isPortrait.js create mode 100644 packages/layout/src/page/isPortrait.ts rename packages/layout/src/steps/{resolveBookmarks.js => resolveBookmarks.ts} (65%) rename packages/layout/src/steps/{resolveLinkSubstitution.js => resolveLinkSubstitution.ts} (69%) rename packages/layout/src/steps/{resolvePageSizes.js => resolvePageSizes.ts} (53%) delete mode 100644 packages/layout/src/steps/resolveStyles.js create mode 100644 packages/layout/src/steps/resolveStyles.ts rename packages/layout/src/steps/{resolveYoga.js => resolveYoga.ts} (62%) create mode 100644 packages/layout/src/types.ts delete mode 100644 packages/layout/tests/page/getOrientation.test.js create mode 100644 packages/layout/tests/page/getOrientation.test.ts delete mode 100644 packages/layout/tests/page/getSize.test.js create mode 100644 packages/layout/tests/page/getSize.test.ts delete mode 100644 packages/layout/tests/page/isHeightAuto.test.js create mode 100644 packages/layout/tests/page/isHeightAuto.test.ts delete mode 100644 packages/layout/tests/page/isLandscape.test.js create mode 100644 packages/layout/tests/page/isLandscape.test.ts delete mode 100644 packages/layout/tests/page/isPortrait.test.js create mode 100644 packages/layout/tests/page/isPortrait.test.ts create mode 100644 packages/layout/tests/steps/__snapshots__/resolveLinkSubstitution.test.ts.snap create mode 100644 packages/layout/tests/steps/__snapshots__/resolveStyles.test.ts.snap rename packages/layout/tests/steps/{resolveBookmarks.test.js => resolveBookmarks.test.ts} (82%) rename packages/layout/tests/steps/{resolveLinkSubstitution.test.js => resolveLinkSubstitution.test.ts} (56%) rename packages/layout/tests/steps/{resolveStyles.test.js => resolveStyles.test.ts} (83%) create mode 100644 packages/layout/tsconfig.json diff --git a/packages/fns/rollup.config.js b/packages/fns/rollup.config.js index a5b21cd8a..7e5967da1 100644 --- a/packages/fns/rollup.config.js +++ b/packages/fns/rollup.config.js @@ -6,7 +6,7 @@ const config = [ { input: 'src/index.ts', output: { format: 'es', dir: 'lib' }, - plugins: [typescript(), del({ targets: 'lib' })], + plugins: [typescript()], }, { input: './lib/types/index.d.ts', diff --git a/packages/fns/src/asyncCompose.ts b/packages/fns/src/asyncCompose.ts index 4b5f9a49b..feb09a97e 100644 --- a/packages/fns/src/asyncCompose.ts +++ b/packages/fns/src/asyncCompose.ts @@ -1,7 +1,20 @@ /* eslint-disable no-await-in-loop */ -import reverse from './reverse'; +type Fn = (arg: any, ...args: any[]) => Promise; +type FirstFnParameterType = T extends [ + ...any, + (arg: infer A, ...args: any[]) => Promise, +] + ? A + : never; + +type LastFnReturnType = T extends [ + (arg: any, ...args: any[]) => Promise, + ...any, +] + ? R + : never; /** * Performs right-to-left function composition with async functions support * @@ -9,17 +22,20 @@ import reverse from './reverse'; * @returns Composed function */ const asyncCompose = - (...fns: any[]) => - async (value: any, ...args: any[]) => { + (...fns: T) => + async ( + value: FirstFnParameterType, + ...args: Parameters extends [any, ...infer Rest] ? Rest : [] + ): Promise> => { let result = value; - const reversedFns = reverse(fns); + const reversedFns = fns.slice().reverse(); for (let i = 0; i < reversedFns.length; i += 1) { const fn = reversedFns[i]; result = await fn(result, ...args); } - return result; + return result as LastFnReturnType; }; export default asyncCompose; diff --git a/packages/layout/package.json b/packages/layout/package.json index 62e95ff7a..3d2524947 100644 --- a/packages/layout/package.json +++ b/packages/layout/package.json @@ -2,7 +2,7 @@ "name": "@react-pdf/layout", "version": "4.2.3", "license": "MIT", - "description": "Resolve overall document component's layout", + "description": "Resolve document component's layout", "author": "Diego Muracciole ", "homepage": "https://github.com/diegomura/react-pdf#readme", "type": "module", @@ -18,7 +18,6 @@ "watch": "rimraf ./lib && rollup -c -w" }, "dependencies": { - "@babel/runtime": "^7.20.13", "@react-pdf/fns": "3.1.1", "@react-pdf/image": "^3.0.2", "@react-pdf/pdfkit": "^4.0.2", diff --git a/packages/layout/rollup.config.js b/packages/layout/rollup.config.js index b778eacfe..8cec24101 100644 --- a/packages/layout/rollup.config.js +++ b/packages/layout/rollup.config.js @@ -1,21 +1,21 @@ -import babel from '@rollup/plugin-babel'; +import typescript from '@rollup/plugin-typescript'; +import { dts } from 'rollup-plugin-dts'; +import del from 'rollup-plugin-delete'; import pkg from './package.json' with { type: 'json' }; -const config = { - input: 'src/index.js', - output: { format: 'es', file: 'lib/index.js' }, - external: Object.keys(pkg.dependencies).concat( - /@babel\/runtime/, - /@react-pdf/, - ), - plugins: [ - babel({ - babelrc: true, - babelHelpers: 'runtime', - exclude: 'node_modules/**', - }), - ], -}; +const config = [ + { + input: 'src/index.ts', + output: { format: 'es', dir: 'lib' }, + external: Object.keys(pkg.dependencies).concat(/@react-pdf/), + plugins: [typescript(), del({ targets: 'lib' })], + }, + { + input: './lib/types/index.d.ts', + output: [{ file: 'lib/index.d.ts', format: 'es' }], + plugins: [dts(), del({ targets: 'lib/types', hook: 'buildEnd' })], + }, +]; export default config; diff --git a/packages/layout/src/index.js b/packages/layout/src/index.ts similarity index 100% rename from packages/layout/src/index.js rename to packages/layout/src/index.ts diff --git a/packages/layout/src/node/getPadding.js b/packages/layout/src/node/getPadding.ts similarity index 83% rename from packages/layout/src/node/getPadding.js rename to packages/layout/src/node/getPadding.ts index 09217580e..5ddb544ae 100644 --- a/packages/layout/src/node/getPadding.js +++ b/packages/layout/src/node/getPadding.ts @@ -1,6 +1,8 @@ import * as Yoga from 'yoga-layout/load'; -const getComputedPadding = (node, edge) => { +import { Node } from '../types'; + +const getComputedPadding = (node: Node, edge) => { const { yogaNode } = node; return yogaNode ? yogaNode.getComputedPadding(edge) : null; }; @@ -8,10 +10,10 @@ const getComputedPadding = (node, edge) => { /** * Get Yoga computed paddings. Zero otherwise * - * @param {Object} node - * @returns {{ paddingTop: number, paddingRight: number, paddingBottom: number, paddingLeft: number }} paddings + * @param node + * @returns paddings */ -const getPadding = (node) => { +const getPadding = (node: Node) => { const { style, box } = node; const paddingTop = diff --git a/packages/layout/src/page/getContentArea.js b/packages/layout/src/page/getContentArea.js deleted file mode 100644 index d1e3d5312..000000000 --- a/packages/layout/src/page/getContentArea.js +++ /dev/null @@ -1,10 +0,0 @@ -import getPadding from '../node/getPadding'; - -const getContentArea = (page) => { - const height = page.style?.height; - const { paddingTop, paddingBottom } = getPadding(page); - - return height - paddingBottom - paddingTop; -}; - -export default getContentArea; diff --git a/packages/layout/src/page/getContentArea.ts b/packages/layout/src/page/getContentArea.ts new file mode 100644 index 000000000..3b639f3d9 --- /dev/null +++ b/packages/layout/src/page/getContentArea.ts @@ -0,0 +1,12 @@ +import getPadding from '../node/getPadding'; +import { PageNode } from '../types'; + +// TODO: Use safe node +const getContentArea = (page: PageNode) => { + const height = page.style?.height as number; + const { paddingTop, paddingBottom } = getPadding(page); + + return height - (paddingBottom as number) - (paddingTop as number); +}; + +export default getContentArea; diff --git a/packages/layout/src/page/getOrientation.js b/packages/layout/src/page/getOrientation.ts similarity index 65% rename from packages/layout/src/page/getOrientation.js rename to packages/layout/src/page/getOrientation.ts index 6cb49b364..31cd33a86 100644 --- a/packages/layout/src/page/getOrientation.js +++ b/packages/layout/src/page/getOrientation.ts @@ -1,12 +1,14 @@ +import { PageNode } from '../types'; + const VALID_ORIENTATIONS = ['portrait', 'landscape']; /** * Get page orientation. Defaults to portrait * - * @param {Object} page object - * @returns {string} page orientation + * @param page - Page object + * @returns Page orientation */ -const getOrientation = (page) => { +const getOrientation = (page: PageNode) => { const value = page.props?.orientation || 'portrait'; return VALID_ORIENTATIONS.includes(value) ? value : 'portrait'; }; diff --git a/packages/layout/src/page/getSize.js b/packages/layout/src/page/getSize.ts similarity index 65% rename from packages/layout/src/page/getSize.js rename to packages/layout/src/page/getSize.ts index f78533009..4531b0b73 100644 --- a/packages/layout/src/page/getSize.js +++ b/packages/layout/src/page/getSize.ts @@ -1,4 +1,9 @@ import isLandscape from './isLandscape'; +import { PageNode } from '../types'; + +type UnitSize = { width: string | number; height?: string | number }; + +type Size = { width: number; height: number }; // Page sizes for 72dpi. 72dpi is used internally by pdfkit. const PAGE_SIZES = { @@ -58,10 +63,12 @@ const PAGE_SIZES = { /** * Parses scalar value in value and unit pairs * - * @param {string} value scalar value - * @returns {Object} parsed value + * @param value - Scalar value + * @returns Parsed value */ -const parseValue = (value) => { +const parseValue = (value: string | number) => { + if (typeof value === 'number') return { value, unit: undefined }; + const match = /^(-?\d*\.?\d+)(in|mm|cm|pt|px)?$/g.exec(value); return match @@ -72,17 +79,20 @@ const parseValue = (value) => { /** * Transform given scalar value to 72dpi equivalent of size * - * @param {string} value styles value - * @param {number} inputDpi user defined dpi - * @returns {Object} transformed value + * @param value - Styles value + * @param inputDpi - User defined dpi + * @returns Transformed value */ -const transformUnit = (value, inputDpi) => { +const transformUnit = (value: string | number, inputDpi: number) => { const scalar = parseValue(value); const outputDpi = 72; const mmFactor = (1 / 25.4) * outputDpi; const cmFactor = (1 / 2.54) * outputDpi; + if (typeof scalar.value === 'string') + throw new Error(`Invalid page size: ${value}`); + switch (scalar.unit) { case 'in': return scalar.value * outputDpi; @@ -97,7 +107,7 @@ const transformUnit = (value, inputDpi) => { } }; -const transformUnits = ({ width, height }, dpi) => ({ +const transformUnits = ({ width, height }: UnitSize, dpi: number): Size => ({ width: transformUnit(width, dpi), height: transformUnit(height, dpi), }); @@ -105,58 +115,59 @@ const transformUnits = ({ width, height }, dpi) => ({ /** * Transforms array into size object * - * @param {number[] | string[]} v array - * @returns {{ width: number | string, height: number | string }} size object with width and height + * @param v - Values array + * @returns Size object with width and height */ -const toSizeObject = (v) => ({ width: v[0], height: v[1] }); +const toSizeObject = (v: (number | string)[]) => ({ + width: v[0], + height: v[1], +}); /** * Flip size object * - * @param {{ width: number, height: number }} v size object - * @returns {{ width: number, height: number }} flipped size object + * @param v - Size object + * @returns Flipped size object */ -const flipSizeObject = (v) => ({ width: v.height, height: v.width }); +const flipSizeObject = (v: Size): Size => ({ + width: v.height, + height: v.width, +}); /** * Returns size object from a given string * - * @param {string} v page size string - * @returns {{ width: number, height: number }} size object with width and height + * @param v - Page size string + * @returns Size object with width and height */ -const getStringSize = (v) => { - return toSizeObject(PAGE_SIZES[v.toUpperCase()]); +const getStringSize = (v: string) => { + return toSizeObject(PAGE_SIZES[v.toUpperCase()]) as Size; }; /** * Returns size object from a single number * - * @param {number|string} n page size number - * @returns {{ width: number|string, height: number|string }} size object with width and height + * @param n - Page size number + * @returns Size object with width and height */ -const getNumberSize = (n) => toSizeObject([n, n]); +const getNumberSize = (n: number) => toSizeObject([n, n]); /** * Return page size in an object { width, height } * - * @param {Object} page instance - * @returns {{ width: number, height: number }} size object with width and height + * @param page - Page node + * @returns Size object with width and height */ -const getSize = (page) => { +const getSize = (page: PageNode) => { const value = page.props?.size || 'A4'; - const dpi = parseFloat(page.props?.dpi || 72); - - const type = typeof value; + const dpi = page.props?.dpi || 72; - /** - * @type {{ width: number, height: number }} - */ - let size; - if (type === 'string') { + let size: Size; + if (typeof value === 'string') { size = getStringSize(value); } else if (Array.isArray(value)) { size = transformUnits(toSizeObject(value), dpi); - } else if (type === 'number') { + } else if (typeof value === 'number') { size = transformUnits(getNumberSize(value), dpi); } else { size = transformUnits(value, dpi); diff --git a/packages/layout/src/page/getWrapArea.js b/packages/layout/src/page/getWrapArea.js deleted file mode 100644 index 254582080..000000000 --- a/packages/layout/src/page/getWrapArea.js +++ /dev/null @@ -1,9 +0,0 @@ -import getPadding from '../node/getPadding'; - -const getWrapArea = (page) => { - const { paddingBottom } = getPadding(page); - const height = page.style?.height; - return height - paddingBottom; -}; - -export default getWrapArea; diff --git a/packages/layout/src/page/getWrapArea.ts b/packages/layout/src/page/getWrapArea.ts new file mode 100644 index 000000000..8ff508aa8 --- /dev/null +++ b/packages/layout/src/page/getWrapArea.ts @@ -0,0 +1,13 @@ +import getPadding from '../node/getPadding'; +import { PageNode } from '../types'; + +// TODO: Use safe node + +const getWrapArea = (page: PageNode) => { + const height = page.style?.height as number; + const { paddingBottom } = getPadding(page); + + return height - (paddingBottom as number); +}; + +export default getWrapArea; diff --git a/packages/layout/src/page/isHeightAuto.js b/packages/layout/src/page/isHeightAuto.js deleted file mode 100644 index 7fdeecbc6..000000000 --- a/packages/layout/src/page/isHeightAuto.js +++ /dev/null @@ -1,11 +0,0 @@ -import { isNil } from '@react-pdf/fns'; - -/** - * Checks if page has auto height - * - * @param {Object} page - * @returns {boolean} is page height auto - */ -const isHeightAuto = (page) => isNil(page.box?.height); - -export default isHeightAuto; diff --git a/packages/layout/src/page/isHeightAuto.ts b/packages/layout/src/page/isHeightAuto.ts new file mode 100644 index 000000000..f114755ac --- /dev/null +++ b/packages/layout/src/page/isHeightAuto.ts @@ -0,0 +1,12 @@ +import { isNil } from '@react-pdf/fns'; +import { PageNode } from '../types'; + +/** + * Checks if page has auto height + * + * @param page + * @returns Is page height auto + */ +const isHeightAuto = (page: PageNode) => isNil(page.box?.height); + +export default isHeightAuto; diff --git a/packages/layout/src/page/isLandscape.js b/packages/layout/src/page/isLandscape.js deleted file mode 100644 index ff9f75517..000000000 --- a/packages/layout/src/page/isLandscape.js +++ /dev/null @@ -1,11 +0,0 @@ -import getOrientation from './getOrientation'; - -/** - * Return true if page is landscape - * - * @param {Object} page instance - * @returns {boolean} is page landscape - */ -const isLandscape = (page) => getOrientation(page) === 'landscape'; - -export default isLandscape; diff --git a/packages/layout/src/page/isLandscape.ts b/packages/layout/src/page/isLandscape.ts new file mode 100644 index 000000000..91156f08e --- /dev/null +++ b/packages/layout/src/page/isLandscape.ts @@ -0,0 +1,12 @@ +import { PageNode } from '../types'; +import getOrientation from './getOrientation'; + +/** + * Return true if page is landscape + * + * @param page - Page instance + * @returns Is page landscape + */ +const isLandscape = (page: PageNode) => getOrientation(page) === 'landscape'; + +export default isLandscape; diff --git a/packages/layout/src/page/isPortrait.js b/packages/layout/src/page/isPortrait.js deleted file mode 100644 index 757213e2d..000000000 --- a/packages/layout/src/page/isPortrait.js +++ /dev/null @@ -1,11 +0,0 @@ -import getOrientation from './getOrientation'; - -/** - * Return true if page is portrait - * - * @param {Object} page instance - * @returns {boolean} is page portrait - */ -const isPortrait = (page) => getOrientation(page) === 'portrait'; - -export default isPortrait; diff --git a/packages/layout/src/page/isPortrait.ts b/packages/layout/src/page/isPortrait.ts new file mode 100644 index 000000000..bf7d7f294 --- /dev/null +++ b/packages/layout/src/page/isPortrait.ts @@ -0,0 +1,12 @@ +import getOrientation from './getOrientation'; +import { PageNode } from '../types'; + +/** + * Return true if page is portrait + * + * @param page - Page node + * @returns Is page portrait + */ +const isPortrait = (page: PageNode) => getOrientation(page) === 'portrait'; + +export default isPortrait; diff --git a/packages/layout/src/steps/resolveBookmarks.js b/packages/layout/src/steps/resolveBookmarks.ts similarity index 65% rename from packages/layout/src/steps/resolveBookmarks.js rename to packages/layout/src/steps/resolveBookmarks.ts index 2c8070a9b..24cadb973 100644 --- a/packages/layout/src/steps/resolveBookmarks.js +++ b/packages/layout/src/steps/resolveBookmarks.ts @@ -1,14 +1,19 @@ -const getBookmarkValue = (title) => { - return typeof title === 'string' - ? { title, fit: false, expanded: false } - : title; +import { Bookmark, DocumentNode, Node } from '../types'; + +const getBookmarkValue = (bookmark: Bookmark) => { + return typeof bookmark === 'string' + ? { title: bookmark, fit: false, expanded: false } + : bookmark; }; -const resolveBookmarks = (node) => { +const resolveBookmarks = (node: DocumentNode) => { let refs = 0; const children = (node.children || []).slice(0); - const listToExplore = children.map((value) => ({ value, parent: null })); + const listToExplore: Node[] = children.map((value) => ({ + value, + parent: null, + })); while (listToExplore.length > 0) { const element = listToExplore.shift(); diff --git a/packages/layout/src/steps/resolveLinkSubstitution.js b/packages/layout/src/steps/resolveLinkSubstitution.ts similarity index 69% rename from packages/layout/src/steps/resolveLinkSubstitution.js rename to packages/layout/src/steps/resolveLinkSubstitution.ts index 617b66bca..2d62361ba 100644 --- a/packages/layout/src/steps/resolveLinkSubstitution.js +++ b/packages/layout/src/steps/resolveLinkSubstitution.ts @@ -1,7 +1,8 @@ import * as P from '@react-pdf/primitives'; import { compose } from '@react-pdf/fns'; +import { Node } from '../types'; -const isType = (type) => (node) => node.type === type; +const isType = (type: string) => (node: Node) => node.type === type; const isLink = isType(P.Link); @@ -12,26 +13,26 @@ const isTextInstance = isType(P.TextInstance); /** * Checks if node has render prop * - * @param {Object} node - * @returns {boolean} has render prop? + * @param node + * @returns Has render prop? */ -const hasRenderProp = (node) => !!node.props?.render; +const hasRenderProp = (node: Node) => 'render' in node.props; /** * Checks if node is text type (Text or TextInstance) * - * @param {Object} node - * @returns {boolean} are all children text instances? + * @param node + * @returns Are all children text instances? */ -const isTextType = (node) => isText(node) || isTextInstance(node); +const isTextType = (node: Node) => isText(node) || isTextInstance(node); /** * Checks if is tet link that needs to be wrapped in Text * - * @param {Object} node - * @returns {boolean} are all children text instances? + * @param node + * @returns Are all children text instances? */ -const isTextLink = (node) => { +const isTextLink = (node: Node) => { const children = node.children || []; // Text string inside a Link @@ -46,8 +47,8 @@ const isTextLink = (node) => { /** * Wraps node children inside Text node * - * @param {Object} node - * @returns {boolean} node with intermediate Text child + * @param node + * @returns Node with intermediate Text child */ const wrapText = (node) => { const textElement = { @@ -61,14 +62,14 @@ const wrapText = (node) => { return Object.assign({}, node, { children: [textElement] }); }; -const transformLink = (node) => { +const transformLink = (node: Node) => { if (!isLink(node)) return node; // If has render prop substitute the instance by a Text, that will // ultimately render the inline Link via the textkit PDF renderer. if (hasRenderProp(node)) return Object.assign({}, node, { type: P.Text }); - // If is a text link (either contains Text or TextInstalce), wrap it + // If is a text link (either contains Text or TextInstance), wrap it // inside a Text element so styles are applied correctly if (isTextLink(node)) return wrapText(node); @@ -79,10 +80,10 @@ const transformLink = (node) => { /** * Transforms Link layout to correctly render text and dynamic rendered links * - * @param {Object} node - * @returns {Object} node with link substitution + * @param node + * @returns Node with link substitution */ -const resolveLinkSubstitution = (node) => { +const resolveLinkSubstitution = (node: Node): Node => { if (!node.children) return node; const resolveChild = compose(transformLink, resolveLinkSubstitution); diff --git a/packages/layout/src/steps/resolvePageSizes.js b/packages/layout/src/steps/resolvePageSizes.ts similarity index 53% rename from packages/layout/src/steps/resolvePageSizes.js rename to packages/layout/src/steps/resolvePageSizes.ts index 6e5d34574..78217ef2c 100644 --- a/packages/layout/src/steps/resolvePageSizes.js +++ b/packages/layout/src/steps/resolvePageSizes.ts @@ -1,27 +1,27 @@ import { flatten } from '@react-pdf/stylesheet'; import getPageSize from '../page/getSize'; +import { DocumentNode, PageNode } from '../types'; /** * Resolves page size * - * @param {Object} page - * @returns {Object} page with resolved size in style attribute + * @param page + * @returns Page with resolved size in style attribute */ -export const resolvePageSize = (page) => { +export const resolvePageSize = (page: PageNode): PageNode => { const size = getPageSize(page); const style = flatten(page.style || {}); - const box = page.box || {}; - return { ...page, box, style: { ...style, ...size } }; + return { ...page, style: { ...style, ...size } }; }; /** * Resolves page sizes * - * @param {Object} root document root - * @returns {Object} document root with resolved page sizes + * @param root -Document root + * @returns Document root with resolved page sizes */ -const resolvePageSizes = (root) => { +const resolvePageSizes = (root: DocumentNode) => { if (!root.children) return root; const children = root.children.map(resolvePageSize); diff --git a/packages/layout/src/steps/resolveStyles.js b/packages/layout/src/steps/resolveStyles.js deleted file mode 100644 index 6d72a0666..000000000 --- a/packages/layout/src/steps/resolveStyles.js +++ /dev/null @@ -1,83 +0,0 @@ -import * as P from '@react-pdf/primitives'; -import stylesheet from '@react-pdf/stylesheet'; - -const isLink = (node) => node.type === P.Link; - -const DEFAULT_LINK_STYLES = { - color: 'blue', - textDecoration: 'underline', -}; - -/** - * Computes styles using stylesheet - * - * @param {Object} container - * @param {Object} node document node - * @returns {Object} computed styles - */ -const computeStyle = (container, node) => { - let baseStyle = node.style; - - if (isLink(node)) { - baseStyle = Array.isArray(node.style) - ? [DEFAULT_LINK_STYLES, ...node.style] - : [DEFAULT_LINK_STYLES, node.style]; - } - - return stylesheet(container, baseStyle); -}; - -/** - * @typedef {Function} ResolveNodeStyles - * @param {Object} node document node - * @returns {Object} node (and subnodes) with resolved styles - */ - -/** - * Resolves node styles - * - * @param {Object} container - * @returns {ResolveNodeStyles} resolve node styles - */ -const resolveNodeStyles = (container) => (node) => { - const style = computeStyle(container, node); - - if (!node.children) return Object.assign({}, node, { style }); - - const children = node.children.map(resolveNodeStyles(container)); - - return Object.assign({}, node, { style, children }); -}; - -/** - * Resolves page styles - * - * @param {Object} page document page - * @returns {Object} document page with resolved styles - */ -export const resolvePageStyles = (page) => { - const dpi = page.props?.dpi || 72; - const width = page.box?.width || page.style.width; - const height = page.box?.height || page.style.height; - const orientation = page.props?.orientation || 'portrait'; - const remBase = page.style?.fontSize || 18; - const container = { width, height, orientation, dpi, remBase }; - - return resolveNodeStyles(container)(page); -}; - -/** - * Resolves document styles - * - * @param {Object} root document root - * @returns {Object} document root with resolved styles - */ -const resolveStyles = (root) => { - if (!root.children) return root; - - const children = root.children.map(resolvePageStyles); - - return Object.assign({}, root, { children }); -}; - -export default resolveStyles; diff --git a/packages/layout/src/steps/resolveStyles.ts b/packages/layout/src/steps/resolveStyles.ts new file mode 100644 index 000000000..2d12f8a9e --- /dev/null +++ b/packages/layout/src/steps/resolveStyles.ts @@ -0,0 +1,88 @@ +import * as P from '@react-pdf/primitives'; +import stylesheet, { Container, Style } from '@react-pdf/stylesheet'; + +import { + DocumentNode, + Node, + PageNode, + SafeDocumentNode, + SafeNode, +} from '../types'; + +const isLink = (node: Node) => node.type === P.Link; + +const DEFAULT_LINK_STYLES: Style = { + color: 'blue', + textDecoration: 'underline', +}; + +/** + * Computes styles using stylesheet + * + * @param container + * @param node - Document node + * @returns Computed styles + */ +const computeStyle = (container: Container, node: Node) => { + let baseStyle: Style[] = [node.style as Style]; + + if (isLink(node)) { + baseStyle = Array.isArray(node.style) + ? [DEFAULT_LINK_STYLES, ...node.style] + : [DEFAULT_LINK_STYLES, node.style]; + } + + return stylesheet(container, baseStyle); +}; + +/** + * Resolves node styles + * + * @param container + * @returns Resolve node styles + */ +const resolveNodeStyles = + (container: Container) => + (node: Node): SafeNode => { + const style = computeStyle(container, node); + + if (!node.children) return Object.assign({}, node, { style }) as SafeNode; + + const children = node.children.map(resolveNodeStyles(container)); + + return Object.assign({}, node, { style, children }) as SafeNode; + }; + +/** + * Resolves page styles + * + * @param page Document page + * @returns Document page with resolved styles + */ +export const resolvePageStyles = (page: PageNode) => { + const dpi = page.props?.dpi || 72; + const style = page.style as Style; + const width = page.box?.width || style.width; + const height = page.box?.height || style.height; + const orientation = page.props?.orientation || 'portrait'; + const remBase = style?.fontSize || 18; + const container = { width, height, orientation, dpi, remBase } as Container; + + return resolveNodeStyles(container)(page); +}; + +/** + * Resolves document styles + * + * @param root - Document root + * @returns Document root with resolved styles + */ +const resolveStyles = (root: DocumentNode): SafeDocumentNode => { + if (!root.children) return root as SafeDocumentNode; + + const children = root.children.map(resolvePageStyles); + + return Object.assign({}, root, { children }) as SafeDocumentNode; +}; + +export default resolveStyles; diff --git a/packages/layout/src/steps/resolveYoga.js b/packages/layout/src/steps/resolveYoga.ts similarity index 62% rename from packages/layout/src/steps/resolveYoga.js rename to packages/layout/src/steps/resolveYoga.ts index 245fd81a0..c3f92badb 100644 --- a/packages/layout/src/steps/resolveYoga.js +++ b/packages/layout/src/steps/resolveYoga.ts @@ -1,6 +1,7 @@ import { loadYoga } from '../yoga/index'; +import { DocumentNode } from '../types'; -const resolveYoga = async (root) => { +const resolveYoga = async (root: DocumentNode) => { const yoga = await loadYoga(); return Object.assign({}, root, { yoga }); diff --git a/packages/layout/src/types.ts b/packages/layout/src/types.ts new file mode 100644 index 000000000..c33b3b673 --- /dev/null +++ b/packages/layout/src/types.ts @@ -0,0 +1,311 @@ +import * as React from 'react'; +import * as P from '@react-pdf/primitives'; +import { SafeStyle, Style } from '@react-pdf/stylesheet'; +import { HyphenationCallback } from '@react-pdf/font'; + +// Generics + +type YogaNode = { + getComputedPadding: (side: number) => number; +}; + +export type Box = { + width: number; + height?: number; + + // TODO: should be optional? + marginTop?: number; + marginRight?: number; + marginBottom?: number; + marginLeft?: number; + paddingTop?: number; + paddingRight?: number; + paddingBottom?: number; + paddingLeft?: number; +}; + +export interface ExpandedBookmark { + title: string; + top?: number; + left?: number; + zoom?: number; + fit?: true | false; + expanded?: true | false; +} + +export type Bookmark = string | ExpandedBookmark; + +type RenderProp = (props: { + pageNumber: number; + totalPages?: number; + subPageNumber: number; + subPageTotalPages?: number; +}) => React.ReactNode | null | undefined; + +type Safe = Omit< + T, + 'style' | 'children' +> & { + style: [T['style']] extends [never] ? never : SafeStyle; + children?: T['children'] extends Array + ? (U extends any ? Safe : never)[] + : T['children']; +}; + +type NodeProps = { + id?: string; + /** + * Render component in all wrapped pages. + * @see https://react-pdf.org/advanced#fixed-components + */ + fixed?: boolean; + /** + * Force the wrapping algorithm to start a new page when rendering the + * element. + * @see https://react-pdf.org/advanced#page-breaks + */ + break?: boolean; + /** + * Hint that no page wrapping should occur between all sibling elements following the element within n points + * @see https://react-pdf.org/advanced#orphan-&-widow-protection + */ + minPresenceAhead?: number; + bookmark?: Bookmark; +}; + +// Text Instance + +export type TextInstanceNode = { + type: typeof P.TextInstance; + props?: never; + style?: never; + box?: never; + children?: never; + yogaNode?: never; + value: string; +}; + +export type SafeTextInstanceNode = TextInstanceNode; + +// Text + +interface TextProps extends NodeProps { + id?: string; + /** + * Enable/disable page wrapping for element. + * @see https://react-pdf.org/components#page-wrapping + */ + wrap?: boolean; + /** + * Enables debug mode on page bounding box. + * @see https://react-pdf.org/advanced#debugging + */ + debug?: boolean; + render?: RenderProp; + /** + * Override the default hyphenation-callback + * @see https://react-pdf.org/fonts#registerhyphenationcallback + */ + hyphenationCallback?: HyphenationCallback; + /** + * Specifies the minimum number of lines in a text element that must be shown at the bottom of a page or its container. + * @see https://react-pdf.org/advanced#orphan-&-widow-protection + */ + orphans?: number; + /** + * Specifies the minimum number of lines in a text element that must be shown at the top of a page or its container.. + * @see https://react-pdf.org/advanced#orphan-&-widow-protection + */ + widows?: number; +} + +export type TextNode = { + type: typeof P.Text; + props: TextProps; + style?: Style | Style[]; + box?: Box; + yogaNode?: YogaNode; + children?: (TextNode | TextInstanceNode)[]; +}; + +export type SafeTextNode = Safe; + +// Link + +interface LinkProps extends NodeProps { + /** + * Enable/disable page wrapping for element. + * @see https://react-pdf.org/components#page-wrapping + */ + wrap?: boolean; + /** + * Enables debug mode on page bounding box. + * @see https://react-pdf.org/advanced#debugging + */ + debug?: boolean; + href?: string; + src?: string; + render?: RenderProp; +} + +export type LinkNode = { + type: typeof P.Link; + props: LinkProps; + style?: Style | Style[]; + box?: Box; + yogaNode?: YogaNode; + children?: (ViewNode | TextNode | TextInstanceNode)[]; +}; + +export type SafeLinkNode = Safe; + +// View + +interface ViewProps extends NodeProps { + id?: string; + /** + * Enable/disable page wrapping for element. + * @see https://react-pdf.org/components#page-wrapping + */ + wrap?: boolean; + /** + * Enables debug mode on page bounding box. + * @see https://react-pdf.org/advanced#debugging + */ + debug?: boolean; + render?: RenderProp; +} + +export type ViewNode = { + type: typeof P.View; + props: ViewProps; + style?: Style | Style[]; + box?: Box; + yogaNode?: YogaNode; + children?: (ViewNode | TextNode | LinkNode)[]; +}; + +export type SafeViewNode = Safe; + +// Page + +export type Orientation = 'portrait' | 'landscape'; + +export type StandardPageSize = + | '4A0' + | '2A0' + | 'A0' + | 'A1' + | 'A2' + | 'A3' + | 'A4' + | 'A5' + | 'A6' + | 'A7' + | 'A8' + | 'A9' + | 'A10' + | 'B0' + | 'B1' + | 'B2' + | 'B3' + | 'B4' + | 'B5' + | 'B6' + | 'B7' + | 'B8' + | 'B9' + | 'B10' + | 'C0' + | 'C1' + | 'C2' + | 'C3' + | 'C4' + | 'C5' + | 'C6' + | 'C7' + | 'C8' + | 'C9' + | 'C10' + | 'RA0' + | 'RA1' + | 'RA2' + | 'RA3' + | 'RA4' + | 'SRA0' + | 'SRA1' + | 'SRA2' + | 'SRA3' + | 'SRA4' + | 'EXECUTIVE' + | 'FOLIO' + | 'LEGAL' + | 'LETTER' + | 'TABLOID' + | 'ID1'; + +type StaticSize = number | string; + +export type PageSize = + | number + | StandardPageSize + | [StaticSize] + | [StaticSize, StaticSize] + | { width: StaticSize; height?: StaticSize }; + +interface PageProps extends NodeProps { + /** + * Enable page wrapping for this page. + * @see https://react-pdf.org/components#page-wrapping + */ + wrap?: boolean; + /** + * Enables debug mode on page bounding box. + * @see https://react-pdf.org/advanced#debugging + */ + debug?: boolean; + size?: PageSize; + orientation?: Orientation; + dpi?: number; +} + +export type PageNode = { + type: typeof P.Page; + props: PageProps; + style?: Style | Style[]; + box?: Box; + yogaNode?: YogaNode; + children?: (ViewNode | TextNode | LinkNode)[]; +}; + +export type SafePageNode = Safe; + +// Document + +export type DocumentNode = { + type: 'DOCUMENT'; + props: object; + box?: never; + style?: never; + yoga?: unknown; + yogaNode?: never; + children: PageNode[]; +}; + +export type SafeDocumentNode = Safe; + +export type Node = + | DocumentNode + | PageNode + | ViewNode + | LinkNode + | TextNode + | TextInstanceNode; + +export type SafeNode = + | SafeDocumentNode + | SafePageNode + | SafeViewNode + | SafeLinkNode + | SafeTextNode + | SafeTextInstanceNode; diff --git a/packages/layout/tests/page/getOrientation.test.js b/packages/layout/tests/page/getOrientation.test.js deleted file mode 100644 index d8c5c7841..000000000 --- a/packages/layout/tests/page/getOrientation.test.js +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect, test } from 'vitest'; - -import getOrientation from '../../src/page/getOrientation'; - -describe('page getOrientation', () => { - test('Should return portrait if no orientation provided', () => { - const page = { props: {} }; - - expect(getOrientation(page)).toBe('portrait'); - }); - - test('Should return landscape if landscape', () => { - const page = { props: { orientation: 'landscape' } }; - - expect(getOrientation(page)).toBe('landscape'); - }); - - test('Should return portrait if portait', () => { - const page = { props: { orientation: 'portrait' } }; - - expect(getOrientation(page)).toBe('portrait'); - }); - - test('Should return portrait if anything else', () => { - const page = { props: { orientation: 'boo' } }; - - expect(getOrientation(page)).toBe('portrait'); - }); -}); diff --git a/packages/layout/tests/page/getOrientation.test.ts b/packages/layout/tests/page/getOrientation.test.ts new file mode 100644 index 000000000..1b1690c41 --- /dev/null +++ b/packages/layout/tests/page/getOrientation.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, test } from 'vitest'; + +import getOrientation from '../../src/page/getOrientation'; + +describe('page getOrientation', () => { + test('Should return portrait if no orientation provided', () => { + const orientation = getOrientation({ type: 'PAGE', props: {} }); + + expect(orientation).toBe('portrait'); + }); + + test('Should return landscape if landscape', () => { + const orientation = getOrientation({ + type: 'PAGE', + props: { orientation: 'landscape' }, + }); + + expect(orientation).toBe('landscape'); + }); + + test('Should return portrait if portait', () => { + const orientation = getOrientation({ + type: 'PAGE', + props: { orientation: 'portrait' }, + }); + + expect(orientation).toBe('portrait'); + }); + + test('Should return portrait if anything else', () => { + const orientation = getOrientation({ + type: 'PAGE', + props: { orientation: 'boo' as any }, + }); + + expect(orientation).toBe('portrait'); + }); +}); diff --git a/packages/layout/tests/page/getSize.test.js b/packages/layout/tests/page/getSize.test.js deleted file mode 100644 index 97694c39b..000000000 --- a/packages/layout/tests/page/getSize.test.js +++ /dev/null @@ -1,87 +0,0 @@ -import { describe, expect, test } from 'vitest'; - -import getSize from '../../src/page/getSize'; - -describe('page getSize', () => { - test('Should default to A4', () => { - const page = { props: {} }; - const size = getSize(page); - - expect(size).toHaveProperty('width', 595.28); - expect(size).toHaveProperty('height', 841.89); - }); - - test('Should default to portrait A4', () => { - const page = { props: { orientation: 'portrait' } }; - const size = getSize(page); - - expect(size).toHaveProperty('width', 595.28); - expect(size).toHaveProperty('height', 841.89); - }); - - test('Should accept size string', () => { - const page = { props: { size: 'A2' } }; - const size = getSize(page); - - expect(size).toHaveProperty('width', 1190.55); - expect(size).toHaveProperty('height', 1683.78); - }); - - test('Should accept size string in landscape mode', () => { - const page = { props: { size: 'A2', orientation: 'landscape' } }; - const size = getSize(page); - - expect(size).toHaveProperty('width', 1683.78); - expect(size).toHaveProperty('height', 1190.55); - }); - - test('Should accept size array', () => { - const page = { props: { size: [100, 200] } }; - const size = getSize(page); - - expect(size).toHaveProperty('width', 100); - expect(size).toHaveProperty('height', 200); - }); - - test('Should accept size array in landscape mode', () => { - const page = { props: { size: [100, 200], orientation: 'landscape' } }; - const size = getSize(page); - - expect(size).toHaveProperty('width', 200); - expect(size).toHaveProperty('height', 100); - }); - - test('Should accept size object', () => { - const page = { props: { size: { width: 100, height: 200 } } }; - const size = getSize(page); - - expect(size).toHaveProperty('width', 100); - expect(size).toHaveProperty('height', 200); - }); - - test('Should accept size object in landscape mode', () => { - const page = { - props: { size: { width: 100, height: 200 }, orientation: 'landscape' }, - }; - const size = getSize(page); - - expect(size).toHaveProperty('width', 200); - expect(size).toHaveProperty('height', 100); - }); - - test('Should accept size number', () => { - const page = { props: { size: 100 } }; - const size = getSize(page); - - expect(size).toHaveProperty('width', 100); - expect(size).toHaveProperty('height', 100); - }); - - test('Should accept size number in landscape mode', () => { - const page = { props: { size: 100, orientation: 'landscape' } }; - const size = getSize(page); - - expect(size).toHaveProperty('width', 100); - expect(size).toHaveProperty('height', 100); - }); -}); diff --git a/packages/layout/tests/page/getSize.test.ts b/packages/layout/tests/page/getSize.test.ts new file mode 100644 index 000000000..b00a0ed7d --- /dev/null +++ b/packages/layout/tests/page/getSize.test.ts @@ -0,0 +1,133 @@ +import { describe, expect, test } from 'vitest'; + +import getSize from '../../src/page/getSize'; + +describe('page getSize', () => { + test('should default to A4', () => { + const size = getSize({ type: 'PAGE', props: {} }); + + expect(size).toHaveProperty('width', 595.28); + expect(size).toHaveProperty('height', 841.89); + }); + + test('should default to portrait A4', () => { + const size = getSize({ + type: 'PAGE', + props: { orientation: 'portrait' }, + }); + + expect(size).toHaveProperty('width', 595.28); + expect(size).toHaveProperty('height', 841.89); + }); + + test('should accept size string', () => { + const size = getSize({ type: 'PAGE', props: { size: 'A2' } }); + + expect(size).toHaveProperty('width', 1190.55); + expect(size).toHaveProperty('height', 1683.78); + }); + + test('should accept size string in landscape mode', () => { + const size = getSize({ + type: 'PAGE', + props: { size: 'A2', orientation: 'landscape' }, + }); + + expect(size).toHaveProperty('width', 1683.78); + expect(size).toHaveProperty('height', 1190.55); + }); + + test('should accept size number array', () => { + const size = getSize({ type: 'PAGE', props: { size: [100, 200] } }); + + expect(size).toHaveProperty('width', 100); + expect(size).toHaveProperty('height', 200); + }); + + test('should accept size string array', () => { + const size = getSize({ type: 'PAGE', props: { size: ['50px', '1in'] } }); + + expect(size).toHaveProperty('width', 50); + expect(size).toHaveProperty('height', 72); + }); + + test('should accept size number array in landscape mode', () => { + const size = getSize({ + type: 'PAGE', + props: { size: [100, 200], orientation: 'landscape' }, + }); + + expect(size).toHaveProperty('width', 200); + expect(size).toHaveProperty('height', 100); + }); + + test('should accept size string array in landscape mode', () => { + const size = getSize({ + type: 'PAGE', + props: { size: ['50px', '1in'], orientation: 'landscape' }, + }); + + expect(size).toHaveProperty('width', 72); + expect(size).toHaveProperty('height', 50); + }); + + test('should accept number size object', () => { + const size = getSize({ + type: 'PAGE', + props: { size: { width: 100, height: 200 } }, + }); + + expect(size).toHaveProperty('width', 100); + expect(size).toHaveProperty('height', 200); + }); + + test('should accept string size object', () => { + const size = getSize({ + type: 'PAGE', + props: { size: { width: '50px', height: '1in' } }, + }); + + expect(size).toHaveProperty('width', 50); + expect(size).toHaveProperty('height', 72); + }); + + test('should accept size object in landscape mode', () => { + const size = getSize({ + type: 'PAGE', + props: { size: { width: 100, height: 200 }, orientation: 'landscape' }, + }); + + expect(size).toHaveProperty('width', 200); + expect(size).toHaveProperty('height', 100); + }); + + test('should accept string size object in landscape mode', () => { + const size = getSize({ + type: 'PAGE', + props: { + size: { width: '50px', height: '1in' }, + orientation: 'landscape', + }, + }); + + expect(size).toHaveProperty('width', 72); + expect(size).toHaveProperty('height', 50); + }); + + test('should accept size number', () => { + const size = getSize({ type: 'PAGE', props: { size: 100 } }); + + expect(size).toHaveProperty('width', 100); + expect(size).toHaveProperty('height', 100); + }); + + test('should accept size number in landscape mode', () => { + const size = getSize({ + type: 'PAGE', + props: { size: 100, orientation: 'landscape' }, + }); + + expect(size).toHaveProperty('width', 100); + expect(size).toHaveProperty('height', 100); + }); +}); diff --git a/packages/layout/tests/page/isHeightAuto.test.js b/packages/layout/tests/page/isHeightAuto.test.js deleted file mode 100644 index f128240e5..000000000 --- a/packages/layout/tests/page/isHeightAuto.test.js +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, expect, test } from 'vitest'; - -import isHeightAuto from '../../src/page/isHeightAuto'; - -describe('page isHeightAuto', () => { - test('Should return false if height present', () => { - const page = { box: { height: 10 } }; - expect(isHeightAuto(page)).toBeFalsy(); - }); - - test('Should return false if height is zero', () => { - const page = { box: { height: 0 } }; - expect(isHeightAuto(page)).toBeFalsy(); - }); - - test('Should return false if height not present', () => { - const page = { box: {} }; - expect(isHeightAuto(page)).toBeTruthy(); - }); - - test('Should return false if height is null', () => { - const page = { box: { height: null } }; - expect(isHeightAuto(page)).toBeTruthy(); - }); - - test('Should return false if height is undefined', () => { - const page = { box: { height: undefined } }; - expect(isHeightAuto(page)).toBeTruthy(); - }); -}); diff --git a/packages/layout/tests/page/isHeightAuto.test.ts b/packages/layout/tests/page/isHeightAuto.test.ts new file mode 100644 index 000000000..5d67a8dd5 --- /dev/null +++ b/packages/layout/tests/page/isHeightAuto.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, test } from 'vitest'; + +import isHeightAuto from '../../src/page/isHeightAuto'; + +describe('page isHeightAuto', () => { + test('Should return false if height present', () => { + const result = isHeightAuto({ + type: 'PAGE', + props: {}, + box: { width: 100, height: 10 }, + }); + + expect(result).toBeFalsy(); + }); + + test('Should return false if height is zero', () => { + const result = isHeightAuto({ + type: 'PAGE', + props: {}, + box: { width: 100, height: 0 }, + }); + + expect(result).toBeFalsy(); + }); + + test('Should return false if height not present', () => { + const result = isHeightAuto({ + type: 'PAGE', + props: {}, + box: { width: 100 }, + }); + + expect(result).toBeTruthy(); + }); + + test('Should return false if height is null', () => { + const result = isHeightAuto({ + type: 'PAGE', + props: {}, + box: { width: 100, height: null as any }, + }); + + expect(result).toBeTruthy(); + }); + + test('Should return false if height is undefined', () => { + const result = isHeightAuto({ + type: 'PAGE', + props: {}, + box: { width: 100, height: undefined }, + }); + + expect(result).toBeTruthy(); + }); +}); diff --git a/packages/layout/tests/page/isLandscape.test.js b/packages/layout/tests/page/isLandscape.test.js deleted file mode 100644 index 5620fc068..000000000 --- a/packages/layout/tests/page/isLandscape.test.js +++ /dev/null @@ -1,23 +0,0 @@ -import { describe, expect, test } from 'vitest'; - -import isLandscape from '../../src/page/isLandscape'; - -describe('page isLandscape', () => { - test('Should return false if no orientation provided', () => { - const page = { props: {} }; - - expect(isLandscape(page)).toBeFalsy(); - }); - - test('Should return true if landscape', () => { - const page = { props: { orientation: 'landscape' } }; - - expect(isLandscape(page)).toBeTruthy(); - }); - - test('Should return false if portait', () => { - const page = { props: { orientation: 'portrait' } }; - - expect(isLandscape(page)).toBeFalsy(); - }); -}); diff --git a/packages/layout/tests/page/isLandscape.test.ts b/packages/layout/tests/page/isLandscape.test.ts new file mode 100644 index 000000000..954026965 --- /dev/null +++ b/packages/layout/tests/page/isLandscape.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from 'vitest'; + +import isLandscape from '../../src/page/isLandscape'; + +describe('page isLandscape', () => { + test('Should return false if no orientation provided', () => { + const result = isLandscape({ type: 'PAGE', props: {} }); + + expect(result).toBeFalsy(); + }); + + test('Should return true if landscape', () => { + const result = isLandscape({ + type: 'PAGE', + props: { orientation: 'landscape' }, + }); + + expect(result).toBeTruthy(); + }); + + test('Should return false if portait', () => { + const result = isLandscape({ + type: 'PAGE', + props: { orientation: 'portrait' }, + }); + + expect(result).toBeFalsy(); + }); +}); diff --git a/packages/layout/tests/page/isPortrait.test.js b/packages/layout/tests/page/isPortrait.test.js deleted file mode 100644 index c2a86d4f6..000000000 --- a/packages/layout/tests/page/isPortrait.test.js +++ /dev/null @@ -1,23 +0,0 @@ -import { describe, expect, test } from 'vitest'; - -import isPortrait from '../../src/page/isPortrait'; - -describe('page isPortrait', () => { - test('Should return true if no orientation provided', () => { - const page = { props: {} }; - - expect(isPortrait(page)).toBeTruthy(); - }); - - test('Should return false if landscape', () => { - const page = { props: { orientation: 'landscape' } }; - - expect(isPortrait(page)).toBeFalsy(); - }); - - test('Should return true if portait', () => { - const page = { props: { orientation: 'portrait' } }; - - expect(isPortrait(page)).toBeTruthy(); - }); -}); diff --git a/packages/layout/tests/page/isPortrait.test.ts b/packages/layout/tests/page/isPortrait.test.ts new file mode 100644 index 000000000..ddb4b3c51 --- /dev/null +++ b/packages/layout/tests/page/isPortrait.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from 'vitest'; + +import isPortrait from '../../src/page/isPortrait'; + +describe('page isPortrait', () => { + test('Should return true if no orientation provided', () => { + const result = isPortrait({ type: 'PAGE', props: {} }); + + expect(result).toBeTruthy(); + }); + + test('Should return false if landscape', () => { + const result = isPortrait({ + type: 'PAGE', + props: { orientation: 'landscape' }, + }); + + expect(result).toBeFalsy(); + }); + + test('Should return true if portait', () => { + const result = isPortrait({ + type: 'PAGE', + props: { orientation: 'portrait' }, + }); + + expect(result).toBeTruthy(); + }); +}); diff --git a/packages/layout/tests/steps/__snapshots__/resolveLinkSubstitution.test.ts.snap b/packages/layout/tests/steps/__snapshots__/resolveLinkSubstitution.test.ts.snap new file mode 100644 index 000000000..ef9327a91 --- /dev/null +++ b/packages/layout/tests/steps/__snapshots__/resolveLinkSubstitution.test.ts.snap @@ -0,0 +1,131 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`layout resolve link substitution > Should leave link with text children as it is 1`] = ` +{ + "children": [ + { + "children": [ + { + "children": [ + { + "props": {}, + "type": "TEXT", + }, + ], + "props": { + "src": "url", + }, + "type": "LINK", + }, + ], + "props": {}, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolve link substitution > Should replace link with only many text instances as children 1`] = ` +{ + "children": [ + { + "children": [ + { + "children": [ + { + "box": {}, + "children": [ + { + "type": "TEXT_INSTANCE", + "value": "1", + }, + { + "type": "TEXT_INSTANCE", + "value": "2", + }, + { + "type": "TEXT_INSTANCE", + "value": "3", + }, + { + "type": "TEXT_INSTANCE", + "value": "4", + }, + ], + "props": {}, + "style": {}, + "type": "TEXT", + }, + ], + "props": { + "src": "url", + }, + "type": "LINK", + }, + ], + "props": {}, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolve link substitution > Should replace link with only one text instance as children 1`] = ` +{ + "children": [ + { + "children": [ + { + "children": [ + { + "box": {}, + "children": [ + { + "type": "TEXT_INSTANCE", + "value": "1", + }, + ], + "props": {}, + "style": {}, + "type": "TEXT", + }, + ], + "props": { + "src": "url", + }, + "type": "LINK", + }, + ], + "props": {}, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolve link substitution > Should replace link with render prop 1`] = ` +{ + "children": [ + { + "children": [ + { + "props": { + "render": [Function], + }, + "type": "TEXT", + }, + ], + "props": {}, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; diff --git a/packages/layout/tests/steps/__snapshots__/resolveStyles.test.ts.snap b/packages/layout/tests/steps/__snapshots__/resolveStyles.test.ts.snap new file mode 100644 index 000000000..72716579e --- /dev/null +++ b/packages/layout/tests/steps/__snapshots__/resolveStyles.test.ts.snap @@ -0,0 +1,344 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`layout resolveStyles > Should overide default link styles 1`] = ` +{ + "children": [ + { + "box": { + "height": 200, + "width": 100, + }, + "children": [ + { + "props": {}, + "style": { + "color": "wheat", + "textDecoration": "none", + }, + "type": "LINK", + }, + ], + "props": {}, + "style": {}, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolveStyles > Should overide default link styles with array 1`] = ` +{ + "children": [ + { + "box": { + "height": 200, + "width": 100, + }, + "children": [ + { + "props": {}, + "style": { + "color": "wheat", + "textDecoration": "none", + }, + "type": "LINK", + }, + ], + "props": {}, + "style": {}, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolveStyles > Should resolve default link styles 1`] = ` +{ + "children": [ + { + "box": { + "height": 200, + "width": 100, + }, + "children": [ + { + "props": {}, + "style": { + "color": "blue", + "textDecoration": "underline", + }, + "type": "LINK", + }, + ], + "props": {}, + "style": {}, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolveStyles > Should resolve nested node styles 1`] = ` +{ + "children": [ + { + "box": { + "height": 200, + "width": 100, + }, + "children": [ + { + "props": {}, + "style": { + "backgroundColor": "red", + "borderBottomColor": "green", + "borderBottomStyle": "dotted", + "borderBottomWidth": 28.346456692913385, + "borderLeftColor": "green", + "borderLeftStyle": "dotted", + "borderLeftWidth": 28.346456692913385, + "borderRightColor": "green", + "borderRightStyle": "dotted", + "borderRightWidth": 28.346456692913385, + "borderTopColor": "green", + "borderTopStyle": "dotted", + "borderTopWidth": 28.346456692913385, + "paddingBottom": 28.346456692913385, + "paddingLeft": 720, + "paddingRight": 720, + "paddingTop": 28.346456692913385, + }, + "type": "VIEW", + }, + ], + "props": {}, + "style": {}, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolveStyles > Should resolve nested node styles array 1`] = ` +{ + "children": [ + { + "box": { + "height": 200, + "width": 100, + }, + "children": [ + { + "props": {}, + "style": { + "backgroundColor": "red", + "borderBottomColor": "green", + "borderBottomStyle": "dotted", + "borderBottomWidth": 28.346456692913385, + "borderLeftColor": "green", + "borderLeftStyle": "dotted", + "borderLeftWidth": 28.346456692913385, + "borderRightColor": "green", + "borderRightStyle": "dotted", + "borderRightWidth": 28.346456692913385, + "borderTopColor": "green", + "borderTopStyle": "dotted", + "borderTopWidth": 28.346456692913385, + "paddingBottom": 28.346456692913385, + "paddingLeft": 720, + "paddingRight": 720, + "paddingTop": 28.346456692913385, + }, + "type": "VIEW", + }, + ], + "props": {}, + "style": {}, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolveStyles > Should resolve nested node styles media queries 1`] = ` +{ + "children": [ + { + "box": { + "height": 200, + "width": 100, + }, + "children": [ + { + "props": {}, + "style": { + "backgroundColor": "green", + }, + "type": "VIEW", + }, + { + "props": {}, + "style": { + "backgroundColor": "red", + }, + "type": "VIEW", + }, + ], + "props": {}, + "style": {}, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolveStyles > Should resolve nested node styles media queries with page style 1`] = ` +{ + "children": [ + { + "children": [ + { + "props": {}, + "style": { + "backgroundColor": "green", + }, + "type": "VIEW", + }, + { + "props": {}, + "style": { + "backgroundColor": "red", + }, + "type": "VIEW", + }, + ], + "props": {}, + "style": { + "height": 200, + "width": 100, + }, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolveStyles > Should resolve page styles 1`] = ` +{ + "children": [ + { + "props": {}, + "style": { + "backgroundColor": "red", + "borderBottomColor": "green", + "borderBottomStyle": "dotted", + "borderBottomWidth": 28.346456692913385, + "borderLeftColor": "green", + "borderLeftStyle": "dotted", + "borderLeftWidth": 28.346456692913385, + "borderRightColor": "green", + "borderRightStyle": "dotted", + "borderRightWidth": 28.346456692913385, + "borderTopColor": "green", + "borderTopStyle": "dotted", + "borderTopWidth": 28.346456692913385, + "paddingBottom": 28.346456692913385, + "paddingLeft": 720, + "paddingRight": 720, + "paddingTop": 28.346456692913385, + }, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolveStyles > Should resolve page styles array 1`] = ` +{ + "children": [ + { + "props": {}, + "style": { + "backgroundColor": "red", + "borderBottomColor": "green", + "borderBottomStyle": "dotted", + "borderBottomWidth": 28.346456692913385, + "borderLeftColor": "green", + "borderLeftStyle": "dotted", + "borderLeftWidth": 28.346456692913385, + "borderRightColor": "green", + "borderRightStyle": "dotted", + "borderRightWidth": 28.346456692913385, + "borderTopColor": "green", + "borderTopStyle": "dotted", + "borderTopWidth": 28.346456692913385, + "paddingBottom": 28.346456692913385, + "paddingLeft": 720, + "paddingRight": 720, + "paddingTop": 28.346456692913385, + }, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolveStyles > Should resolve several pages styles 1`] = ` +{ + "children": [ + { + "props": {}, + "style": { + "borderTopColor": "#FF00000", + "borderTopStyle": "solid", + "borderTopWidth": 4, + "paddingBottom": 28.346456692913385, + "paddingLeft": 720, + "paddingRight": 720, + "paddingTop": 28.346456692913385, + }, + "type": "PAGE", + }, + { + "props": {}, + "style": { + "backgroundColor": "red", + "borderBottomColor": "green", + "borderBottomStyle": "dotted", + "borderBottomWidth": 28.346456692913385, + "borderLeftColor": "green", + "borderLeftStyle": "dotted", + "borderLeftWidth": 28.346456692913385, + "borderRightColor": "green", + "borderRightStyle": "dotted", + "borderRightWidth": 28.346456692913385, + "borderTopColor": "green", + "borderTopStyle": "dotted", + "borderTopWidth": 28.346456692913385, + "fontWeight": 700, + }, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; diff --git a/packages/layout/tests/steps/resolveBookmarks.test.js b/packages/layout/tests/steps/resolveBookmarks.test.ts similarity index 82% rename from packages/layout/tests/steps/resolveBookmarks.test.js rename to packages/layout/tests/steps/resolveBookmarks.test.ts index 085b73563..590e45d21 100644 --- a/packages/layout/tests/steps/resolveBookmarks.test.js +++ b/packages/layout/tests/steps/resolveBookmarks.test.ts @@ -1,26 +1,30 @@ import { describe, expect, test } from 'vitest'; import resolveBookmarks from '../../src/steps/resolveBookmarks'; +import { DocumentNode } from '../../src/types'; describe('layout resolveBookmarks', () => { test('should keep nodes the same if no bookmark passed', () => { const root = { type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', children: [{ type: 'VIEW', props: {} }], }, ], - }; + } as DocumentNode; + const result = resolveBookmarks(root); expect(result).toEqual(root); }); test('should resolve bookmark in page node', () => { - const root = { + const result = resolveBookmarks({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', @@ -28,10 +32,9 @@ describe('layout resolveBookmarks', () => { children: [{ type: 'VIEW', props: {} }], }, ], - }; - const result = resolveBookmarks(root); + }); - expect(result.children[0].props.bookmark).toEqual({ + expect(result.children?.[0]?.props?.bookmark).toEqual({ ref: 0, title: 'page', fit: false, @@ -40,8 +43,9 @@ describe('layout resolveBookmarks', () => { }); test('should resolve bookmark hierarchy', () => { - const root = { + const result = resolveBookmarks({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', @@ -76,12 +80,12 @@ describe('layout resolveBookmarks', () => { ], }, ], - }; - const result = resolveBookmarks(root); + }); + const page = result.children[0]; - const view = page.children[1]; - const nestedView = page.children[0].children[0]; - const subNestedView = nestedView.children[0]; + const view = page.children![1]; + const nestedView = page.children![0].children![0]; + const subNestedView = nestedView!.children![0]; expect(page.props.bookmark).toEqual({ ref: 0, @@ -98,7 +102,7 @@ describe('layout resolveBookmarks', () => { expanded: false, }); - expect(nestedView.props.bookmark).toEqual({ + expect(nestedView.props!.bookmark).toEqual({ ref: 2, parent: 0, title: 'chapter 1', @@ -106,7 +110,7 @@ describe('layout resolveBookmarks', () => { expanded: false, }); - expect(subNestedView.props.bookmark).toEqual({ + expect(subNestedView!.props!.bookmark).toEqual({ ref: 3, parent: 2, title: 'sub chapter', @@ -116,8 +120,9 @@ describe('layout resolveBookmarks', () => { }); test('should resolve bookmark object prop', () => { - const root = { + const result = resolveBookmarks({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', @@ -133,8 +138,7 @@ describe('layout resolveBookmarks', () => { children: [{ type: 'VIEW', props: {} }], }, ], - }; - const result = resolveBookmarks(root); + }); expect(result.children[0].props.bookmark).toEqual({ ref: 0, diff --git a/packages/layout/tests/steps/resolveLinkSubstitution.test.js b/packages/layout/tests/steps/resolveLinkSubstitution.test.ts similarity index 56% rename from packages/layout/tests/steps/resolveLinkSubstitution.test.js rename to packages/layout/tests/steps/resolveLinkSubstitution.test.ts index c2546dc86..b68c12390 100644 --- a/packages/layout/tests/steps/resolveLinkSubstitution.test.js +++ b/packages/layout/tests/steps/resolveLinkSubstitution.test.ts @@ -2,13 +2,15 @@ import { describe, expect, test } from 'vitest'; import resolveLinkSubstitution from '../../src/steps/resolveLinkSubstitution'; -describe('layout resolveStyles', () => { - test('should leave link with text children as it is', () => { - const root = { +describe('layout resolve link substitution', () => { + test('Should leave link with text children as it is', () => { + const result = resolveLinkSubstitution({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, children: [ { type: 'LINK', @@ -16,81 +18,86 @@ describe('layout resolveStyles', () => { children: [ { type: 'TEXT', + props: {}, }, ], }, ], }, ], - }; - const result = resolveLinkSubstitution(root); + }); expect(result).toMatchSnapshot(); }); - test('should replace link with only one text instance as children', () => { - const root = { + test('Should replace link with only one text instance as children', () => { + const result = resolveLinkSubstitution({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, children: [ { type: 'LINK', props: { src: 'url' }, - children: [{ type: 'TEXT_INSTANCE' }], + children: [{ type: 'TEXT_INSTANCE', value: '1' }], }, ], }, ], - }; - const result = resolveLinkSubstitution(root); + }); expect(result).toMatchSnapshot(); }); - test('should replace link with only many text instances as children', () => { - const root = { + test('Should replace link with only many text instances as children', () => { + const result = resolveLinkSubstitution({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, children: [ { type: 'LINK', props: { src: 'url' }, children: [ - { type: 'TEXT_INSTANCE' }, - { type: 'TEXT_INSTANCE' }, - { type: 'TEXT_INSTANCE' }, - { type: 'TEXT_INSTANCE' }, + { type: 'TEXT_INSTANCE', value: '1' }, + { type: 'TEXT_INSTANCE', value: '2' }, + { type: 'TEXT_INSTANCE', value: '3' }, + { type: 'TEXT_INSTANCE', value: '4' }, ], }, ], }, ], - }; - const result = resolveLinkSubstitution(root); + }); expect(result).toMatchSnapshot(); }); - test('should replace link with render prop', () => { - const root = { + test('Should replace link with render prop', () => { + const result = resolveLinkSubstitution({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, children: [ { type: 'LINK', - props: { render: () => {} }, + props: { + render: () => null, + }, }, ], }, ], - }; - const result = resolveLinkSubstitution(root); + }); expect(result).toMatchSnapshot(); }); diff --git a/packages/layout/tests/steps/resolveStyles.test.js b/packages/layout/tests/steps/resolveStyles.test.ts similarity index 83% rename from packages/layout/tests/steps/resolveStyles.test.js rename to packages/layout/tests/steps/resolveStyles.test.ts index 0b06f50d8..e0058720f 100644 --- a/packages/layout/tests/steps/resolveStyles.test.js +++ b/packages/layout/tests/steps/resolveStyles.test.ts @@ -4,11 +4,13 @@ import resolveStyles from '../../src/steps/resolveStyles'; describe('layout resolveStyles', () => { test('Should resolve page styles', () => { - const root = { + const result = resolveStyles({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, style: { paddingHorizontal: '10in', paddingVertical: '10mm', @@ -17,18 +19,19 @@ describe('layout resolveStyles', () => { }, }, ], - }; - const result = resolveStyles(root); + }); expect(result).toMatchSnapshot(); }); test('Should resolve several pages styles', () => { - const root = { + const result = resolveStyles({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, style: { paddingHorizontal: '10in', paddingVertical: '10mm', @@ -37,6 +40,7 @@ describe('layout resolveStyles', () => { }, { type: 'PAGE', + props: {}, style: { backgroundColor: 'red', border: '1cm dotted green', @@ -44,18 +48,19 @@ describe('layout resolveStyles', () => { }, }, ], - }; - const result = resolveStyles(root); + }); expect(result).toMatchSnapshot(); }); test('Should resolve page styles array', () => { - const root = { + const result = resolveStyles({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, style: [ { paddingHorizontal: '10in', @@ -68,22 +73,24 @@ describe('layout resolveStyles', () => { ], }, ], - }; - const result = resolveStyles(root); + }); expect(result).toMatchSnapshot(); }); test('Should resolve nested node styles', () => { - const root = { + const result = resolveStyles({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, box: { width: 100, height: 200 }, children: [ { type: 'VIEW', + props: {}, style: { paddingHorizontal: '10in', paddingVertical: '10mm', @@ -94,22 +101,24 @@ describe('layout resolveStyles', () => { ], }, ], - }; - const result = resolveStyles(root); + }); expect(result).toMatchSnapshot(); }); test('Should resolve nested node styles media queries', () => { - const root = { + const result = resolveStyles({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, box: { width: 100, height: 200 }, children: [ { type: 'VIEW', + props: {}, style: { backgroundColor: 'red', '@media max-width: 500': { @@ -119,6 +128,7 @@ describe('layout resolveStyles', () => { }, { type: 'VIEW', + props: {}, style: { backgroundColor: 'red', '@media min-width: 500': { @@ -129,22 +139,24 @@ describe('layout resolveStyles', () => { ], }, ], - }; - const result = resolveStyles(root); + }); expect(result).toMatchSnapshot(); }); test('Should resolve nested node styles media queries with page style', () => { - const root = { + const result = resolveStyles({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, style: { width: 100, height: 200 }, children: [ { type: 'VIEW', + props: {}, style: { backgroundColor: 'red', '@media max-width: 500': { @@ -154,6 +166,7 @@ describe('layout resolveStyles', () => { }, { type: 'VIEW', + props: {}, style: { backgroundColor: 'red', '@media min-width: 500': { @@ -164,22 +177,24 @@ describe('layout resolveStyles', () => { ], }, ], - }; - const result = resolveStyles(root); + }); expect(result).toMatchSnapshot(); }); test('Should resolve nested node styles array', () => { - const root = { + const result = resolveStyles({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, box: { width: 100, height: 200 }, children: [ { type: 'VIEW', + props: {}, style: [ { paddingHorizontal: '10in', @@ -194,71 +209,76 @@ describe('layout resolveStyles', () => { ], }, ], - }; - const result = resolveStyles(root); + }); expect(result).toMatchSnapshot(); }); test('Should resolve default link styles', () => { - const root = { + const result = resolveStyles({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, box: { width: 100, height: 200 }, children: [ { type: 'LINK', + props: {}, style: {}, }, ], }, ], - }; - const result = resolveStyles(root); + }); expect(result).toMatchSnapshot(); }); test('Should overide default link styles', () => { - const root = { + const result = resolveStyles({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, box: { width: 100, height: 200 }, children: [ { type: 'LINK', + props: {}, style: { color: 'wheat', textDecoration: 'none' }, }, ], }, ], - }; - const result = resolveStyles(root); + }); expect(result).toMatchSnapshot(); }); test('Should overide default link styles with array', () => { - const root = { + const result = resolveStyles({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, box: { width: 100, height: 200 }, children: [ { type: 'LINK', + props: {}, style: [{ color: 'wheat', textDecoration: 'none' }], }, ], }, ], - }; - const result = resolveStyles(root); + }); expect(result).toMatchSnapshot(); }); diff --git a/packages/layout/tsconfig.json b/packages/layout/tsconfig.json new file mode 100644 index 000000000..490d6d29a --- /dev/null +++ b/packages/layout/tsconfig.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "outDir": "lib", + "declaration": true, + "declarationDir": "lib/types", + "target": "ES2022", + "module": "ESNext", + "lib": ["ES2022", "DOM"], + "moduleResolution": "Node", + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": false, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["vitest/globals"], + }, + "include": ["src", "globals.d.ts"], +} diff --git a/packages/stylesheet/src/index.ts b/packages/stylesheet/src/index.ts index 72c40bbb6..18f7936d0 100644 --- a/packages/stylesheet/src/index.ts +++ b/packages/stylesheet/src/index.ts @@ -31,6 +31,6 @@ export { default as transformColor } from './utils/colors'; export { default as flatten } from './flatten'; -export { Style, SafeStyle } from './types'; +export { Style, SafeStyle, Container } from './types'; export default resolveStyles; From fda1ccac416615e7bc0d6e062a467c2c08a13f3c Mon Sep 17 00:00:00 2001 From: Diego Muracciole Date: Mon, 24 Feb 2025 01:22:36 +0100 Subject: [PATCH 2/3] refactor: convert layout package to TS --- packages/fns/src/asyncCompose.ts | 6 +- packages/fns/src/capitalize.ts | 2 +- packages/fns/src/evolve.ts | 17 +- packages/fns/src/index.ts | 1 + packages/fns/src/isNil.ts | 2 +- packages/fns/src/parseFloat.ts | 11 + packages/fns/tests/parseFloat.test.ts | 29 ++ packages/fns/tsconfig.json | 2 +- packages/font/src/index.ts | 25 +- packages/image/src/index.ts | 2 +- packages/image/src/jpeg.ts | 2 +- packages/image/src/types.ts | 1 + packages/layout/babel.config.js | 3 - packages/layout/globals.d.ts | 245 +++++++++++ packages/layout/package.json | 1 + .../{measureCanvas.js => measureCanvas.ts} | 69 +-- .../image/{fetchImage.js => fetchImage.ts} | 13 +- packages/layout/src/image/getRatio.js | 11 - packages/layout/src/image/getRatio.ts | 13 + packages/layout/src/image/getSource.js | 10 - packages/layout/src/image/getSource.ts | 14 + packages/layout/src/image/measureImage.js | 86 ---- packages/layout/src/image/measureImage.ts | 82 ++++ .../{resolveSource.js => resolveSource.ts} | 8 +- packages/layout/src/index.ts | 2 + ...{createInstances.js => createInstances.ts} | 40 +- .../{getBorderWidth.js => getBorderWidth.ts} | 14 +- .../node/{getDimension.js => getDimension.ts} | 8 +- .../src/node/{getMargin.js => getMargin.ts} | 18 +- .../src/node/{getOrigin.js => getOrigin.ts} | 18 +- packages/layout/src/node/getPadding.ts | 14 +- .../node/{getPosition.js => getPosition.ts} | 8 +- .../src/node/{getWrap.js => getWrap.ts} | 8 +- packages/layout/src/node/isFixed.js | 3 - packages/layout/src/node/isFixed.ts | 9 + .../{removePaddings.js => removePaddings.ts} | 9 +- .../src/node/{setAlign.js => setAlign.ts} | 21 +- ...{setAlignContent.js => setAlignContent.ts} | 6 +- .../{setAlignItems.js => setAlignItems.ts} | 6 +- .../node/{setAlignSelf.js => setAlignSelf.ts} | 6 +- .../{setAspectRatio.js => setAspectRatio.ts} | 13 +- .../{setBorderWidth.js => setBorderWidth.ts} | 37 +- .../node/{setDimension.js => setDimension.ts} | 36 +- .../src/node/{setDisplay.js => setDisplay.ts} | 13 +- .../node/{setFlexBasis.js => setFlexBasis.ts} | 6 +- ...etFlexDirection.js => setFlexDirection.ts} | 14 +- packages/layout/src/node/setFlexGrow.js | 19 - packages/layout/src/node/setFlexGrow.ts | 14 + packages/layout/src/node/setFlexShrink.js | 19 - packages/layout/src/node/setFlexShrink.ts | 14 + .../node/{setFlexWrap.js => setFlexWrap.ts} | 15 +- .../layout/src/node/{setGap.js => setGap.ts} | 18 +- ...JustifyContent.js => setJustifyContent.ts} | 13 +- packages/layout/src/node/setMargin.js | 62 --- packages/layout/src/node/setMargin.ts | 58 +++ .../node/{setOverflow.js => setOverflow.ts} | 13 +- packages/layout/src/node/setPadding.js | 62 --- packages/layout/src/node/setPadding.ts | 58 +++ packages/layout/src/node/setPosition.js | 62 --- packages/layout/src/node/setPosition.ts | 58 +++ ...{setPositionType.js => setPositionType.ts} | 13 +- packages/layout/src/node/setYogaValue.js | 58 --- packages/layout/src/node/setYogaValue.ts | 52 +++ .../node/{shouldBreak.js => shouldBreak.ts} | 24 +- .../src/node/{splitNode.js => splitNode.ts} | 12 +- packages/layout/src/page/getContentArea.ts | 7 +- packages/layout/src/page/getSize.ts | 7 +- packages/layout/src/page/getWrapArea.ts | 6 +- packages/layout/src/page/isHeightAuto.ts | 4 +- .../{resolveAssets.js => resolveAssets.ts} | 38 +- packages/layout/src/steps/resolveBookmarks.ts | 14 +- ...olveDimensions.js => resolveDimensions.ts} | 106 ++--- ...veInheritance.js => resolveInheritance.ts} | 50 ++- .../src/steps/resolveLinkSubstitution.ts | 1 + .../{resolveOrigins.js => resolveOrigins.ts} | 13 +- .../layout/src/steps/resolvePagePaddings.js | 75 ---- .../layout/src/steps/resolvePagePaddings.ts | 70 +++ packages/layout/src/steps/resolvePageSizes.ts | 1 + ...olvePagination.js => resolvePagination.ts} | 103 +++-- ...rcentHeight.js => resolvePercentHeight.ts} | 47 +- ...rcentRadius.js => resolvePercentRadius.ts} | 24 +- .../steps/{resolveSvg.js => resolveSvg.ts} | 162 ++++--- ...olveTextLayout.js => resolveTextLayout.ts} | 21 +- packages/layout/src/steps/resolveYoga.ts | 2 +- packages/layout/src/steps/resolveZIndex.js | 36 -- packages/layout/src/steps/resolveZIndex.ts | 47 ++ .../svg/{getContainer.js => getContainer.ts} | 5 +- packages/layout/src/svg/getDefs.js | 17 - packages/layout/src/svg/getDefs.ts | 19 + .../svg/{inheritProps.js => inheritProps.ts} | 3 +- .../src/svg/{layoutText.js => layoutText.ts} | 41 +- .../src/svg/{measureSvg.js => measureSvg.ts} | 24 +- packages/layout/src/svg/parseAspectRatio.js | 13 - packages/layout/src/svg/parseAspectRatio.ts | 19 + .../svg/{parseViewbox.js => parseViewbox.ts} | 9 +- packages/layout/src/svg/replaceDefs.js | 51 --- packages/layout/src/svg/replaceDefs.ts | 60 +++ .../layout/src/text/{emoji.js => emoji.ts} | 47 +- ...ontSubstitution.js => fontSubstitution.ts} | 14 +- packages/layout/src/text/fromFragments.js | 27 -- ...ibutedString.js => getAttributedString.ts} | 55 ++- ...ghtAtLineIndex.js => heightAtLineIndex.ts} | 8 +- .../text/{ignoreChars.js => ignoreChars.ts} | 6 +- .../src/text/{layoutText.js => layoutText.ts} | 19 +- ...eIndexAtHeight.js => lineIndexAtHeight.ts} | 8 +- .../text/{linesHeight.js => linesHeight.ts} | 8 +- .../src/text/{linesWidth.js => linesWidth.ts} | 8 +- packages/layout/src/text/measureText.js | 49 --- packages/layout/src/text/measureText.ts | 49 +++ .../src/text/{splitText.js => splitText.ts} | 17 +- .../text/{standardFont.js => standardFont.ts} | 10 +- .../{transformText.js => transformText.ts} | 6 +- packages/layout/src/types.ts | 311 ------------- packages/layout/src/types/base.ts | 118 +++++ packages/layout/src/types/canvas.ts | 26 ++ packages/layout/src/types/checkbox.ts | 28 ++ packages/layout/src/types/circle.ts | 25 ++ packages/layout/src/types/clip-path.ts | 43 ++ packages/layout/src/types/defs.ts | 26 ++ packages/layout/src/types/document.ts | 23 + packages/layout/src/types/ellipse.ts | 26 ++ packages/layout/src/types/field-set.ts | 27 ++ packages/layout/src/types/g.ts | 51 +++ packages/layout/src/types/image.ts | 73 ++++ packages/layout/src/types/index.ts | 28 ++ packages/layout/src/types/line.ts | 26 ++ packages/layout/src/types/linear-gradient.ts | 25 ++ packages/layout/src/types/link.ts | 40 ++ packages/layout/src/types/node.ts | 90 ++++ packages/layout/src/types/note.ts | 20 + packages/layout/src/types/page.ts | 129 ++++++ packages/layout/src/types/path.ts | 23 + packages/layout/src/types/polygon.ts | 23 + packages/layout/src/types/polyline.ts | 23 + packages/layout/src/types/radial-gradient.ts | 26 ++ packages/layout/src/types/rect.ts | 28 ++ packages/layout/src/types/select.ts | 55 +++ packages/layout/src/types/stop.ts | 19 + packages/layout/src/types/svg.ts | 81 ++++ packages/layout/src/types/text-input.ts | 60 +++ packages/layout/src/types/text-instance.ts | 14 + packages/layout/src/types/text.ts | 59 +++ packages/layout/src/types/tspan.ts | 25 ++ packages/layout/src/types/view.ts | 63 +++ .../layout/src/yoga/{index.js => index.ts} | 4 +- packages/layout/tests/image/getSource.test.js | 27 -- packages/layout/tests/image/getSource.test.ts | 25 ++ ...veSource.test.js => resolveSource.test.ts} | 0 ...erWidth.test.js => getBorderWidth.test.ts} | 15 +- ...Dimension.test.js => getDimension.test.ts} | 15 +- .../{getMargin.test.js => getMargin.test.ts} | 59 ++- .../{getOrigin.test.js => getOrigin.test.ts} | 27 +- ...{getPadding.test.js => getPadding.test.ts} | 59 ++- ...etPosition.test.js => getPosition.test.ts} | 15 +- .../layout/tests/node/removePaddings.test.js | 45 -- .../layout/tests/node/removePaddings.test.ts | 85 ++++ ...ontent.test.js => setAlignContent.test.ts} | 17 +- ...ignItems.test.js => setAlignItems.test.ts} | 17 +- ...AlignSelf.test.js => setAlignSelf.test.ts} | 17 +- ...ctRatio.test.js => setAspectRatio.test.ts} | 17 +- ...erWidth.test.js => setBorderWidth.test.ts} | 25 +- ...Dimension.test.js => setDimension.test.ts} | 21 +- ...{setDisplay.test.js => setDisplay.test.ts} | 18 +- ...FlexBasis.test.js => setFlexBasis.test.ts} | 18 +- ...ction.test.js => setFlexDirection.test.ts} | 18 +- ...etFlexGrow.test.js => setFlexGrow.test.ts} | 18 +- ...exShrink.test.js => setFlexShrink.test.ts} | 18 +- ...etFlexWrap.test.js => setFlexWrap.test.ts} | 18 +- ...tent.test.js => setJustifyContent.test.ts} | 18 +- .../{setMargin.test.js => setMargin.test.ts} | 20 +- ...etOverflow.test.js => setOverflow.test.ts} | 18 +- ...{setPadding.test.js => setPadding.test.ts} | 20 +- ...etPosition.test.js => setPosition.test.ts} | 20 +- ...onType.test.js => setPositionType.test.ts} | 18 +- ...houldBreak.test.js => shouldBreak.test.ts} | 409 ++++++++++++++++-- .../__snapshots__/resolveOrigins.test.js.snap | 4 +- .../__snapshots__/resolveOrigins.test.ts.snap | 153 +++++++ .../resolvePagePaddings.test.ts.snap | 190 ++++++++ .../resolvePercentHeight.test.ts.snap | 73 ++++ .../__snapshots__/resolveStyles.test.ts.snap | 24 + ...ance.test.js => resolveInhritance.test.ts} | 71 +-- ...Origins.test.js => resolveOrigins.test.ts} | 83 +++- ...gs.test.js => resolvePagePaddings.test.ts} | 89 ++-- ...Sizes.test.js => resolvePageSizes.test.ts} | 73 ++-- ...tion.test.js => resolvePagination.test.ts} | 105 ++--- ...t.test.js => resolvePercentHeight.test.ts} | 35 +- .../layout/tests/steps/resolveStyles.test.ts | 54 ++- ...yout.test.js => resolveTextLayout.test.ts} | 21 +- ...ution.test.js => fontSubstitution.test.ts} | 44 +- .../layout/tests/text/fromFragments.test.js | 43 -- ...ndex.test.js => heightAtLineIndex.test.ts} | 35 +- ...{layoutText.test.js => layoutText.test.ts} | 48 +- ...ight.test.js => lineIndexAtHeight.test.ts} | 51 ++- packages/stylesheet/src/resolve/text.ts | 1 + packages/stylesheet/src/types.ts | 50 ++- packages/stylesheet/tests/dimensions.test.ts | 26 ++ packages/textkit/package.json | 3 + packages/textkit/src/glyph/fromCodePoint.ts | 1 - packages/textkit/src/index.ts | 4 + .../textkit/src/layout/finalizeFragments.ts | 16 +- packages/textkit/src/types.ts | 51 +-- packages/textkit/tests/internal/font.ts | 4 +- 202 files changed, 4895 insertions(+), 2246 deletions(-) create mode 100644 packages/fns/src/parseFloat.ts create mode 100644 packages/fns/tests/parseFloat.test.ts delete mode 100644 packages/layout/babel.config.js create mode 100644 packages/layout/globals.d.ts rename packages/layout/src/canvas/{measureCanvas.js => measureCanvas.ts} (59%) rename packages/layout/src/image/{fetchImage.js => fetchImage.ts} (70%) delete mode 100644 packages/layout/src/image/getRatio.js create mode 100644 packages/layout/src/image/getRatio.ts delete mode 100644 packages/layout/src/image/getSource.js create mode 100644 packages/layout/src/image/getSource.ts delete mode 100644 packages/layout/src/image/measureImage.js create mode 100644 packages/layout/src/image/measureImage.ts rename packages/layout/src/image/{resolveSource.js => resolveSource.ts} (67%) rename packages/layout/src/node/{createInstances.js => createInstances.ts} (52%) rename packages/layout/src/node/{getBorderWidth.js => getBorderWidth.ts} (61%) rename packages/layout/src/node/{getDimension.js => getDimension.ts} (72%) rename packages/layout/src/node/{getMargin.js => getMargin.ts} (66%) rename packages/layout/src/node/{getOrigin.js => getOrigin.ts} (56%) rename packages/layout/src/node/{getPosition.js => getPosition.ts} (69%) rename packages/layout/src/node/{getWrap.js => getWrap.ts} (53%) delete mode 100644 packages/layout/src/node/isFixed.js create mode 100644 packages/layout/src/node/isFixed.ts rename packages/layout/src/node/{removePaddings.js => removePaddings.ts} (67%) rename packages/layout/src/node/{setAlign.js => setAlign.ts} (65%) rename packages/layout/src/node/{setAlignContent.js => setAlignContent.ts} (64%) rename packages/layout/src/node/{setAlignItems.js => setAlignItems.ts} (63%) rename packages/layout/src/node/{setAlignSelf.js => setAlignSelf.ts} (62%) rename packages/layout/src/node/{setAspectRatio.js => setAspectRatio.ts} (50%) rename packages/layout/src/node/{setBorderWidth.js => setBorderWidth.ts} (52%) rename packages/layout/src/node/{setDimension.js => setDimension.ts} (53%) rename packages/layout/src/node/{setDisplay.js => setDisplay.ts} (53%) rename packages/layout/src/node/{setFlexBasis.js => setFlexBasis.ts} (64%) rename packages/layout/src/node/{setFlexDirection.js => setFlexDirection.ts} (64%) delete mode 100644 packages/layout/src/node/setFlexGrow.js create mode 100644 packages/layout/src/node/setFlexGrow.ts delete mode 100644 packages/layout/src/node/setFlexShrink.js create mode 100644 packages/layout/src/node/setFlexShrink.ts rename packages/layout/src/node/{setFlexWrap.js => setFlexWrap.ts} (50%) rename packages/layout/src/node/{setGap.js => setGap.ts} (53%) rename packages/layout/src/node/{setJustifyContent.js => setJustifyContent.ts} (69%) delete mode 100644 packages/layout/src/node/setMargin.js create mode 100644 packages/layout/src/node/setMargin.ts rename packages/layout/src/node/{setOverflow.js => setOverflow.ts} (62%) delete mode 100644 packages/layout/src/node/setPadding.js create mode 100644 packages/layout/src/node/setPadding.ts delete mode 100644 packages/layout/src/node/setPosition.js create mode 100644 packages/layout/src/node/setPosition.ts rename packages/layout/src/node/{setPositionType.js => setPositionType.ts} (62%) delete mode 100644 packages/layout/src/node/setYogaValue.js create mode 100644 packages/layout/src/node/setYogaValue.ts rename packages/layout/src/node/{shouldBreak.js => shouldBreak.ts} (63%) rename packages/layout/src/node/{splitNode.js => splitNode.ts} (72%) rename packages/layout/src/steps/{resolveAssets.js => resolveAssets.ts} (54%) rename packages/layout/src/steps/{resolveDimensions.js => resolveDimensions.ts} (77%) rename packages/layout/src/steps/{resolveInheritance.js => resolveInheritance.ts} (62%) rename packages/layout/src/steps/{resolveOrigins.js => resolveOrigins.ts} (66%) delete mode 100644 packages/layout/src/steps/resolvePagePaddings.js create mode 100644 packages/layout/src/steps/resolvePagePaddings.ts rename packages/layout/src/steps/{resolvePagination.js => resolvePagination.ts} (74%) rename packages/layout/src/steps/{resolvePercentHeight.js => resolvePercentHeight.ts} (57%) rename packages/layout/src/steps/{resolvePercentRadius.js => resolvePercentRadius.ts} (58%) rename packages/layout/src/steps/{resolveSvg.js => resolveSvg.ts} (51%) rename packages/layout/src/steps/{resolveTextLayout.js => resolveTextLayout.ts} (56%) delete mode 100644 packages/layout/src/steps/resolveZIndex.js create mode 100644 packages/layout/src/steps/resolveZIndex.ts rename packages/layout/src/svg/{getContainer.js => getContainer.ts} (75%) delete mode 100644 packages/layout/src/svg/getDefs.js create mode 100644 packages/layout/src/svg/getDefs.ts rename packages/layout/src/svg/{inheritProps.js => inheritProps.ts} (94%) rename packages/layout/src/svg/{layoutText.js => layoutText.ts} (77%) rename packages/layout/src/svg/{measureSvg.js => measureSvg.ts} (59%) delete mode 100644 packages/layout/src/svg/parseAspectRatio.js create mode 100644 packages/layout/src/svg/parseAspectRatio.ts rename packages/layout/src/svg/{parseViewbox.js => parseViewbox.ts} (56%) delete mode 100644 packages/layout/src/svg/replaceDefs.js create mode 100644 packages/layout/src/svg/replaceDefs.ts rename packages/layout/src/text/{emoji.js => emoji.ts} (71%) rename packages/layout/src/text/{fontSubstitution.js => fontSubstitution.ts} (86%) delete mode 100644 packages/layout/src/text/fromFragments.js rename packages/layout/src/text/{getAttributedString.js => getAttributedString.ts} (68%) rename packages/layout/src/text/{heightAtLineIndex.js => heightAtLineIndex.ts} (67%) rename packages/layout/src/text/{ignoreChars.js => ignoreChars.ts} (80%) rename packages/layout/src/text/{layoutText.js => layoutText.ts} (83%) rename packages/layout/src/text/{lineIndexAtHeight.js => lineIndexAtHeight.ts} (69%) rename packages/layout/src/text/{linesHeight.js => linesHeight.ts} (57%) rename packages/layout/src/text/{linesWidth.js => linesWidth.ts} (56%) delete mode 100644 packages/layout/src/text/measureText.js create mode 100644 packages/layout/src/text/measureText.ts rename packages/layout/src/text/{splitText.js => splitText.ts} (79%) rename packages/layout/src/text/{standardFont.js => standardFont.ts} (93%) rename packages/layout/src/text/{transformText.js => transformText.ts} (78%) delete mode 100644 packages/layout/src/types.ts create mode 100644 packages/layout/src/types/base.ts create mode 100644 packages/layout/src/types/canvas.ts create mode 100644 packages/layout/src/types/checkbox.ts create mode 100644 packages/layout/src/types/circle.ts create mode 100644 packages/layout/src/types/clip-path.ts create mode 100644 packages/layout/src/types/defs.ts create mode 100644 packages/layout/src/types/document.ts create mode 100644 packages/layout/src/types/ellipse.ts create mode 100644 packages/layout/src/types/field-set.ts create mode 100644 packages/layout/src/types/g.ts create mode 100644 packages/layout/src/types/image.ts create mode 100644 packages/layout/src/types/index.ts create mode 100644 packages/layout/src/types/line.ts create mode 100644 packages/layout/src/types/linear-gradient.ts create mode 100644 packages/layout/src/types/link.ts create mode 100644 packages/layout/src/types/node.ts create mode 100644 packages/layout/src/types/note.ts create mode 100644 packages/layout/src/types/page.ts create mode 100644 packages/layout/src/types/path.ts create mode 100644 packages/layout/src/types/polygon.ts create mode 100644 packages/layout/src/types/polyline.ts create mode 100644 packages/layout/src/types/radial-gradient.ts create mode 100644 packages/layout/src/types/rect.ts create mode 100644 packages/layout/src/types/select.ts create mode 100644 packages/layout/src/types/stop.ts create mode 100644 packages/layout/src/types/svg.ts create mode 100644 packages/layout/src/types/text-input.ts create mode 100644 packages/layout/src/types/text-instance.ts create mode 100644 packages/layout/src/types/text.ts create mode 100644 packages/layout/src/types/tspan.ts create mode 100644 packages/layout/src/types/view.ts rename packages/layout/src/yoga/{index.js => index.ts} (79%) delete mode 100644 packages/layout/tests/image/getSource.test.js create mode 100644 packages/layout/tests/image/getSource.test.ts rename packages/layout/tests/image/{resolveSource.test.js => resolveSource.test.ts} (100%) rename packages/layout/tests/node/{getBorderWidth.test.js => getBorderWidth.test.ts} (80%) rename packages/layout/tests/node/{getDimension.test.js => getDimension.test.ts} (71%) rename packages/layout/tests/node/{getMargin.test.js => getMargin.test.ts} (69%) rename packages/layout/tests/node/{getOrigin.test.js => getOrigin.test.ts} (58%) rename packages/layout/tests/node/{getPadding.test.js => getPadding.test.ts} (69%) rename packages/layout/tests/node/{getPosition.test.js => getPosition.test.ts} (79%) delete mode 100644 packages/layout/tests/node/removePaddings.test.js create mode 100644 packages/layout/tests/node/removePaddings.test.ts rename packages/layout/tests/node/{setAlignContent.test.js => setAlignContent.test.ts} (87%) rename packages/layout/tests/node/{setAlignItems.test.js => setAlignItems.test.ts} (86%) rename packages/layout/tests/node/{setAlignSelf.test.js => setAlignSelf.test.ts} (86%) rename packages/layout/tests/node/{setAspectRatio.test.js => setAspectRatio.test.ts} (62%) rename packages/layout/tests/node/{setBorderWidth.test.js => setBorderWidth.test.ts} (89%) rename packages/layout/tests/node/{setDimension.test.js => setDimension.test.ts} (93%) rename packages/layout/tests/node/{setDisplay.test.js => setDisplay.test.ts} (74%) rename packages/layout/tests/node/{setFlexBasis.test.js => setFlexBasis.test.ts} (61%) rename packages/layout/tests/node/{setFlexDirection.test.js => setFlexDirection.test.ts} (81%) rename packages/layout/tests/node/{setFlexGrow.test.js => setFlexGrow.test.ts} (68%) rename packages/layout/tests/node/{setFlexShrink.test.js => setFlexShrink.test.ts} (68%) rename packages/layout/tests/node/{setFlexWrap.test.js => setFlexWrap.test.ts} (78%) rename packages/layout/tests/node/{setJustifyContent.test.js => setJustifyContent.test.ts} (83%) rename packages/layout/tests/node/{setMargin.test.js => setMargin.test.ts} (95%) rename packages/layout/tests/node/{setOverflow.test.js => setOverflow.test.ts} (75%) rename packages/layout/tests/node/{setPadding.test.js => setPadding.test.ts} (94%) rename packages/layout/tests/node/{setPosition.test.js => setPosition.test.ts} (94%) rename packages/layout/tests/node/{setPositionType.test.js => setPositionType.test.ts} (75%) rename packages/layout/tests/node/{shouldBreak.test.js => shouldBreak.test.ts} (59%) create mode 100644 packages/layout/tests/steps/__snapshots__/resolveOrigins.test.ts.snap create mode 100644 packages/layout/tests/steps/__snapshots__/resolvePagePaddings.test.ts.snap create mode 100644 packages/layout/tests/steps/__snapshots__/resolvePercentHeight.test.ts.snap rename packages/layout/tests/steps/{resolveInhritance.test.js => resolveInhritance.test.ts} (74%) rename packages/layout/tests/steps/{resolveOrigins.test.js => resolveOrigins.test.ts} (52%) rename packages/layout/tests/steps/{resolvePagePaddings.test.js => resolvePagePaddings.test.ts} (56%) rename packages/layout/tests/steps/{resolvePageSizes.test.js => resolvePageSizes.test.ts} (83%) rename packages/layout/tests/steps/{resolvePagination.test.js => resolvePagination.test.ts} (76%) rename packages/layout/tests/steps/{resolvePercentHeight.test.js => resolvePercentHeight.test.ts} (60%) rename packages/layout/tests/steps/{resolveTextLayout.test.js => resolveTextLayout.test.ts} (73%) rename packages/layout/tests/text/{fontSubstitution.test.js => fontSubstitution.test.ts} (77%) delete mode 100644 packages/layout/tests/text/fromFragments.test.js rename packages/layout/tests/text/{heightAtLineIndex.test.js => heightAtLineIndex.test.ts} (62%) rename packages/layout/tests/text/{layoutText.test.js => layoutText.test.ts} (63%) rename packages/layout/tests/text/{lineIndexAtHeight.test.js => lineIndexAtHeight.test.ts} (61%) diff --git a/packages/fns/src/asyncCompose.ts b/packages/fns/src/asyncCompose.ts index feb09a97e..7072991e1 100644 --- a/packages/fns/src/asyncCompose.ts +++ b/packages/fns/src/asyncCompose.ts @@ -1,16 +1,16 @@ /* eslint-disable no-await-in-loop */ -type Fn = (arg: any, ...args: any[]) => Promise; +type Fn = (arg: any, ...args: any[]) => Promise | any; type FirstFnParameterType = T extends [ ...any, - (arg: infer A, ...args: any[]) => Promise, + (arg: infer A, ...args: any[]) => Promise | any, ] ? A : never; type LastFnReturnType = T extends [ - (arg: any, ...args: any[]) => Promise, + (arg: any, ...args: any[]) => Promise | infer R, ...any, ] ? R diff --git a/packages/fns/src/capitalize.ts b/packages/fns/src/capitalize.ts index fbfeb154e..82f904704 100644 --- a/packages/fns/src/capitalize.ts +++ b/packages/fns/src/capitalize.ts @@ -1,7 +1,7 @@ /** * Capitalize first letter of each word * - * @param value = Any string + * @param value - Any string * @returns Capitalized string */ const capitalize = (value?: string | null) => { diff --git a/packages/fns/src/evolve.ts b/packages/fns/src/evolve.ts index 2bb6044b4..f26a3b833 100644 --- a/packages/fns/src/evolve.ts +++ b/packages/fns/src/evolve.ts @@ -1,5 +1,3 @@ -type Transformation = Record any>; - /** * Applies a set of transformations to an object and returns a new object with the transformed values. * @@ -8,26 +6,25 @@ type Transformation = Record any>; * @param object - The object to transform. * @returns The transformed object. */ -const evolve = ( - transformations: Transformation, - object: Record, -): Record => { +function evolve>( + transformations: Partial<{ [K in keyof T]: (value: T[K]) => T[K] }>, + object: T, +): T { const result: Record = {}; const keys = Object.keys(object); for (let i = 0; i < keys.length; i += 1) { const key = keys[i]; const transformation = transformations[key]; - const type = typeof transformation; - if (type === 'function') { + if (typeof transformation === 'function') { result[key] = transformation(object[key]); } else { result[key] = object[key]; } } - return result; -}; + return result as T; +} export default evolve; diff --git a/packages/fns/src/index.ts b/packages/fns/src/index.ts index be385642b..1940ba071 100644 --- a/packages/fns/src/index.ts +++ b/packages/fns/src/index.ts @@ -16,3 +16,4 @@ export { default as repeat } from './repeat.js'; export { default as reverse } from './reverse.js'; export { default as upperFirst } from './upperFirst.js'; export { default as without } from './without.js'; +export { default as parseFloat } from './parseFloat.js'; diff --git a/packages/fns/src/isNil.ts b/packages/fns/src/isNil.ts index 73767261a..91b9dfcfe 100644 --- a/packages/fns/src/isNil.ts +++ b/packages/fns/src/isNil.ts @@ -5,7 +5,7 @@ * @param value - The value to check * @returns True if the value is null or undefined, false otherwise */ -const isNil = (value: T): boolean => +const isNil = (value: unknown): value is null | undefined => value === null || value === undefined; export default isNil; diff --git a/packages/fns/src/parseFloat.ts b/packages/fns/src/parseFloat.ts new file mode 100644 index 000000000..c5d2f8c6b --- /dev/null +++ b/packages/fns/src/parseFloat.ts @@ -0,0 +1,11 @@ +/** + * Parse a string or number to a float + * + * @param value - String or number + * @returns Parsed float + */ +const parseFloat = (value: string | number) => { + return typeof value === 'string' ? Number.parseFloat(value) : value; +}; + +export default parseFloat; diff --git a/packages/fns/tests/parseFloat.test.ts b/packages/fns/tests/parseFloat.test.ts new file mode 100644 index 000000000..4bb139d79 --- /dev/null +++ b/packages/fns/tests/parseFloat.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, test } from 'vitest'; + +import parseFloat from '../src/parseFloat'; + +describe('parseFloat', () => { + test('should return undefined for undefined', () => { + expect(parseFloat(undefined!)).toBe(undefined); + }); + + test('should return null for null', () => { + expect(parseFloat(null!)).toBe(null); + }); + + test('should parse integer', () => { + expect(parseFloat(10)).toBe(10); + }); + + test('should parse float', () => { + expect(parseFloat(10.1)).toBe(10.1); + }); + + test('should parse string integer', () => { + expect(parseFloat('10')).toBe(10); + }); + + test('should parse string float', () => { + expect(parseFloat('10.1')).toBe(10.1); + }); +}); diff --git a/packages/fns/tsconfig.json b/packages/fns/tsconfig.json index 4dcaa2d62..8898b8f55 100644 --- a/packages/fns/tsconfig.json +++ b/packages/fns/tsconfig.json @@ -10,7 +10,7 @@ "moduleResolution": "Node", "esModuleInterop": true, "allowSyntheticDefaultImports": true, - "strict": false, + "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "types": ["vitest/globals"], diff --git a/packages/font/src/index.ts b/packages/font/src/index.ts index 7470d61de..f534f5f85 100644 --- a/packages/font/src/index.ts +++ b/packages/font/src/index.ts @@ -9,7 +9,7 @@ import { } from './types'; class FontStore { - fonts = {}; + fonts: Record = {}; emojiSource: EmojiSource | null = null; @@ -25,25 +25,17 @@ class FontStore { // Bulk loading if ('fonts' in data) { for (let i = 0; i < data.fonts.length; i += 1) { - this.fonts[family].register({ family, ...data.fonts[i] }); + const { src, fontStyle, fontWeight, ...options } = data.fonts[i]; + this.fonts[family].register({ src, fontStyle, fontWeight, ...options }); } } else { - this.fonts[family].register(data); + const { src, fontStyle, fontWeight, ...options } = data; + this.fonts[family].register({ src, fontStyle, fontWeight, ...options }); } }; registerEmojiSource = (emojiSource: EmojiSource) => { - const url = 'url' in emojiSource ? emojiSource.url : undefined; - const format = 'format' in emojiSource ? emojiSource.format : undefined; - const builder = 'builder' in emojiSource ? emojiSource.builder : undefined; - const withVariationSelectors = emojiSource.withVariationSelectors || false; - - this.emojiSource = { - url, - format: format || 'png', - builder, - withVariationSelectors, - }; + this.emojiSource = emojiSource; }; registerHyphenationCallback = (callback: HyphenationCallback) => { @@ -89,7 +81,10 @@ class FontStore { for (let i = 0; i < keys.length; i += 1) { const key = keys[i]; - this.fonts[key].data = null; + for (let j = 0; j < this.fonts[key].sources.length; j++) { + const fontSource = this.fonts[key].sources[j]; + fontSource.data = null; + } } }; diff --git a/packages/image/src/index.ts b/packages/image/src/index.ts index e3e6edb36..73ac0050e 100644 --- a/packages/image/src/index.ts +++ b/packages/image/src/index.ts @@ -1,5 +1,5 @@ import resolveImage from './resolve'; -export type { ImageSrc } from './types'; +export type { Image, ImageSrc } from './types'; export default resolveImage; diff --git a/packages/image/src/jpeg.ts b/packages/image/src/jpeg.ts index 415e3bc17..177ed14e1 100644 --- a/packages/image/src/jpeg.ts +++ b/packages/image/src/jpeg.ts @@ -7,7 +7,7 @@ class JPEG implements Image { height: number; format: 'jpeg'; - constructor(data) { + constructor(data: Buffer) { this.data = data; this.format = 'jpeg'; diff --git a/packages/image/src/types.ts b/packages/image/src/types.ts index 63c09b2ac..c48d08fdf 100644 --- a/packages/image/src/types.ts +++ b/packages/image/src/types.ts @@ -3,6 +3,7 @@ export interface Image { height: number; data: Buffer; format: 'jpeg' | 'png'; + key?: string; } export type ImageFormat = 'jpg' | 'jpeg' | 'png'; diff --git a/packages/layout/babel.config.js b/packages/layout/babel.config.js deleted file mode 100644 index defa6f8c2..000000000 --- a/packages/layout/babel.config.js +++ /dev/null @@ -1,3 +0,0 @@ -export default { - extends: '../../babel.config.js', -}; diff --git a/packages/layout/globals.d.ts b/packages/layout/globals.d.ts new file mode 100644 index 000000000..e69fd4d1c --- /dev/null +++ b/packages/layout/globals.d.ts @@ -0,0 +1,245 @@ +declare module 'yoga-layout/load' { + export enum Align { + Auto = 0, + FlexStart = 1, + Center = 2, + FlexEnd = 3, + Stretch = 4, + Baseline = 5, + SpaceBetween = 6, + SpaceAround = 7, + SpaceEvenly = 8, + } + + export enum BoxSizing { + BorderBox = 0, + ContentBox = 1, + } + + export enum Dimension { + Width = 0, + Height = 1, + } + + export enum Direction { + Inherit = 0, + LTR = 1, + RTL = 2, + } + + export enum Display { + Flex = 0, + None = 1, + Contents = 2, + } + + export enum Edge { + Left = 0, + Top = 1, + Right = 2, + Bottom = 3, + Start = 4, + End = 5, + Horizontal = 6, + Vertical = 7, + All = 8, + } + + export enum Errata { + None = 0, + StretchFlexBasis = 1, + AbsolutePositionWithoutInsetsExcludesPadding = 2, + AbsolutePercentAgainstInnerSize = 4, + All = 2147483647, + Classic = 2147483646, + } + + export enum ExperimentalFeature { + WebFlexBasis = 0, + } + + export enum FlexDirection { + Column = 0, + ColumnReverse = 1, + Row = 2, + RowReverse = 3, + } + + export enum Gutter { + Column = 0, + Row = 1, + All = 2, + } + + export enum Justify { + FlexStart = 0, + Center = 1, + FlexEnd = 2, + SpaceBetween = 3, + SpaceAround = 4, + SpaceEvenly = 5, + } + + export enum LogLevel { + Error = 0, + Warn = 1, + Info = 2, + Debug = 3, + Verbose = 4, + Fatal = 5, + } + + export enum MeasureMode { + Undefined = 0, + Exactly = 1, + AtMost = 2, + } + + export enum NodeType { + Default = 0, + Text = 1, + } + + export enum Overflow { + Visible = 0, + Hidden = 1, + Scroll = 2, + } + + export enum PositionType { + Static = 0, + Relative = 1, + Absolute = 2, + } + + export enum Unit { + Undefined = 0, + Point = 1, + Percent = 2, + Auto = 3, + } + + export enum Wrap { + NoWrap = 0, + Wrap = 1, + WrapReverse = 2, + } + + export type MeasureFunction = ( + width: number, + widthMeasureMode: MeasureMode, + height: number, + heightMeasureMode: MeasureMode, + ) => { + width?: number | undefined; + height?: number | undefined; + } | null; + + export interface YogaNode { + calculateLayout( + width?: number, + height?: number, + direction?: Direction, + ): void; + copyStyle(node: YogaNode): void; + free(): void; + freeRecursive(): void; + getAlignContent(): Align; + getAlignItems(): Align; + getAlignSelf(): Align; + getAspectRatio(): number; + getBorder(edge: Edge): number; + getChild(index: number): YogaNode; + getChildCount(): number; + getComputedBorder(edge: Edge): number; + getComputedBottom(): number; + getComputedHeight(): number; + // getComputedLayout(): Layout; + getComputedLeft(): number; + getComputedMargin(edge: Edge): number; + getComputedPadding(edge: Edge): number; + getComputedRight(): number; + getComputedTop(): number; + getComputedWidth(): number; + getDisplay(): Display; + getFlexBasis(): number; + getFlexDirection(): FlexDirection; + getFlexGrow(): number; + getFlexShrink(): number; + getFlexWrap(): Wrap; + getHeight(): Value; + getJustifyContent(): Justify; + getOverflow(): Overflow; + getParent(): YogaNode | null; + getPositionType(): PositionType; + insertChild(child: YogaNode, index: number): void; + isDirty(): boolean; + markDirty(): void; + removeChild(child: YogaNode): void; + reset(): void; + setAlignContent(alignContent: Align): void; + setAlignItems(alignItems: Align): void; + setAlignSelf(alignSelf: Align): void; + setAspectRatio(aspectRatio: number): void; + setBorder(edge: Edge, borderWidth: number): void; + setDisplay(display: Display): void; + setFlex(flex: number): void; + setFlexBasis(flexBasis: number | string): void; + setFlexBasisPercent(flexBasis: number): void; + setFlexDirection(flexDirection: Direction): void; + setFlexGrow(flexGrow: number): void; + setFlexShrink(flexShrink: number): void; + setFlexWrap(flexWrap: Wrap): void; + setHeight(height: number | string): void; + setHeightAuto(): void; + setHeightPercent(height: number): void; + setJustifyContent(justifyContent: Justify): void; + setMargin(edge: Edge, margin: number | string): void; + setMarginAuto(edge: Edge): void; + setMarginPercent(edge: Edge, margin: number): void; + setMaxHeight(maxHeight: number | string): void; + setMaxHeightPercent(maxHeight: number): void; + setMaxWidth(maxWidth: number | string): void; + setMaxWidthPercent(maxWidth: number): void; + setGap(gap: Gutter, value: number): void; + setMeasureFunc(measureFunction: MeasureFunction): void; + setMinHeight(minHeight: number | string): void; + setMinHeightPercent(minHeight: number): void; + setMinWidth(minWidth: number | string): void; + setMinWidthPercent(minWidth: number): void; + setOverflow(overflow: Overflow): void; + setPadding(edge: Edge, padding: number | string): void; + setPaddingPercent(edge: Edge, padding: number): void; + setPosition(edge: Edge, position: number | string): void; + setPositionPercent(edge: Edge, position: number): void; + setPositionType(positionType: PositionType): void; + setWidth(width: number | string): void; + setWidthAuto(): void; + setWidthPercent(width: number): void; + unsetMeasureFunc(): void; + } + + interface YogaConfig { + setPointScaleFactor(factor: number): void; + } + + interface ConfigStatic { + create(): YogaConfig; + destroy(config: YogaConfig): any; + } + + interface NodeStatic { + create(): YogaNode; + createDefault(): YogaNode; + createWithConfig(config: YogaConfig): YogaNode; + destroy(node: YogaNode): any; + } + + export interface Yoga { + Node: NodeStatic; + Config: ConfigStatic; + getInstanceCount(): number; + } + + export const loadYoga: () => Promise; +} diff --git a/packages/layout/package.json b/packages/layout/package.json index 3d2524947..f231fac37 100644 --- a/packages/layout/package.json +++ b/packages/layout/package.json @@ -7,6 +7,7 @@ "homepage": "https://github.com/diegomura/react-pdf#readme", "type": "module", "main": "./lib/index.js", + "types": "./lib/index.d.ts", "repository": { "type": "git", "url": "https://github.com/diegomura/react-pdf.git", diff --git a/packages/layout/src/canvas/measureCanvas.js b/packages/layout/src/canvas/measureCanvas.ts similarity index 59% rename from packages/layout/src/canvas/measureCanvas.js rename to packages/layout/src/canvas/measureCanvas.ts index 669cfbeb6..57b192a04 100644 --- a/packages/layout/src/canvas/measureCanvas.js +++ b/packages/layout/src/canvas/measureCanvas.ts @@ -1,28 +1,33 @@ +import { MeasureFunction } from 'yoga-layout/load'; import getMargin from '../node/getMargin'; import getPadding from '../node/getPadding'; import isHeightAuto from '../page/isHeightAuto'; +import { SafePageNode } from '../types'; +import { SafeCanvasNode } from '../types/canvas'; const SAFETY_HEIGHT = 10; -const getMax = (values) => Math.max(-Infinity, ...values); +type Point = [number, number]; + +const getMax = (values: number[]) => Math.max(-Infinity, ...values); /** * Helper object to predict canvas size * TODO: Implement remaining functions (as close as possible); */ const measureCtx = () => { - const ctx = {}; - const points = []; + const ctx: any = {}; + const points: Point[] = []; const nil = () => ctx; - const addPoint = (x, y) => points.push([x, y]); + const addPoint = (x: number, y: number) => points.push([x, y]); - const moveTo = (...args) => { - addPoint(...args); + const moveTo = (x: number, y: number) => { + addPoint(x, y); return ctx; }; - const rect = (x, y, w, h) => { + const rect = (x: number, y: number, w: number, h: number) => { addPoint(x, y); addPoint(x + w, y); addPoint(x, y + h); @@ -30,7 +35,7 @@ const measureCtx = () => { return ctx; }; - const ellipse = (x, y, rx, ry) => { + const ellipse = (x: number, y: number, rx: number, ry: number) => { ry = ry || rx; addPoint(x - rx, y - ry); @@ -41,7 +46,7 @@ const measureCtx = () => { return ctx; }; - const polygon = (...pts) => { + const polygon = (...pts: Point[]) => { points.push(...pts); return ctx; }; @@ -104,26 +109,30 @@ const measureCtx = () => { * @param {Object} node * @returns {MeasureCanvas} measure canvas */ -const measureCanvas = (page, node) => () => { - const imageMargin = getMargin(node); - const pagePadding = getPadding(page); - const pageArea = isHeightAuto(page) - ? Infinity - : page.box.height - - pagePadding.paddingTop - - pagePadding.paddingBottom - - imageMargin.marginTop - - imageMargin.marginBottom - - SAFETY_HEIGHT; - - const ctx = measureCtx(); - - node.props.paint(ctx); - - const width = ctx.getWidth(); - const height = Math.min(pageArea, ctx.getHeight()); - - return { width, height }; -}; +const measureCanvas = + (page: SafePageNode, node: SafeCanvasNode): MeasureFunction => + () => { + const imageMargin = getMargin(node); + const pagePadding = getPadding(page); + + // TODO: Check image percentage margins + const pageArea = isHeightAuto(page) + ? Infinity + : (page.box?.height || 0) - + (pagePadding.paddingTop as number) - + (pagePadding.paddingBottom as number) - + (imageMargin.marginTop as number) - + (imageMargin.marginBottom as number) - + SAFETY_HEIGHT; + + const ctx = measureCtx(); + + node.props.paint(ctx); + + const width = ctx.getWidth(); + const height = Math.min(pageArea, ctx.getHeight()); + + return { width, height }; + }; export default measureCanvas; diff --git a/packages/layout/src/image/fetchImage.js b/packages/layout/src/image/fetchImage.ts similarity index 70% rename from packages/layout/src/image/fetchImage.js rename to packages/layout/src/image/fetchImage.ts index 3bc30c120..e65e959a3 100644 --- a/packages/layout/src/image/fetchImage.js +++ b/packages/layout/src/image/fetchImage.ts @@ -2,14 +2,15 @@ import resolveImage from '@react-pdf/image'; import getSource from './getSource'; import resolveSource from './resolveSource'; +import { SafeImageNode } from '../types'; /** * Fetches image and append data to node * Ideally this fn should be immutable. * - * @param {Object} node + * @param node */ -const fetchImage = async (node) => { +const fetchImage = async (node: SafeImageNode) => { const src = getSource(node); const { cache } = node.props; @@ -26,9 +27,11 @@ const fetchImage = async (node) => { } node.image = await resolveImage(source, { cache }); - node.image.key = source.data ? source.data.toString() : source.uri; - } catch (e) { - node.image = { width: 0, height: 0, key: null }; + + if (Buffer.isBuffer(source) || source instanceof Blob) return; + + node.image.key = 'data' in source ? source.data.toString() : source.uri; + } catch (e: any) { console.warn(e.message); } }; diff --git a/packages/layout/src/image/getRatio.js b/packages/layout/src/image/getRatio.js deleted file mode 100644 index f395f7602..000000000 --- a/packages/layout/src/image/getRatio.js +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Get image ratio - * - * @param {Object} node image node - * @returns {number} image ratio - */ -const getRatio = (node) => { - return node.image?.data ? node.image.width / node.image.height : 1; -}; - -export default getRatio; diff --git a/packages/layout/src/image/getRatio.ts b/packages/layout/src/image/getRatio.ts new file mode 100644 index 000000000..71ff6f5d5 --- /dev/null +++ b/packages/layout/src/image/getRatio.ts @@ -0,0 +1,13 @@ +import { SafeImageNode } from '../types'; + +/** + * Get image ratio + * + * @param node - Image node + * @returns Image ratio + */ +const getRatio = (node: SafeImageNode) => { + return node.image?.data ? node.image.width / node.image.height : 1; +}; + +export default getRatio; diff --git a/packages/layout/src/image/getSource.js b/packages/layout/src/image/getSource.js deleted file mode 100644 index 6e59f1f8d..000000000 --- a/packages/layout/src/image/getSource.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Get image source - * - * @param {Object} node image node - * @returns {string | Object} image src - */ -const getSource = (node) => - node.props?.src || node.props?.source || node.props?.href; - -export default getSource; diff --git a/packages/layout/src/image/getSource.ts b/packages/layout/src/image/getSource.ts new file mode 100644 index 000000000..0a7e47bcc --- /dev/null +++ b/packages/layout/src/image/getSource.ts @@ -0,0 +1,14 @@ +import { SafeImageNode } from '../types'; + +/** + * Get image source + * + * @param node - Image node + * @returns Image src + */ +const getSource = (node: SafeImageNode) => { + if ('src' in node.props) return node.props.src; + if ('source' in node.props) return node.props.source; +}; + +export default getSource; diff --git a/packages/layout/src/image/measureImage.js b/packages/layout/src/image/measureImage.js deleted file mode 100644 index 9d7dff004..000000000 --- a/packages/layout/src/image/measureImage.js +++ /dev/null @@ -1,86 +0,0 @@ -import * as Yoga from 'yoga-layout/load'; - -import getRatio from './getRatio'; -import getMargin from '../node/getMargin'; -import getPadding from '../node/getPadding'; -import isHeightAuto from '../page/isHeightAuto'; - -const SAFETY_HEIGHT = 10; - -/** - * @typedef {Function} MeasureImage - * @param {number} width - * @param {number} widthMode - * @param {number} height - * @param {number} heightMode - * @returns {{ width: number, height: number }} image width and height - */ - -/** - * Yoga image measure function - * - * @param {Object} page page - * @param {Object} node node - * @returns {MeasureImage} measure image - */ -const measureImage = (page, node) => (width, widthMode, height, heightMode) => { - const imageRatio = getRatio(node); - const imageMargin = getMargin(node); - const pagePadding = getPadding(page); - const pageArea = isHeightAuto(page) - ? Infinity - : page.box.height - - pagePadding.paddingTop - - pagePadding.paddingBottom - - imageMargin.marginTop - - imageMargin.marginBottom - - SAFETY_HEIGHT; - - // Skip measure if image data not present yet - if (!node.image) return { width: 0, height: 0 }; - - if ( - widthMode === Yoga.MeasureMode.Exactly && - heightMode === Yoga.MeasureMode.Undefined - ) { - const scaledHeight = width / imageRatio; - return { height: Math.min(pageArea, scaledHeight) }; - } - - if ( - heightMode === Yoga.MeasureMode.Exactly && - (widthMode === Yoga.MeasureMode.AtMost || - widthMode === Yoga.MeasureMode.Undefined) - ) { - return { width: Math.min(height * imageRatio, width) }; - } - - if ( - widthMode === Yoga.MeasureMode.Exactly && - heightMode === Yoga.MeasureMode.AtMost - ) { - const scaledHeight = width / imageRatio; - return { height: Math.min(height, pageArea, scaledHeight) }; - } - - if ( - widthMode === Yoga.MeasureMode.AtMost && - heightMode === Yoga.MeasureMode.AtMost - ) { - if (imageRatio > 1) { - return { - width, - height: Math.min(width / imageRatio, height), - }; - } - - return { - height, - width: Math.min(height * imageRatio, width), - }; - } - - return { height, width }; -}; - -export default measureImage; diff --git a/packages/layout/src/image/measureImage.ts b/packages/layout/src/image/measureImage.ts new file mode 100644 index 000000000..17e8d7c92 --- /dev/null +++ b/packages/layout/src/image/measureImage.ts @@ -0,0 +1,82 @@ +import * as Yoga from 'yoga-layout/load'; + +import getRatio from './getRatio'; +import getMargin from '../node/getMargin'; +import getPadding from '../node/getPadding'; +import isHeightAuto from '../page/isHeightAuto'; +import { SafeImageNode, SafePageNode } from '../types'; + +const SAFETY_HEIGHT = 10; + +/** + * Yoga image measure function + * + * @param page - Page + * @param node - Node + * @returns Measure image + */ +const measureImage = + (page: SafePageNode, node: SafeImageNode): Yoga.MeasureFunction => + (width, widthMode, height, heightMode) => { + const imageRatio = getRatio(node); + const imageMargin = getMargin(node); + const pagePadding = getPadding(page); + + // TODO: Check image percentage margins + const pageArea = isHeightAuto(page) + ? Infinity + : (page.box?.height || 0) - + (pagePadding.paddingTop as number) - + (pagePadding.paddingBottom as number) - + (imageMargin.marginTop as number) - + (imageMargin.marginBottom as number) - + SAFETY_HEIGHT; + + // Skip measure if image data not present yet + if (!node.image) return { width: 0, height: 0 }; + + if ( + widthMode === Yoga.MeasureMode.Exactly && + heightMode === Yoga.MeasureMode.Undefined + ) { + const scaledHeight = width / imageRatio; + return { height: Math.min(pageArea, scaledHeight) }; + } + + if ( + heightMode === Yoga.MeasureMode.Exactly && + (widthMode === Yoga.MeasureMode.AtMost || + widthMode === Yoga.MeasureMode.Undefined) + ) { + return { width: Math.min(height * imageRatio, width) }; + } + + if ( + widthMode === Yoga.MeasureMode.Exactly && + heightMode === Yoga.MeasureMode.AtMost + ) { + const scaledHeight = width / imageRatio; + return { height: Math.min(height, pageArea, scaledHeight) }; + } + + if ( + widthMode === Yoga.MeasureMode.AtMost && + heightMode === Yoga.MeasureMode.AtMost + ) { + if (imageRatio > 1) { + return { + width, + height: Math.min(width / imageRatio, height), + }; + } + + return { + height, + width: Math.min(height * imageRatio, width), + }; + } + + return { height, width }; + }; + +export default measureImage; diff --git a/packages/layout/src/image/resolveSource.js b/packages/layout/src/image/resolveSource.ts similarity index 67% rename from packages/layout/src/image/resolveSource.js rename to packages/layout/src/image/resolveSource.ts index 376da1152..e78ead27a 100644 --- a/packages/layout/src/image/resolveSource.js +++ b/packages/layout/src/image/resolveSource.ts @@ -1,12 +1,14 @@ +import { SourceObject } from '../types'; + /** * Resolves `src` to `@react-pdf/image` interface. * * Also it handles factories and async sources. * - * @param {string | Object | Function} src - * @returns {Promise} resolved src + * @param src + * @returns Resolved src */ -const resolveSource = async (src) => { +const resolveSource = async (src: SourceObject) => { const source = typeof src === 'function' ? await src() : await src; return typeof source === 'string' ? { uri: source } : source; }; diff --git a/packages/layout/src/index.ts b/packages/layout/src/index.ts index f4ea01250..7247df2fb 100644 --- a/packages/layout/src/index.ts +++ b/packages/layout/src/index.ts @@ -37,4 +37,6 @@ const layout = asyncCompose( resolveYoga, ); +export * from './types'; + export default layout; diff --git a/packages/layout/src/node/createInstances.js b/packages/layout/src/node/createInstances.ts similarity index 52% rename from packages/layout/src/node/createInstances.js rename to packages/layout/src/node/createInstances.ts index 192608476..152ea5678 100644 --- a/packages/layout/src/node/createInstances.js +++ b/packages/layout/src/node/createInstances.ts @@ -1,11 +1,16 @@ import { castArray } from '@react-pdf/fns'; -import { TextInstance } from '@react-pdf/primitives'; +import * as P from '@react-pdf/primitives'; +import React from 'react'; -const isString = (value) => typeof value === 'string'; +import { Node } from '../types'; -const isNumber = (value) => typeof value === 'number'; +const isString = (value: any): value is string => typeof value === 'string'; -const isFragment = (value) => +const isNumber = (value: any): value is number => typeof value === 'number'; + +const isBoolean = (value: any): value is boolean => typeof value === 'boolean'; + +const isFragment = (value: any): value is React.ReactFragment => value && value.type === Symbol.for('react.fragment'); /** @@ -13,31 +18,37 @@ const isFragment = (value) => * * Can return multiple instances in the case of arrays or fragments. * - * @param {Object} element React element - * @returns {Object[]} parsed React elements + * @param element - React element + * @returns Parsed React elements */ -const createInstances = (element) => { +const createInstances = (element: React.ReactNode): Node[] => { if (!element) return []; + if (Array.isArray(element)) { + return element.reduce((acc, el) => acc.concat(createInstances(el)), []); + } + + if (isBoolean(element)) { + return []; + } + if (isString(element) || isNumber(element)) { - return [{ type: TextInstance, value: `${element}` }]; + return [{ type: P.TextInstance, value: `${element}` }]; } if (isFragment(element)) { + // @ts-expect-error figure out why this is complains return createInstances(element.props.children); } - if (Array.isArray(element)) { - return element.reduce((acc, el) => acc.concat(createInstances(el)), []); - } - if (!isString(element.type)) { + // @ts-expect-error figure out why this is complains return createInstances(element.type(element.props)); } const { type, - props: { style = {}, children = [], ...props }, + props: { style = {}, children, ...props }, } = element; const nextChildren = castArray(children).reduce( @@ -50,10 +61,9 @@ const createInstances = (element) => { type, style, props, - box: {}, children: nextChildren, }, - ]; + ] as Node[]; }; export default createInstances; diff --git a/packages/layout/src/node/getBorderWidth.js b/packages/layout/src/node/getBorderWidth.ts similarity index 61% rename from packages/layout/src/node/getBorderWidth.js rename to packages/layout/src/node/getBorderWidth.ts index b261bfe4e..f45836e7a 100644 --- a/packages/layout/src/node/getBorderWidth.js +++ b/packages/layout/src/node/getBorderWidth.ts @@ -1,15 +1,19 @@ import * as Yoga from 'yoga-layout/load'; -const getComputedBorder = (yogaNode, edge) => - yogaNode ? yogaNode.getComputedBorder(edge) : 0; +import { SafeNode } from '../types'; + +const getComputedBorder = ( + yogaNode: Yoga.YogaNode | undefined, + edge: Yoga.Edge, +) => (yogaNode ? yogaNode.getComputedBorder(edge) : 0); /** * Get Yoga computed border width. Zero otherwise * - * @param {Object} node - * @returns {{ borderTopWidth: number, borderRightWidth: number, borderBottomWidth: number, borderLeftWidth: number }} border widths + * @param node + * @returns Border widths */ -const getBorderWidth = (node) => { +const getBorderWidth = (node: SafeNode) => { const { yogaNode } = node; return { diff --git a/packages/layout/src/node/getDimension.js b/packages/layout/src/node/getDimension.ts similarity index 72% rename from packages/layout/src/node/getDimension.js rename to packages/layout/src/node/getDimension.ts index d1228abd3..7c6885521 100644 --- a/packages/layout/src/node/getDimension.js +++ b/packages/layout/src/node/getDimension.ts @@ -1,3 +1,5 @@ +import { SafeNode } from '../types'; + const DEFAULT_DIMENSION = { width: 0, height: 0, @@ -6,10 +8,10 @@ const DEFAULT_DIMENSION = { /** * Get Yoga computed dimensions. Zero otherwise * - * @param {Object} node - * @returns {{ width: number, height: number }} dimensions + * @param node + * @returns Dimensions */ -const getDimension = (node) => { +const getDimension = (node: SafeNode) => { const { yogaNode } = node; if (!yogaNode) return DEFAULT_DIMENSION; diff --git a/packages/layout/src/node/getMargin.js b/packages/layout/src/node/getMargin.ts similarity index 66% rename from packages/layout/src/node/getMargin.js rename to packages/layout/src/node/getMargin.ts index d55109e8e..7e615df45 100644 --- a/packages/layout/src/node/getMargin.js +++ b/packages/layout/src/node/getMargin.ts @@ -1,6 +1,8 @@ import * as Yoga from 'yoga-layout/load'; -const getComputedMargin = (node, edge) => { +import { SafeNode } from '../types'; + +const getComputedMargin = (node: SafeNode, edge: Yoga.Edge) => { const { yogaNode } = node; return yogaNode ? yogaNode.getComputedMargin(edge) : null; }; @@ -8,42 +10,34 @@ const getComputedMargin = (node, edge) => { /** * Get Yoga computed magins. Zero otherwise * - * @param {Object} node - * @returns {{ marginTop: number, marginRight: number, marginBottom: number, marginLeft: number }} margins + * @param node + * @returns Margins */ -const getMargin = (node) => { +const getMargin = (node: SafeNode) => { const { style, box } = node; const marginTop = getComputedMargin(node, Yoga.Edge.Top) || box?.marginTop || style?.marginTop || - style?.marginVertical || - style?.margin || 0; const marginRight = getComputedMargin(node, Yoga.Edge.Right) || box?.marginRight || style?.marginRight || - style?.marginHorizontal || - style?.margin || 0; const marginBottom = getComputedMargin(node, Yoga.Edge.Bottom) || box?.marginBottom || style?.marginBottom || - style?.marginVertical || - style?.margin || 0; const marginLeft = getComputedMargin(node, Yoga.Edge.Left) || box?.marginLeft || style?.marginLeft || - style?.marginHorizontal || - style?.margin || 0; return { marginTop, marginRight, marginBottom, marginLeft }; diff --git a/packages/layout/src/node/getOrigin.js b/packages/layout/src/node/getOrigin.ts similarity index 56% rename from packages/layout/src/node/getOrigin.js rename to packages/layout/src/node/getOrigin.ts index 8a2ba3989..3f6f6621e 100644 --- a/packages/layout/src/node/getOrigin.js +++ b/packages/layout/src/node/getOrigin.ts @@ -1,16 +1,18 @@ import { isNil, matchPercent } from '@react-pdf/fns'; +import { Origin, SafeNode } from '../types'; -const getTransformStyle = (s) => (node) => - isNil(node.style?.[s]) ? '50%' : node.style?.[s]; +const getTransformStyle = + (s: 'transformOriginX' | 'transformOriginY') => (node: SafeNode) => + isNil(node.style?.[s]) ? '50%' : node.style?.[s] ?? null; /** * Get node origin * - * @param {Object} node + * @param node * @returns {{ left?: number, top?: number }} node origin */ -const getOrigin = (node) => { - if (!node.box) return {}; +const getOrigin = (node: SafeNode): Origin | null => { + if (!node.box) return null; const { left, top, width, height } = node.box; const transformOriginX = getTransformStyle('transformOriginX')(node); @@ -22,6 +24,12 @@ const getOrigin = (node) => { const offsetX = percentX ? width * percentX.percent : transformOriginX; const offsetY = percentY ? height * percentY.percent : transformOriginY; + if (isNil(offsetX) || typeof offsetX === 'string') + throw new Error(`Invalid origin offsetX: ${offsetX}`); + + if (isNil(offsetY) || typeof offsetY === 'string') + throw new Error(`Invalid origin offsetY: ${offsetY}`); + return { left: left + offsetX, top: top + offsetY }; }; diff --git a/packages/layout/src/node/getPadding.ts b/packages/layout/src/node/getPadding.ts index 5ddb544ae..221954375 100644 --- a/packages/layout/src/node/getPadding.ts +++ b/packages/layout/src/node/getPadding.ts @@ -1,8 +1,8 @@ import * as Yoga from 'yoga-layout/load'; -import { Node } from '../types'; +import { SafeNode } from '../types'; -const getComputedPadding = (node: Node, edge) => { +const getComputedPadding = (node: SafeNode, edge: Yoga.Edge) => { const { yogaNode } = node; return yogaNode ? yogaNode.getComputedPadding(edge) : null; }; @@ -13,39 +13,31 @@ const getComputedPadding = (node: Node, edge) => { * @param node * @returns paddings */ -const getPadding = (node: Node) => { +const getPadding = (node: SafeNode) => { const { style, box } = node; const paddingTop = getComputedPadding(node, Yoga.Edge.Top) || box?.paddingTop || style?.paddingTop || - style?.paddingVertical || - style?.padding || 0; const paddingRight = getComputedPadding(node, Yoga.Edge.Right) || box?.paddingRight || style?.paddingRight || - style?.paddingHorizontal || - style?.padding || 0; const paddingBottom = getComputedPadding(node, Yoga.Edge.Bottom) || box?.paddingBottom || style?.paddingBottom || - style?.paddingVertical || - style?.padding || 0; const paddingLeft = getComputedPadding(node, Yoga.Edge.Left) || box?.paddingLeft || style?.paddingLeft || - style?.paddingHorizontal || - style?.padding || 0; return { paddingTop, paddingRight, paddingBottom, paddingLeft }; diff --git a/packages/layout/src/node/getPosition.js b/packages/layout/src/node/getPosition.ts similarity index 69% rename from packages/layout/src/node/getPosition.js rename to packages/layout/src/node/getPosition.ts index c5cc0f7e5..986df5db3 100644 --- a/packages/layout/src/node/getPosition.js +++ b/packages/layout/src/node/getPosition.ts @@ -1,10 +1,12 @@ +import { SafeNode } from '../types'; + /** * Get Yoga computed position. Zero otherwise * - * @param {Object} node - * @returns {{ top: number, right: number, bottom: number, left: number }} position + * @param node + * @returns Position */ -const getPosition = (node) => { +const getPosition = (node: SafeNode) => { const { yogaNode } = node; return { diff --git a/packages/layout/src/node/getWrap.js b/packages/layout/src/node/getWrap.ts similarity index 53% rename from packages/layout/src/node/getWrap.js rename to packages/layout/src/node/getWrap.ts index fe19cd8fc..cc5839420 100644 --- a/packages/layout/src/node/getWrap.js +++ b/packages/layout/src/node/getWrap.ts @@ -1,12 +1,14 @@ import * as P from '@react-pdf/primitives'; -import { isNil } from '@react-pdf/fns'; +import { SafeNode } from '../types'; const NON_WRAP_TYPES = [P.Svg, P.Note, P.Image, P.Canvas]; -const getWrap = (node) => { +const getWrap = (node: SafeNode) => { if (NON_WRAP_TYPES.includes(node.type)) return false; - return isNil(node.props?.wrap) ? true : node.props.wrap; + if (!node.props) return true; + + return 'wrap' in node.props ? node.props.wrap : true; }; export default getWrap; diff --git a/packages/layout/src/node/isFixed.js b/packages/layout/src/node/isFixed.js deleted file mode 100644 index 98260e469..000000000 --- a/packages/layout/src/node/isFixed.js +++ /dev/null @@ -1,3 +0,0 @@ -const isFixed = (node) => node.props?.fixed === true; - -export default isFixed; diff --git a/packages/layout/src/node/isFixed.ts b/packages/layout/src/node/isFixed.ts new file mode 100644 index 000000000..2b059910e --- /dev/null +++ b/packages/layout/src/node/isFixed.ts @@ -0,0 +1,9 @@ +import { SafeNode } from '../types'; + +const isFixed = (node: SafeNode) => { + if (!node.props) return false; + + return 'fixed' in node.props ? node.props.fixed === true : false; +}; + +export default isFixed; diff --git a/packages/layout/src/node/removePaddings.js b/packages/layout/src/node/removePaddings.ts similarity index 67% rename from packages/layout/src/node/removePaddings.js rename to packages/layout/src/node/removePaddings.ts index 3ae997147..19b971e29 100644 --- a/packages/layout/src/node/removePaddings.js +++ b/packages/layout/src/node/removePaddings.ts @@ -1,6 +1,7 @@ import { omit } from '@react-pdf/fns'; import setPadding from './setPadding'; +import { SafeNode } from '../types'; const PADDING_PROPS = [ 'padding', @@ -15,12 +16,12 @@ const PADDING_PROPS = [ /** * Removes padding on node * - * @param {Object} node - * @returns {Object} node without padding + * @param node + * @returns Node without padding */ -const removePaddings = (node) => { +const removePaddings = (node: SafeNode) => { const style = omit(PADDING_PROPS, node.style || {}); - const newNode = Object.assign({}, node, { style }); + const newNode: SafeNode = Object.assign({}, node, { style }); setPadding(0)(newNode); diff --git a/packages/layout/src/node/setAlign.js b/packages/layout/src/node/setAlign.ts similarity index 65% rename from packages/layout/src/node/setAlign.js rename to packages/layout/src/node/setAlign.ts index 4b5a5639f..03b768659 100644 --- a/packages/layout/src/node/setAlign.js +++ b/packages/layout/src/node/setAlign.ts @@ -1,5 +1,6 @@ import * as Yoga from 'yoga-layout/load'; import { upperFirst } from '@react-pdf/fns'; +import { SafeNode } from '../types'; const ALIGN = { 'flex-start': Yoga.Align.FlexStart, @@ -12,25 +13,15 @@ const ALIGN = { 'space-evenly': Yoga.Align.SpaceEvenly, }; -/** - * @typedef {Function} NodeInstanceWrapper - * @param {Object} node node instance - * @returns {Object} node instance - */ - -/** - * @typedef {Function} AlignSetter - * @param {string} value align value - * @returns {NodeInstanceWrapper} node instance wrapper - */ - /** * Set generic align attribute to node's Yoga instance * - * @param {string} attr specific align property - * @returns {AlignSetter} align setter + * @param attr - Specific align property + * @param value - Specific align value + * @param node - Node + * @returns Node */ -const setAlign = (attr) => (value) => (node) => { +const setAlign = (attr: string) => (value: any) => (node: SafeNode) => { const { yogaNode } = node; const defaultValue = attr === 'items' ? Yoga.Align.Stretch : Yoga.Align.Auto; diff --git a/packages/layout/src/node/setAlignContent.js b/packages/layout/src/node/setAlignContent.ts similarity index 64% rename from packages/layout/src/node/setAlignContent.js rename to packages/layout/src/node/setAlignContent.ts index 1bba23e09..b7c24d977 100644 --- a/packages/layout/src/node/setAlignContent.js +++ b/packages/layout/src/node/setAlignContent.ts @@ -3,9 +3,9 @@ import setAlign from './setAlign'; /** * Set align content attribute to node's Yoga instance * - * @param {string} align value - * @param {Object} node instance - * @returns {Object} node instance + * @param align - Value + * @param node - Instance + * @returns Node instance */ const setAlignContent = setAlign('content'); diff --git a/packages/layout/src/node/setAlignItems.js b/packages/layout/src/node/setAlignItems.ts similarity index 63% rename from packages/layout/src/node/setAlignItems.js rename to packages/layout/src/node/setAlignItems.ts index 239409524..cb4da9dba 100644 --- a/packages/layout/src/node/setAlignItems.js +++ b/packages/layout/src/node/setAlignItems.ts @@ -3,9 +3,9 @@ import setAlign from './setAlign'; /** * Set align items attribute to node's Yoga instance * - * @param {string} align value - * @param {Object} node instance - * @returns {Object} node instance + * @param align - Value + * @param node - Node instance + * @returns Node instance */ const setAlignItems = setAlign('items'); diff --git a/packages/layout/src/node/setAlignSelf.js b/packages/layout/src/node/setAlignSelf.ts similarity index 62% rename from packages/layout/src/node/setAlignSelf.js rename to packages/layout/src/node/setAlignSelf.ts index 7aba87f63..938a89060 100644 --- a/packages/layout/src/node/setAlignSelf.js +++ b/packages/layout/src/node/setAlignSelf.ts @@ -3,9 +3,9 @@ import setAlign from './setAlign'; /** * Set align self attribute to node's Yoga instance * - * @param {string} align value - * @param {Object} node instance - * @returns {Object} node instance + * @param align - Value + * @param node - Node instance + * @returns Node instance */ const setAlignSelf = setAlign('self'); diff --git a/packages/layout/src/node/setAspectRatio.js b/packages/layout/src/node/setAspectRatio.ts similarity index 50% rename from packages/layout/src/node/setAspectRatio.js rename to packages/layout/src/node/setAspectRatio.ts index 36b4d361e..d81abee58 100644 --- a/packages/layout/src/node/setAspectRatio.js +++ b/packages/layout/src/node/setAspectRatio.ts @@ -1,18 +1,13 @@ import { isNil } from '@react-pdf/fns'; - -/** - * @typedef {Function} NodeInstanceWrapper - * @param {Object} node node instance - * @returns {Object} node instance - */ +import { SafeNode } from '../types'; /** * Set aspect ratio attribute to node's Yoga instance * - * @param {number} value ratio - * @returns {NodeInstanceWrapper} node instance wrapper + * @param value - Ratio + * @returns Node instance */ -const setAspectRatio = (value) => (node) => { +const setAspectRatio = (value?: number | null) => (node: SafeNode) => { const { yogaNode } = node; if (!isNil(value) && yogaNode) { diff --git a/packages/layout/src/node/setBorderWidth.js b/packages/layout/src/node/setBorderWidth.ts similarity index 52% rename from packages/layout/src/node/setBorderWidth.js rename to packages/layout/src/node/setBorderWidth.ts index 78d806ae9..603a6a9e7 100644 --- a/packages/layout/src/node/setBorderWidth.js +++ b/packages/layout/src/node/setBorderWidth.ts @@ -1,56 +1,51 @@ import * as Yoga from 'yoga-layout/load'; import setYogaValue from './setYogaValue'; - -/** - * @typedef {Function} NodeInstanceWrapper - * @param {Object} node node instance - * @returns {Object} node instance - */ +import { SafeNode } from '../types'; /** * Set border top attribute to node's Yoga instance * - * @param {number} border border top width - * @param {Object} node node instance - * @returns {Object} node instance + * @param border - Border top width + * @param node - Node instance + * @returns Node instance */ export const setBorderTop = setYogaValue('border', Yoga.Edge.Top); /** * Set border right attribute to node's Yoga instance * - * @param {number} border border right width - * @param {Object} node node instance - * @returns {Object} node instance + * @param border - Border right width + * @param node - Node instance + * @returns Node instance */ export const setBorderRight = setYogaValue('border', Yoga.Edge.Right); /** * Set border bottom attribute to node's Yoga instance * - * @param {number} border border bottom width - * @param {Object} node node instance - * @returns {Object} node instance + * @param border - Border bottom width + * @param node - Node instance + * @returns Node instance */ export const setBorderBottom = setYogaValue('border', Yoga.Edge.Bottom); /** * Set border left attribute to node's Yoga instance * - * @param {number} border border left width - * @param {Object} node node instance - * @returns {Object} node instance + * @param border - Border left width + * @param node - Node instance + * @returns Node instance */ export const setBorderLeft = setYogaValue('border', Yoga.Edge.Left); /** * Set all border widths at once * - * @param {number | string} width border width - * @returns {NodeInstanceWrapper} node instance wrapper + * @param width - Border width + * @returns Node instance wrapper */ -export const setBorder = (width) => (node) => { +export const setBorder = (width?: number | null) => (node: SafeNode) => { setBorderTop(width)(node); setBorderRight(width)(node); setBorderBottom(width)(node); diff --git a/packages/layout/src/node/setDimension.js b/packages/layout/src/node/setDimension.ts similarity index 53% rename from packages/layout/src/node/setDimension.js rename to packages/layout/src/node/setDimension.ts index bb8c8b207..9c7f9bd1a 100644 --- a/packages/layout/src/node/setDimension.js +++ b/packages/layout/src/node/setDimension.ts @@ -3,53 +3,53 @@ import setYogaValue from './setYogaValue'; /** * Set width to node's Yoga instance * - * @param {number} width - * @param {Object} node instance - * @returns {Object} node instance + * @param width - Width + * @param node - Node instance + * @returns Node instance */ export const setWidth = setYogaValue('width'); /** * Set min width to node's Yoga instance * - * @param {number} min width - * @param {Object} node instance - * @returns {Object} node instance + * @param min - Width + * @param node - Node instance + * @returns Node instance */ export const setMinWidth = setYogaValue('minWidth'); /** * Set max width to node's Yoga instance * - * @param {number} max width - * @param {Object} node instance - * @returns {Object} node instance + * @param max - Width + * @param node - Node instance + * @returns Node instance */ export const setMaxWidth = setYogaValue('maxWidth'); /** * Set height to node's Yoga instance * - * @param {number} height - * @param {Object} node instance - * @returns {Object} node instance + * @param height - Height + * @param node - Node instance + * @returns Node instance */ export const setHeight = setYogaValue('height'); /** * Set min height to node's Yoga instance * - * @param {number} min height - * @param {Object} node instance - * @returns {Object} node instance + * @param min - Height + * @param node - Node instance + * @returns Node instance */ export const setMinHeight = setYogaValue('minHeight'); /** * Set max height to node's Yoga instance * - * @param {number} max height - * @param {Object} node instance - * @returns {Object} node instance + * @param max - Height + * @param node - Node instance + * @returns Node instance */ export const setMaxHeight = setYogaValue('maxHeight'); diff --git a/packages/layout/src/node/setDisplay.js b/packages/layout/src/node/setDisplay.ts similarity index 53% rename from packages/layout/src/node/setDisplay.js rename to packages/layout/src/node/setDisplay.ts index 0d4b7c824..08f873007 100644 --- a/packages/layout/src/node/setDisplay.js +++ b/packages/layout/src/node/setDisplay.ts @@ -1,18 +1,13 @@ import * as Yoga from 'yoga-layout/load'; - -/** - * @typedef {Function} NodeInstanceWrapper - * @param {Object} node node instance - * @returns {Object} node instance - */ +import { SafeNode } from '../types'; /** * Set display attribute to node's Yoga instance * - * @param {string} value display - * @returns {NodeInstanceWrapper} node instance wrapper + * @param value - Display + * @returns Node instance wrapper */ -const setDisplay = (value) => (node) => { +const setDisplay = (value?: string | null) => (node: SafeNode) => { const { yogaNode } = node; if (yogaNode) { diff --git a/packages/layout/src/node/setFlexBasis.js b/packages/layout/src/node/setFlexBasis.ts similarity index 64% rename from packages/layout/src/node/setFlexBasis.js rename to packages/layout/src/node/setFlexBasis.ts index e26958ce7..47d657f38 100644 --- a/packages/layout/src/node/setFlexBasis.js +++ b/packages/layout/src/node/setFlexBasis.ts @@ -3,9 +3,9 @@ import setYogaValue from './setYogaValue'; /** * Set flex basis attribute to node's Yoga instance * - * @param {number} flex basis value - * @param {Object} node instance - * @returns {Object} node instance + * @param flex - Basis value + * @param node - Node instance + * @returns Node instance */ const setFlexBasis = setYogaValue('flexBasis'); diff --git a/packages/layout/src/node/setFlexDirection.js b/packages/layout/src/node/setFlexDirection.ts similarity index 64% rename from packages/layout/src/node/setFlexDirection.js rename to packages/layout/src/node/setFlexDirection.ts index ea5295533..f572d0dc2 100644 --- a/packages/layout/src/node/setFlexDirection.js +++ b/packages/layout/src/node/setFlexDirection.ts @@ -1,24 +1,20 @@ import * as Yoga from 'yoga-layout/load'; +import { SafeNode } from '../types'; + const FLEX_DIRECTIONS = { row: Yoga.FlexDirection.Row, 'row-reverse': Yoga.FlexDirection.RowReverse, 'column-reverse': Yoga.FlexDirection.ColumnReverse, }; -/** - * @typedef {Function} NodeInstanceWrapper - * @param {Object} node node instance - * @returns {Object} node instance - */ - /** * Set flex direction attribute to node's Yoga instance * - * @param {string} value flex direction value - * @returns {NodeInstanceWrapper} node instance wrapper + * @param value - Flex direction value + * @returns Node instance wrapper */ -const setFlexDirection = (value) => (node) => { +const setFlexDirection = (value?: string | null) => (node: SafeNode) => { const { yogaNode } = node; if (yogaNode) { diff --git a/packages/layout/src/node/setFlexGrow.js b/packages/layout/src/node/setFlexGrow.js deleted file mode 100644 index 67f080939..000000000 --- a/packages/layout/src/node/setFlexGrow.js +++ /dev/null @@ -1,19 +0,0 @@ -import setYogaValue from './setYogaValue'; - -/** - * @typedef {Function} NodeInstanceWrapper - * @param {Object} node node instance - * @returns {Object} node instance - */ - -/** - * Set flex grow attribute to node's Yoga instance - * - * @param {number} value flex grow value - * @returns {NodeInstanceWrapper} node instance wrapper - */ -const setFlexGrow = (value) => (node) => { - return setYogaValue('flexGrow')(value || 0)(node); -}; - -export default setFlexGrow; diff --git a/packages/layout/src/node/setFlexGrow.ts b/packages/layout/src/node/setFlexGrow.ts new file mode 100644 index 000000000..9ee51ebc6 --- /dev/null +++ b/packages/layout/src/node/setFlexGrow.ts @@ -0,0 +1,14 @@ +import { SafeNode } from '../types'; +import setYogaValue from './setYogaValue'; + +/** + * Set flex grow attribute to node's Yoga instance + * + * @param value - Flex grow value + * @returns Node instance wrapper + */ +const setFlexGrow = (value?: number | null) => (node: SafeNode) => { + return setYogaValue('flexGrow')(value || 0)(node); +}; + +export default setFlexGrow; diff --git a/packages/layout/src/node/setFlexShrink.js b/packages/layout/src/node/setFlexShrink.js deleted file mode 100644 index 01d1a9afc..000000000 --- a/packages/layout/src/node/setFlexShrink.js +++ /dev/null @@ -1,19 +0,0 @@ -import setYogaValue from './setYogaValue'; - -/** - * @typedef {Function} NodeInstanceWrapper - * @param {Object} node node instance - * @returns {Object} node instance - */ - -/** - * Set flex shrink attribute to node's Yoga instance - * - * @param {number} value flex shrink value - * @returns {NodeInstanceWrapper} node instance wrapper - */ -const setFlexShrink = (value) => (node) => { - return setYogaValue('flexShrink')(value || 1)(node); -}; - -export default setFlexShrink; diff --git a/packages/layout/src/node/setFlexShrink.ts b/packages/layout/src/node/setFlexShrink.ts new file mode 100644 index 000000000..f9bdff0b1 --- /dev/null +++ b/packages/layout/src/node/setFlexShrink.ts @@ -0,0 +1,14 @@ +import { SafeNode } from '../types'; +import setYogaValue from './setYogaValue'; + +/** + * Set flex shrink attribute to node's Yoga instance + * + * @param value - Flex shrink value + * @returns Node instance wrapper + */ +const setFlexShrink = (value?: string | number | null) => (node: SafeNode) => { + return setYogaValue('flexShrink')(value || 1)(node); +}; + +export default setFlexShrink; diff --git a/packages/layout/src/node/setFlexWrap.js b/packages/layout/src/node/setFlexWrap.ts similarity index 50% rename from packages/layout/src/node/setFlexWrap.js rename to packages/layout/src/node/setFlexWrap.ts index 9c5e12d2b..23380652f 100644 --- a/packages/layout/src/node/setFlexWrap.js +++ b/packages/layout/src/node/setFlexWrap.ts @@ -1,27 +1,22 @@ import * as Yoga from 'yoga-layout/load'; +import { SafeNode } from '../types'; const FLEX_WRAP = { wrap: Yoga.Wrap.Wrap, 'wrap-reverse': Yoga.Wrap.WrapReverse, }; -/** - * @typedef {Function} NodeInstanceWrapper - * @param {Object} node node instance - * @returns {Object} node instance - */ - /** * Set flex wrap attribute to node's Yoga instance * - * @param {string} value flex wrap value - * @returns {NodeInstanceWrapper} node instance wrapper + * @param value - Flex wrap value + * @returns Node instance wrapper */ -const setFlexWrap = (value) => (node) => { +const setFlexWrap = (value?: string | null) => (node: SafeNode) => { const { yogaNode } = node; if (yogaNode) { - const flexWrap = FLEX_WRAP[value] || Yoga.Wrap.NoWrap; + const flexWrap: Yoga.Wrap = FLEX_WRAP[value] || Yoga.Wrap.NoWrap; yogaNode.setFlexWrap(flexWrap); } diff --git a/packages/layout/src/node/setGap.js b/packages/layout/src/node/setGap.ts similarity index 53% rename from packages/layout/src/node/setGap.js rename to packages/layout/src/node/setGap.ts index ddb5396b1..5028a54ed 100644 --- a/packages/layout/src/node/setGap.js +++ b/packages/layout/src/node/setGap.ts @@ -1,19 +1,15 @@ import * as Yoga from 'yoga-layout/load'; import { isNil } from '@react-pdf/fns'; -/** - * @typedef {Function} NodeInstanceWrapper - * @param {Object} node node instance - * @returns {Object} node instance - */ +import { SafeNode } from '../types'; /** * Set rowGap value to node's Yoga instance * - * @param {number} value gap value - * @returns {NodeInstanceWrapper} node instance wrapper + * @param value - Gap value + * @returns Node instance wrapper */ -export const setRowGap = (value) => (node) => { +export const setRowGap = (value: number) => (node: SafeNode) => { const { yogaNode } = node; if (!isNil(value) && yogaNode) { @@ -26,10 +22,10 @@ export const setRowGap = (value) => (node) => { /** * Set columnGap value to node's Yoga instance * - * @param {number} value gap value - * @returns {NodeInstanceWrapper} node instance wrapper + * @param value - Gap value + * @returns Node instance wrapper */ -export const setColumnGap = (value) => (node) => { +export const setColumnGap = (value: number) => (node: SafeNode) => { const { yogaNode } = node; if (!isNil(value) && yogaNode) { diff --git a/packages/layout/src/node/setJustifyContent.js b/packages/layout/src/node/setJustifyContent.ts similarity index 69% rename from packages/layout/src/node/setJustifyContent.js rename to packages/layout/src/node/setJustifyContent.ts index b09524f7b..0aba1b903 100644 --- a/packages/layout/src/node/setJustifyContent.js +++ b/packages/layout/src/node/setJustifyContent.ts @@ -1,5 +1,6 @@ import * as Yoga from 'yoga-layout/load'; import { isNil } from '@react-pdf/fns'; +import { SafeNode } from '../types'; const JUSTIFY_CONTENT = { center: Yoga.Justify.Center, @@ -9,19 +10,13 @@ const JUSTIFY_CONTENT = { 'space-evenly': Yoga.Justify.SpaceEvenly, }; -/** - * @typedef {Function} NodeInstanceWrapper - * @param {Object} node node instance - * @returns {Object} node instance - */ - /** * Set justify content attribute to node's Yoga instance * - * @param {string} value justify content value - * @returns {NodeInstanceWrapper} node instance wrapper + * @param value - Justify content value + * @returns Node instance wrapper */ -const setJustifyContent = (value) => (node) => { +const setJustifyContent = (value?: string | null) => (node: SafeNode) => { const { yogaNode } = node; if (!isNil(value) && yogaNode) { diff --git a/packages/layout/src/node/setMargin.js b/packages/layout/src/node/setMargin.js deleted file mode 100644 index 90eace89c..000000000 --- a/packages/layout/src/node/setMargin.js +++ /dev/null @@ -1,62 +0,0 @@ -import * as Yoga from 'yoga-layout/load'; - -import setYogaValue from './setYogaValue'; - -/** - * @typedef {Function} NodeInstanceWrapper - * @param {Object} node node instance - * @returns {Object} node instance - */ - -/** - * Set margin top attribute to node's Yoga instance - * - * @param {number} margin margin top - * @param {Object} node node instance - * @returns {Object} node instance - */ -export const setMarginTop = setYogaValue('margin', Yoga.Edge.Top); - -/** - * Set margin right attribute to node's Yoga instance - * - * @param {number} margin margin right - * @param {Object} node node instance - * @returns {Object} node instance - */ -export const setMarginRight = setYogaValue('margin', Yoga.Edge.Right); - -/** - * Set margin bottom attribute to node's Yoga instance - * - * @param {number} margin margin bottom - * @param {Object} node node instance - * @returns {Object} node instance - */ -export const setMarginBottom = setYogaValue('margin', Yoga.Edge.Bottom); - -/** - * Set margin left attribute to node's Yoga instance - * - * @param {number} margin margin left - * @param {Object} node node instance - * @returns {Object} node instance - */ -export const setMarginLeft = setYogaValue('margin', Yoga.Edge.Left); - -/** - * Set all margins at once - * - * @param {number | string} margin margin - * @returns {NodeInstanceWrapper} node instance wrapper - */ -export const setMargin = (margin) => (node) => { - setMarginTop(margin)(node); - setMarginRight(margin)(node); - setMarginBottom(margin)(node); - setMarginLeft(margin)(node); - - return node; -}; - -export default setMargin; diff --git a/packages/layout/src/node/setMargin.ts b/packages/layout/src/node/setMargin.ts new file mode 100644 index 000000000..9a2b0dea9 --- /dev/null +++ b/packages/layout/src/node/setMargin.ts @@ -0,0 +1,58 @@ +import * as Yoga from 'yoga-layout/load'; + +import setYogaValue from './setYogaValue'; +import { SafeNode } from '../types'; + +/** + * Set margin top attribute to node's Yoga instance + * + * @param margin - Margin top + * @param node - Node instance + * @returns Node instance + */ +export const setMarginTop = setYogaValue('margin', Yoga.Edge.Top); + +/** + * Set margin right attribute to node's Yoga instance + * + * @param margin - Margin right + * @param node - Node instance + * @returns Node instance + */ +export const setMarginRight = setYogaValue('margin', Yoga.Edge.Right); + +/** + * Set margin bottom attribute to node's Yoga instance + * + * @param margin - Margin bottom + * @param node - Node instance + * @returns Node instance + */ +export const setMarginBottom = setYogaValue('margin', Yoga.Edge.Bottom); + +/** + * Set margin left attribute to node's Yoga instance + * + * @param margin - Margin left + * @param node - Node instance + * @returns Node instance + */ +export const setMarginLeft = setYogaValue('margin', Yoga.Edge.Left); + +/** + * Set all margins at once + * + * @param margin - Margin + * @returns Node instance wrapper + */ +export const setMargin = + (margin?: number | string | null) => (node: SafeNode) => { + setMarginTop(margin)(node); + setMarginRight(margin)(node); + setMarginBottom(margin)(node); + setMarginLeft(margin)(node); + + return node; + }; + +export default setMargin; diff --git a/packages/layout/src/node/setOverflow.js b/packages/layout/src/node/setOverflow.ts similarity index 62% rename from packages/layout/src/node/setOverflow.js rename to packages/layout/src/node/setOverflow.ts index 3dc9a0a08..ed74d5e6c 100644 --- a/packages/layout/src/node/setOverflow.js +++ b/packages/layout/src/node/setOverflow.ts @@ -1,24 +1,19 @@ import * as Yoga from 'yoga-layout/load'; import { isNil } from '@react-pdf/fns'; +import { SafeNode } from '../types'; const OVERFLOW = { hidden: Yoga.Overflow.Hidden, scroll: Yoga.Overflow.Scroll, }; -/** - * @typedef {Function} NodeInstanceWrapper - * @param {Object} node node instance - * @returns {Object} node instance - */ - /** * Set overflow attribute to node's Yoga instance * - * @param {string} value overflow value - * @returns {NodeInstanceWrapper} node instance wrapper + * @param value - Overflow value + * @returns Node instance wrapper */ -const setOverflow = (value) => (node) => { +const setOverflow = (value?: string | null) => (node: SafeNode) => { const { yogaNode } = node; if (!isNil(value) && yogaNode) { diff --git a/packages/layout/src/node/setPadding.js b/packages/layout/src/node/setPadding.js deleted file mode 100644 index 9382597cf..000000000 --- a/packages/layout/src/node/setPadding.js +++ /dev/null @@ -1,62 +0,0 @@ -import * as Yoga from 'yoga-layout/load'; - -import setYogaValue from './setYogaValue'; - -/** - * @typedef {Function} NodeInstanceWrapper - * @param {Object} node node instance - * @returns {Object} node instance - */ - -/** - * Set padding top attribute to node's Yoga instance - * - * @param {number} padding padding top - * @param {Object} node node instance - * @returns {Object} node instance - */ -export const setPaddingTop = setYogaValue('padding', Yoga.Edge.Top); - -/** - * Set padding right attribute to node's Yoga instance - * - * @param {number} padding padding right - * @param {Object} node node instance - * @returns {Object} node instance - */ -export const setPaddingRight = setYogaValue('padding', Yoga.Edge.Right); - -/** - * Set padding bottom attribute to node's Yoga instance - * - * @param {number} padding padding bottom - * @param {Object} node node instance - * @returns {Object} node instance - */ -export const setPaddingBottom = setYogaValue('padding', Yoga.Edge.Bottom); - -/** - * Set padding left attribute to node's Yoga instance - * - * @param {number} padding padding left - * @param {Object} node node instance - * @returns {Object} node instance - */ -export const setPaddingLeft = setYogaValue('padding', Yoga.Edge.Left); - -/** - * Set all paddings at once - * - * @param {number | string} padding padding - * @returns {NodeInstanceWrapper} node instance wrapper - */ -export const setPadding = (padding) => (node) => { - setPaddingTop(padding)(node); - setPaddingRight(padding)(node); - setPaddingBottom(padding)(node); - setPaddingLeft(padding)(node); - - return node; -}; - -export default setPadding; diff --git a/packages/layout/src/node/setPadding.ts b/packages/layout/src/node/setPadding.ts new file mode 100644 index 000000000..55d3cf79d --- /dev/null +++ b/packages/layout/src/node/setPadding.ts @@ -0,0 +1,58 @@ +import * as Yoga from 'yoga-layout/load'; + +import setYogaValue from './setYogaValue'; +import { SafeNode } from '../types'; + +/** + * Set padding top attribute to node's Yoga instance + * + * @param padding - Padding top + * @param node - Node instance + * @returns Node instance + */ +export const setPaddingTop = setYogaValue('padding', Yoga.Edge.Top); + +/** + * Set padding right attribute to node's Yoga instance + * + * @param padding - Padding right + * @param node - Node instance + * @returns Node instance + */ +export const setPaddingRight = setYogaValue('padding', Yoga.Edge.Right); + +/** + * Set padding bottom attribute to node's Yoga instance + * + * @param padding - Padding bottom + * @param node Node instance + * @returns Node instance + */ +export const setPaddingBottom = setYogaValue('padding', Yoga.Edge.Bottom); + +/** + * Set padding left attribute to node's Yoga instance + * + * @param padding - Padding left + * @param node - Node instance + * @returns Node instance + */ +export const setPaddingLeft = setYogaValue('padding', Yoga.Edge.Left); + +/** + * Set all paddings at once + * + * @param padding padding + * @returns Node instance + */ +export const setPadding = + (padding?: number | string | null) => (node: SafeNode) => { + setPaddingTop(padding)(node); + setPaddingRight(padding)(node); + setPaddingBottom(padding)(node); + setPaddingLeft(padding)(node); + + return node; + }; + +export default setPadding; diff --git a/packages/layout/src/node/setPosition.js b/packages/layout/src/node/setPosition.js deleted file mode 100644 index 10d26b961..000000000 --- a/packages/layout/src/node/setPosition.js +++ /dev/null @@ -1,62 +0,0 @@ -import * as Yoga from 'yoga-layout/load'; - -import setYogaValue from './setYogaValue'; - -/** - * @typedef {Function} NodeInstanceWrapper - * @param {Object} node node instance - * @returns {Object} node instance - */ - -/** - * Set position top attribute to node's Yoga instance - * - * @param {number} position position top - * @param {Object} node node instance - * @returns {Object} node instance - */ -export const setPositionTop = setYogaValue('position', Yoga.Edge.Top); - -/** - * Set position right attribute to node's Yoga instance - * - * @param {number} position position right - * @param {Object} node node instance - * @returns {Object} node instance - */ -export const setPositionRight = setYogaValue('position', Yoga.Edge.Right); - -/** - * Set position bottom attribute to node's Yoga instance - * - * @param {number} position position bottom - * @param {Object} node node instance - * @returns {Object} node instance - */ -export const setPositionBottom = setYogaValue('position', Yoga.Edge.Bottom); - -/** - * Set position left attribute to node's Yoga instance - * - * @param {number} position position left - * @param {Object} node node instance - * @returns {Object} node instance - */ -export const setPositionLeft = setYogaValue('position', Yoga.Edge.Left); - -/** - * Set all positions at once - * - * @param {number | string} position position - * @returns {NodeInstanceWrapper} node instance wrapper - */ -export const setPosition = (position) => (node) => { - setPositionTop(position)(node); - setPositionRight(position)(node); - setPositionBottom(position)(node); - setPositionLeft(position)(node); - - return node; -}; - -export default setPosition; diff --git a/packages/layout/src/node/setPosition.ts b/packages/layout/src/node/setPosition.ts new file mode 100644 index 000000000..cf41dcfde --- /dev/null +++ b/packages/layout/src/node/setPosition.ts @@ -0,0 +1,58 @@ +import * as Yoga from 'yoga-layout/load'; + +import setYogaValue from './setYogaValue'; +import { SafeNode } from '../types'; + +/** + * Set position top attribute to node's Yoga instance + * + * @param position - Position top + * @param node - Node instance + * @returns Node instance + */ +export const setPositionTop = setYogaValue('position', Yoga.Edge.Top); + +/** + * Set position right attribute to node's Yoga instance + * + * @param position - Position right + * @param node - Node instance + * @returns Node instance + */ +export const setPositionRight = setYogaValue('position', Yoga.Edge.Right); + +/** + * Set position bottom attribute to node's Yoga instance + * + * @param position - Position bottom + * @param node - Node instance + * @returns Node instance + */ +export const setPositionBottom = setYogaValue('position', Yoga.Edge.Bottom); + +/** + * Set position left attribute to node's Yoga instance + * + * @param position - Position left + * @param node - Node instance + * @returns Node instance + */ +export const setPositionLeft = setYogaValue('position', Yoga.Edge.Left); + +/** + * Set all positions at once + * + * @param position - Position + * @returns Node instance wrapper + */ +export const setPosition = + (position?: number | string | null) => (node: SafeNode) => { + setPositionTop(position)(node); + setPositionRight(position)(node); + setPositionBottom(position)(node); + setPositionLeft(position)(node); + + return node; + }; + +export default setPosition; diff --git a/packages/layout/src/node/setPositionType.js b/packages/layout/src/node/setPositionType.ts similarity index 62% rename from packages/layout/src/node/setPositionType.js rename to packages/layout/src/node/setPositionType.ts index 705910be0..3230eb1b6 100644 --- a/packages/layout/src/node/setPositionType.js +++ b/packages/layout/src/node/setPositionType.ts @@ -1,5 +1,6 @@ import * as Yoga from 'yoga-layout/load'; import { isNil } from '@react-pdf/fns'; +import { SafeNode } from '../types'; const POSITION = { absolute: Yoga.PositionType.Absolute, @@ -7,19 +8,13 @@ const POSITION = { static: Yoga.PositionType.Static, }; -/** - * @typedef {Function} NodeInstanceWrapper - * @param {Object} node node instance - * @returns {Object} node instance - */ - /** * Set position type attribute to node's Yoga instance * - * @param {string} value position position type - * @returns {NodeInstanceWrapper} node instance wrapper + * @param value - Position position type + * @returns Node instance */ -const setPositionType = (value) => (node) => { +const setPositionType = (value?: string | null) => (node: SafeNode) => { const { yogaNode } = node; if (!isNil(value) && yogaNode) { diff --git a/packages/layout/src/node/setYogaValue.js b/packages/layout/src/node/setYogaValue.js deleted file mode 100644 index 8f3e5c31d..000000000 --- a/packages/layout/src/node/setYogaValue.js +++ /dev/null @@ -1,58 +0,0 @@ -import { isNil, upperFirst, matchPercent } from '@react-pdf/fns'; - -/** - * @typedef {Function} NodeInstanceWrapper - * @param {Object} node node instance - * @returns {Object} node instance - */ - -/** - * @typedef {Function} YogaValueSetter - * @param {any} value - * @returns {NodeInstanceWrapper} node instance wrapper - */ - -/** - * Set generic yoga attribute to node's Yoga instance, handing `auto`, edges and percentage cases - * - * @param {string} attr property - * @param {number} [edge] edge - * @returns {YogaValueSetter} node instance wrapper - */ -const setYogaValue = (attr, edge) => (value) => (node) => { - const { yogaNode } = node; - - if (!isNil(value) && yogaNode) { - const hasEdge = !isNil(edge); - const fixedMethod = `set${upperFirst(attr)}`; - const autoMethod = `${fixedMethod}Auto`; - const percentMethod = `${fixedMethod}Percent`; - const percent = matchPercent(value); - - if (percent && !yogaNode[percentMethod]) { - throw new Error(`You can't pass percentage values to ${attr} property`); - } - - if (percent) { - if (hasEdge) { - yogaNode[percentMethod]?.(edge, percent.value); - } else { - yogaNode[percentMethod]?.(percent.value); - } - } else if (value === 'auto') { - if (hasEdge) { - yogaNode[autoMethod]?.(edge); - } else { - yogaNode[autoMethod]?.(); - } - } else if (hasEdge) { - yogaNode[fixedMethod]?.(edge, value); - } else { - yogaNode[fixedMethod]?.(value); - } - } - - return node; -}; - -export default setYogaValue; diff --git a/packages/layout/src/node/setYogaValue.ts b/packages/layout/src/node/setYogaValue.ts new file mode 100644 index 000000000..51e4c0529 --- /dev/null +++ b/packages/layout/src/node/setYogaValue.ts @@ -0,0 +1,52 @@ +import { isNil, upperFirst, matchPercent } from '@react-pdf/fns'; +import * as Yoga from 'yoga-layout/load'; + +import { SafeNode } from '../types'; + +/** + * Set generic yoga attribute to node's Yoga instance, handing `auto`, edges and percentage cases + * + * @param attr - Property + * @param edge - Edge + * @returns Node instance wrapper + */ +const setYogaValue = + (attr: string, edge?: Yoga.Edge) => + (value?: string | number | null) => + (node: SafeNode) => { + const { yogaNode } = node; + + if (!isNil(value) && yogaNode) { + const hasEdge = !isNil(edge); + const fixedMethod = `set${upperFirst(attr)}`; + const autoMethod = `${fixedMethod}Auto`; + const percentMethod = `${fixedMethod}Percent`; + const percent = matchPercent(value); + + if (percent && !yogaNode[percentMethod]) { + throw new Error(`You can't pass percentage values to ${attr} property`); + } + + if (percent) { + if (hasEdge) { + yogaNode[percentMethod]?.(edge, percent.value); + } else { + yogaNode[percentMethod]?.(percent.value); + } + } else if (value === 'auto') { + if (hasEdge) { + yogaNode[autoMethod]?.(edge); + } else { + yogaNode[autoMethod]?.(); + } + } else if (hasEdge) { + yogaNode[fixedMethod]?.(edge, value); + } else { + yogaNode[fixedMethod]?.(value); + } + } + + return node; + }; + +export default setYogaValue; diff --git a/packages/layout/src/node/shouldBreak.js b/packages/layout/src/node/shouldBreak.ts similarity index 63% rename from packages/layout/src/node/shouldBreak.js rename to packages/layout/src/node/shouldBreak.ts index 1ab5bf442..bf164088c 100644 --- a/packages/layout/src/node/shouldBreak.js +++ b/packages/layout/src/node/shouldBreak.ts @@ -1,13 +1,16 @@ +import { SafeNode } from '../types'; import getWrap from './getWrap'; -const getBreak = (node) => node.props?.break || false; +const getBreak = (node: SafeNode) => + 'break' in node.props ? node.props.break : false; -const getMinPresenceAhead = (node) => node.props?.minPresenceAhead || 0; +const getMinPresenceAhead = (node: SafeNode) => + 'minPresenceAhead' in node.props ? node.props.minPresenceAhead : 0; -const getFurthestEnd = (elements) => +const getFurthestEnd = (elements: SafeNode[]) => Math.max(...elements.map((node) => node.box.top + node.box.height)); -const getEndOfMinPresenceAhead = (child) => { +const getEndOfMinPresenceAhead = (child: SafeNode) => { return ( child.box.top + child.box.height + @@ -16,22 +19,27 @@ const getEndOfMinPresenceAhead = (child) => { ); }; -const getEndOfPresence = (child, futureElements) => { +const getEndOfPresence = (child: SafeNode, futureElements: SafeNode[]) => { const afterMinPresenceAhead = getEndOfMinPresenceAhead(child); const endOfFurthestFutureElement = getFurthestEnd( - futureElements.filter((node) => !node.props?.fixed), + futureElements.filter((node) => !('fixed' in node.props)), ); return Math.min(afterMinPresenceAhead, endOfFurthestFutureElement); }; -const shouldBreak = (child, futureElements, height) => { - if (child.props?.fixed) return false; +const shouldBreak = ( + child: SafeNode, + futureElements: SafeNode[], + height: number, +) => { + if ('fixed' in child.props) return false; const shouldSplit = height < child.box.top + child.box.height; const canWrap = getWrap(child); // Calculate the y coordinate where the desired presence of the child ends const endOfPresence = getEndOfPresence(child, futureElements); + // If the child is already at the top of the page, breaking won't improve its presence // (as long as react-pdf does not support breaking into differently sized containers) const breakingImprovesPresence = child.box.top > child.box.marginTop; diff --git a/packages/layout/src/node/splitNode.js b/packages/layout/src/node/splitNode.ts similarity index 72% rename from packages/layout/src/node/splitNode.js rename to packages/layout/src/node/splitNode.ts index 6b5955ac8..b48944edd 100644 --- a/packages/layout/src/node/splitNode.js +++ b/packages/layout/src/node/splitNode.ts @@ -1,15 +1,17 @@ import { isNil } from '@react-pdf/fns'; -const getTop = (node) => node.box?.top || 0; +import { SafeNode } from '../types'; -const hasFixedHeight = (node) => !isNil(node.style?.height); +const getTop = (node: SafeNode) => node.box?.top || 0; -const splitNode = (node, height) => { +const hasFixedHeight = (node: SafeNode) => !isNil(node.style?.height); + +const splitNode = (node: SafeNode, height: number) => { if (!node) return [null, null]; const nodeTop = getTop(node); - const current = Object.assign({}, node, { + const current: SafeNode = Object.assign({}, node, { box: { ...node.box, borderBottomWidth: 0, @@ -30,7 +32,7 @@ const splitNode = (node, height) => { ? node.box.height - (height - nodeTop) : null; - const next = Object.assign({}, node, { + const next: SafeNode = Object.assign({}, node, { box: { ...node.box, top: 0, diff --git a/packages/layout/src/page/getContentArea.ts b/packages/layout/src/page/getContentArea.ts index 3b639f3d9..9c8e5b220 100644 --- a/packages/layout/src/page/getContentArea.ts +++ b/packages/layout/src/page/getContentArea.ts @@ -1,10 +1,9 @@ import getPadding from '../node/getPadding'; -import { PageNode } from '../types'; +import { SafePageNode } from '../types'; -// TODO: Use safe node -const getContentArea = (page: PageNode) => { +const getContentArea = (page: SafePageNode) => { const height = page.style?.height as number; - const { paddingTop, paddingBottom } = getPadding(page); + const { paddingTop, paddingBottom } = getPadding(page as any); return height - (paddingBottom as number) - (paddingTop as number); }; diff --git a/packages/layout/src/page/getSize.ts b/packages/layout/src/page/getSize.ts index 4531b0b73..136fa7d69 100644 --- a/packages/layout/src/page/getSize.ts +++ b/packages/layout/src/page/getSize.ts @@ -83,7 +83,12 @@ const parseValue = (value: string | number) => { * @param inputDpi - User defined dpi * @returns Transformed value */ -const transformUnit = (value: string | number, inputDpi: number) => { +const transformUnit = ( + value: string | number | undefined, + inputDpi: number, +) => { + if (!value) return 0; + const scalar = parseValue(value); const outputDpi = 72; diff --git a/packages/layout/src/page/getWrapArea.ts b/packages/layout/src/page/getWrapArea.ts index 8ff508aa8..20617c738 100644 --- a/packages/layout/src/page/getWrapArea.ts +++ b/packages/layout/src/page/getWrapArea.ts @@ -1,9 +1,7 @@ import getPadding from '../node/getPadding'; -import { PageNode } from '../types'; +import { SafePageNode } from '../types'; -// TODO: Use safe node - -const getWrapArea = (page: PageNode) => { +const getWrapArea = (page: SafePageNode) => { const height = page.style?.height as number; const { paddingBottom } = getPadding(page); diff --git a/packages/layout/src/page/isHeightAuto.ts b/packages/layout/src/page/isHeightAuto.ts index f114755ac..39731bff8 100644 --- a/packages/layout/src/page/isHeightAuto.ts +++ b/packages/layout/src/page/isHeightAuto.ts @@ -1,5 +1,5 @@ import { isNil } from '@react-pdf/fns'; -import { PageNode } from '../types'; +import { SafePageNode } from '../types'; /** * Checks if page has auto height @@ -7,6 +7,6 @@ import { PageNode } from '../types'; * @param page * @returns Is page height auto */ -const isHeightAuto = (page: PageNode) => isNil(page.box?.height); +const isHeightAuto = (page: SafePageNode) => isNil(page.box?.height); export default isHeightAuto; diff --git a/packages/layout/src/steps/resolveAssets.js b/packages/layout/src/steps/resolveAssets.ts similarity index 54% rename from packages/layout/src/steps/resolveAssets.js rename to packages/layout/src/steps/resolveAssets.ts index dd5ccdf30..b43ae467b 100644 --- a/packages/layout/src/steps/resolveAssets.js +++ b/packages/layout/src/steps/resolveAssets.ts @@ -1,19 +1,23 @@ import * as P from '@react-pdf/primitives'; +import FontStore from '@react-pdf/font'; +import { castArray } from '@react-pdf/fns'; import fetchEmojis from '../text/emoji'; import fetchImage from '../image/fetchImage'; +import { SafeImageNode, SafeNode } from '../types'; -const isImage = (node) => node.type === P.Image; +const isImage = (node: SafeNode): node is SafeImageNode => + node.type === P.Image; /** * Get all asset promises that need to be resolved * - * @param {Object} fontStore font store - * @param {Object} node root node - * @returns {Promise[]} asset promises + * @param fontStore - Font store + * @param node - Root node + * @returns Asset promises */ -const fetchAssets = (fontStore, node) => { - const promises = []; +const fetchAssets = (fontStore: FontStore, node: SafeNode) => { + const promises: Promise[] = []; const listToExplore = node.children?.slice(0) || []; const emojiSource = fontStore ? fontStore.getEmojiSource() : null; @@ -25,14 +29,24 @@ const fetchAssets = (fontStore, node) => { } if (fontStore && n.style?.fontFamily) { - promises.push(fontStore.load(n.style)); + const fontFamilies = castArray(n.style.fontFamily); + + promises.push( + ...fontFamilies.map((fontFamily) => + fontStore.load({ + fontFamily, + fontStyle: n.style.fontStyle, + fontWeight: n.style.fontWeight, + }), + ), + ); } if (typeof n === 'string') { promises.push(...fetchEmojis(n, emojiSource)); } - if (typeof n.value === 'string') { + if ('value' in n && typeof n.value === 'string') { promises.push(...fetchEmojis(n.value, emojiSource)); } @@ -50,11 +64,11 @@ const fetchAssets = (fontStore, node) => { * Fetch image, font and emoji assets in parallel. * Layout process will not be resumed until promise resolves. * - * @param {Object} node root node - * @param {Object} fontStore font store - * @returns {Promise} root node + * @param node root node + * @param fontStore font store + * @returns Root node */ -const resolveAssets = async (node, fontStore) => { +const resolveAssets = async (node: SafeNode, fontStore: FontStore) => { const promises = fetchAssets(fontStore, node); await Promise.all(promises); return node; diff --git a/packages/layout/src/steps/resolveBookmarks.ts b/packages/layout/src/steps/resolveBookmarks.ts index 24cadb973..58c2fa97b 100644 --- a/packages/layout/src/steps/resolveBookmarks.ts +++ b/packages/layout/src/steps/resolveBookmarks.ts @@ -6,22 +6,32 @@ const getBookmarkValue = (bookmark: Bookmark) => { : bookmark; }; +type Parent = Bookmark & { ref: number; parent: number | null }; + +type Item = { + value: Node; + parent: Parent | null; +}; + const resolveBookmarks = (node: DocumentNode) => { let refs = 0; const children = (node.children || []).slice(0); - const listToExplore: Node[] = children.map((value) => ({ + const listToExplore: Item[] = children.map((value) => ({ value, parent: null, })); while (listToExplore.length > 0) { const element = listToExplore.shift(); + + if (!element) break; + const child = element.value; let parent = element.parent; - if (child.props?.bookmark) { + if (child.props && 'bookmark' in child.props) { const bookmark = getBookmarkValue(child.props.bookmark); const ref = refs++; const newHierarchy = { ref, parent: parent?.ref, ...bookmark }; diff --git a/packages/layout/src/steps/resolveDimensions.js b/packages/layout/src/steps/resolveDimensions.ts similarity index 77% rename from packages/layout/src/steps/resolveDimensions.js rename to packages/layout/src/steps/resolveDimensions.ts index f818f6842..3db12abb4 100644 --- a/packages/layout/src/steps/resolveDimensions.js +++ b/packages/layout/src/steps/resolveDimensions.ts @@ -1,5 +1,6 @@ import * as P from '@react-pdf/primitives'; import { isNil, compose } from '@react-pdf/fns'; +import FontStore from '@react-pdf/font'; import getMargin from '../node/getMargin'; import getPadding from '../node/getPadding'; @@ -56,6 +57,13 @@ import measureSvg from '../svg/measureSvg'; import measureText from '../text/measureText'; import measureImage from '../image/measureImage'; import measureCanvas from '../canvas/measureCanvas'; +import { + Box, + SafeDocumentNode, + SafeNode, + SafePageNode, + YogaInstance, +} from '../types'; const isType = (type) => (node) => node.type === type; @@ -67,18 +75,17 @@ const isImage = isType(P.Image); const isCanvas = isType(P.Canvas); const isTextInstance = isType(P.TextInstance); -const setNodeHeight = (node) => { - const value = isPage(node) ? node.box.height : node.style.height; +const setNodeHeight = (node: SafeNode) => { + const value = isPage(node) ? node.box?.height : node.style?.height; return setHeight(value); }; /** * Set styles valeus into yoga node before layout calculation * - * @param {Object} node - * @returns {Object} node + * @param node */ -const setYogaValues = (node) => { +const setYogaValues = (node: SafeNode) => { compose( setNodeHeight(node), setWidth(node.style.width), @@ -120,17 +127,11 @@ const setYogaValues = (node) => { )(node); }; -/** - * @typedef {Function} InsertYogaNodes - * @param {Object} child child node - * @returns {Object} node - */ - /** * Inserts child into parent' yoga node * - * @param {Object} parent parent - * @returns {InsertYogaNodes} insert yoga nodes + * @param parent parent + * @returns Insert yoga nodes */ const insertYogaNodes = (parent) => (child) => { parent.insertChild(child.yogaNode, parent.getChildCount()); @@ -172,36 +173,38 @@ const isLayoutElement = (node) => * Creates and add yoga node to document tree * Handles measure function for text and image nodes * - * @returns {CreateYogaNodes} create yoga nodes + * @returns Create yoga nodes */ -const createYogaNodes = (page, fontStore, yoga) => (node) => { - const yogaNode = yoga.node.create(); +const createYogaNodes = + (page: SafePageNode, fontStore: FontStore, yoga: YogaInstance) => + (node: SafeNode) => { + const yogaNode = yoga.node.create(); - const result = Object.assign({}, node, { yogaNode }); + const result = Object.assign({}, node, { yogaNode }); - setYogaValues(result); + setYogaValues(result); - if (isLayoutElement(node) && node.children) { - const resolveChild = compose( - insertYogaNodes(yogaNode), - createYogaNodes(page, fontStore, yoga), - ); + if (isLayoutElement(node) && node.children) { + const resolveChild = compose( + insertYogaNodes(yogaNode), + createYogaNodes(page, fontStore, yoga), + ); - result.children = node.children.map(resolveChild); - } + result.children = node.children.map(resolveChild); + } - setMeasureFunc(result, page, fontStore); + setMeasureFunc(result, page, fontStore); - return result; -}; + return result; + }; /** * Performs yoga calculation * - * @param {Object} page page node - * @returns {Object} page node + * @param page - Page node + * @returns Page node */ -const calculateLayout = (page) => { +const calculateLayout = (page: SafePageNode) => { page.yogaNode.calculateLayout(); return page; }; @@ -209,13 +212,13 @@ const calculateLayout = (page) => { /** * Saves Yoga layout result into 'box' attribute of node * - * @param {Object} node - * @returns {Object} node with box data + * @param node + * @returns Node with box data */ -const persistDimensions = (node) => { +const persistDimensions = (node: SafeNode) => { if (isTextInstance(node)) return node; - const box = Object.assign( + const box: Box = Object.assign( getPadding(node), getMargin(node), getBorderWidth(node), @@ -235,10 +238,10 @@ const persistDimensions = (node) => { /** * Removes yoga node from document tree * - * @param {Object} node - * @returns {Object} node without yoga node + * @param node + * @returns Node without yoga node */ -const destroyYogaNodes = (node) => { +const destroyYogaNodes = (node: SafeNode): SafeNode => { const newNode = Object.assign({}, node); delete newNode.yogaNode; @@ -253,10 +256,10 @@ const destroyYogaNodes = (node) => { /** * Free yoga node from document tree * - * @param {Object} node - * @returns {Object} node without yoga node + * @param node + * @returns Node without yoga node */ -const freeYogaNodes = (node) => { +const freeYogaNodes = (node: SafeNode) => { if (node.yogaNode) node.yogaNode.freeRecursive(); return node; }; @@ -266,10 +269,14 @@ const freeYogaNodes = (node) => { * Takes node values from 'box' and 'style' attributes, and persist them back into 'box' * Destroy yoga values at the end. * - * @param {Object} page object - * @returns {Object} page object with correct 'box' layout attributes + * @param page - Object + * @returns Page object with correct 'box' layout attributes */ -export const resolvePageDimensions = (page, fontStore, yoga) => { +export const resolvePageDimensions = ( + page: SafePageNode, + fontStore: FontStore, + yoga: YogaInstance, +) => { if (isNil(page)) return null; return compose( @@ -284,15 +291,16 @@ export const resolvePageDimensions = (page, fontStore, yoga) => { /** * Calculates root object layout using Yoga. * - * @param {Object} node root object - * @param {Object} fontStore font store - * @returns {Object} root object with correct 'box' layout attributes + * @param node - Root object + * @param fontStore - Font store + * @returns Root object with correct 'box' layout attributes */ -const resolveDimensions = (node, fontStore) => { +const resolveDimensions = (node: SafeDocumentNode, fontStore: FontStore) => { if (!node.children) return node; - const resolveChild = (child) => + const resolveChild = (child: SafePageNode) => resolvePageDimensions(child, fontStore, node.yoga); + const children = node.children.map(resolveChild); return Object.assign({}, node, { children }); diff --git a/packages/layout/src/steps/resolveInheritance.js b/packages/layout/src/steps/resolveInheritance.ts similarity index 62% rename from packages/layout/src/steps/resolveInheritance.js rename to packages/layout/src/steps/resolveInheritance.ts index 7ee6359ac..192a9ded7 100644 --- a/packages/layout/src/steps/resolveInheritance.js +++ b/packages/layout/src/steps/resolveInheritance.ts @@ -1,5 +1,10 @@ import * as P from '@react-pdf/primitives'; import { pick, compose } from '@react-pdf/fns'; +import { SafeStyle } from '@react-pdf/stylesheet'; + +import { SafeNode } from '../types'; + +type StyleKey = keyof SafeStyle; const BASE_INHERITABLE_PROPERTIES = [ 'color', @@ -22,12 +27,18 @@ const TEXT_INHERITABLE_PROPERTIES = [ 'backgroundColor', ]; -const isSvg = (node) => node.type === P.Svg; +const isType = (type: string) => (node: SafeNode) => node.type === type; -const isText = (node) => node.type === P.Text; +const isSvg = isType(P.Svg); + +const isText = isType(P.Text); // Merge style values -const mergeValues = (styleName, value, inheritedValue) => { +const mergeValues = ( + styleName: K, + value: SafeStyle[K], + inheritedValue: SafeStyle[K], +) => { switch (styleName) { case 'textDecoration': { // merge not none and not false textDecoration values to one rule @@ -39,12 +50,12 @@ const mergeValues = (styleName, value, inheritedValue) => { }; // Merge inherited and node styles -const merge = (inheritedStyles, style) => { +const merge = (inheritedStyles: SafeStyle, style: SafeStyle): SafeStyle => { const mergedStyles = { ...inheritedStyles }; Object.entries(style).forEach(([styleName, value]) => { mergedStyles[styleName] = mergeValues( - styleName, + styleName as StyleKey, value, inheritedStyles[styleName], ); @@ -53,34 +64,31 @@ const merge = (inheritedStyles, style) => { return mergedStyles; }; -/** - * @typedef {Function} MergeStyles - * @param {Object} node - * @returns {Object} node with styles merged - */ - /** * Merges styles with node * - * @param {Object} inheritedStyles style object - * @returns {MergeStyles} merge styles function + * @param inheritedStyles - Style object + * @returns Merge styles function */ -const mergeStyles = (inheritedStyles) => (node) => { - const style = merge(inheritedStyles, node.style || {}); - return Object.assign({}, node, { style }); -}; +const mergeStyles = + (inheritedStyles: SafeStyle) => + (node: SafeNode): SafeNode => { + const style = merge(inheritedStyles, node.style || {}); + + return Object.assign({}, node, { style }); + }; /** * Inherit style values from the root to the leafs * - * @param {Object} node document root - * @returns {Object} document root with inheritance + * @param node - Document root + * @returns Document root with inheritance * */ -const resolveInheritance = (node) => { +const resolveInheritance = (node: SafeNode) => { if (isSvg(node)) return node; - if (!node.children) return node; + if (!('children' in node)) return node; const inheritableProperties = isText(node) ? TEXT_INHERITABLE_PROPERTIES diff --git a/packages/layout/src/steps/resolveLinkSubstitution.ts b/packages/layout/src/steps/resolveLinkSubstitution.ts index 2d62361ba..37b23a723 100644 --- a/packages/layout/src/steps/resolveLinkSubstitution.ts +++ b/packages/layout/src/steps/resolveLinkSubstitution.ts @@ -1,5 +1,6 @@ import * as P from '@react-pdf/primitives'; import { compose } from '@react-pdf/fns'; + import { Node } from '../types'; const isType = (type: string) => (node: Node) => node.type === type; diff --git a/packages/layout/src/steps/resolveOrigins.js b/packages/layout/src/steps/resolveOrigins.ts similarity index 66% rename from packages/layout/src/steps/resolveOrigins.js rename to packages/layout/src/steps/resolveOrigins.ts index 518a6a095..1c742ed0e 100644 --- a/packages/layout/src/steps/resolveOrigins.js +++ b/packages/layout/src/steps/resolveOrigins.ts @@ -1,12 +1,13 @@ import getOrigin from '../node/getOrigin'; +import { SafeDocumentNode, SafeNode } from '../types'; /** * Resolve node origin * - * @param {Object} node - * @returns {Object} node with origin attribute + * @param node + * @returns Node with origin attribute */ -const resolveNodeOrigin = (node) => { +const resolveNodeOrigin = (node: SafeNode): SafeNode => { const origin = getOrigin(node); const newNode = Object.assign({}, node, { origin }); @@ -21,11 +22,11 @@ const resolveNodeOrigin = (node) => { /** * Resolve document origins * - * @param {Object} root document root - * @returns {Object} document root + * @param root - Document root + * @returns Document root */ -const resolveOrigin = (root) => { +const resolveOrigin = (root: SafeDocumentNode) => { if (!root.children) return root; const children = root.children.map(resolveNodeOrigin); diff --git a/packages/layout/src/steps/resolvePagePaddings.js b/packages/layout/src/steps/resolvePagePaddings.js deleted file mode 100644 index 1881ce9a1..000000000 --- a/packages/layout/src/steps/resolvePagePaddings.js +++ /dev/null @@ -1,75 +0,0 @@ -import { evolve, matchPercent } from '@react-pdf/fns'; - -/** - * @typedef {Function} ResolvePageHorizontalPadding - * @param {string} value padding value - * @returns {Object} translated padding value - */ - -/** - * Translates page percentage horizontal paddings in fixed ones - * - * @param {Object} container page container - * @returns {ResolvePageHorizontalPadding} resolve page horizontal padding - */ -const resolvePageHorizontalPadding = (container) => (value) => { - const match = matchPercent(value); - return match ? match.percent * container.width : value; -}; - -/** - * @typedef {Function} ResolvePageVerticalPadding - * @param {string} padding value - * @returns {Object} translated padding value - */ - -/** - * Translates page percentage vertical paddings in fixed ones - * - * @param {Object} container page container - * @returns {ResolvePageVerticalPadding} resolve page vertical padding - */ -const resolvePageVerticalPadding = (container) => (value) => { - const match = matchPercent(value); - return match ? match.percent * container.height : value; -}; - -/** - * Translates page percentage paddings in fixed ones - * - * @param {Object} page - * @returns {Object} page with fixed paddings - */ -const resolvePagePaddings = (page) => { - const container = page.style; - - const style = evolve( - { - paddingTop: resolvePageVerticalPadding(container), - paddingLeft: resolvePageHorizontalPadding(container), - paddingRight: resolvePageHorizontalPadding(container), - paddingBottom: resolvePageVerticalPadding(container), - }, - page.style, - ); - - return Object.assign({}, page, { style }); -}; - -/** - * Translates all pages percentage paddings in fixed ones - * This has to be computed from pages calculated size and not by Yoga - * because at this point we didn't performed pagination yet. - * - * @param {Object} root document root - * @returns {Object} document root with translated page paddings - */ -const resolvePagesPaddings = (root) => { - if (!root.children) return root; - - const children = root.children.map(resolvePagePaddings); - - return Object.assign({}, root, { children }); -}; - -export default resolvePagesPaddings; diff --git a/packages/layout/src/steps/resolvePagePaddings.ts b/packages/layout/src/steps/resolvePagePaddings.ts new file mode 100644 index 000000000..1d7f78a05 --- /dev/null +++ b/packages/layout/src/steps/resolvePagePaddings.ts @@ -0,0 +1,70 @@ +import { evolve, matchPercent } from '@react-pdf/fns'; +import { SafeStyle } from '@react-pdf/stylesheet'; + +import { SafeDocumentNode, SafePageNode } from '../types'; + +/** + * Translates page percentage horizontal paddings in fixed ones + * + * @param container - Page container + * @returns Resolve page horizontal padding + */ +const resolvePageHorizontalPadding = + (container: SafeStyle) => (value: number) => { + const match = matchPercent(value); + const width = container.width as number; + return match ? match.percent * width : value; + }; + +/** + * Translates page percentage vertical paddings in fixed ones + * + * @param container - Page container + * @returns Resolve page vertical padding + */ +const resolvePageVerticalPadding = + (container: SafeStyle) => (value: number) => { + const match = matchPercent(value); + const height = container.height as number; + return match ? match.percent * height : value; + }; + +/** + * Translates page percentage paddings in fixed ones + * + * @param page + * @returns Page with fixed paddings + */ +const resolvePagePaddings = (page: SafePageNode): SafePageNode => { + const container = page.style; + + const style = evolve( + { + paddingTop: resolvePageVerticalPadding(container), + paddingLeft: resolvePageHorizontalPadding(container), + paddingRight: resolvePageHorizontalPadding(container), + paddingBottom: resolvePageVerticalPadding(container), + }, + page.style, + ); + + return Object.assign({}, page, { style }); +}; + +/** + * Translates all pages percentage paddings in fixed ones + * This has to be computed from pages calculated size and not by Yoga + * because at this point we didn't performed pagination yet. + * + * @param root - Document root + * @returns Document root with translated page paddings + */ +const resolvePagesPaddings = (root: SafeDocumentNode) => { + if (!root.children) return root; + + const children = root.children.map(resolvePagePaddings); + + return Object.assign({}, root, { children }); +}; + +export default resolvePagesPaddings; diff --git a/packages/layout/src/steps/resolvePageSizes.ts b/packages/layout/src/steps/resolvePageSizes.ts index 78217ef2c..d5b980306 100644 --- a/packages/layout/src/steps/resolvePageSizes.ts +++ b/packages/layout/src/steps/resolvePageSizes.ts @@ -1,4 +1,5 @@ import { flatten } from '@react-pdf/stylesheet'; + import getPageSize from '../page/getSize'; import { DocumentNode, PageNode } from '../types'; diff --git a/packages/layout/src/steps/resolvePagination.js b/packages/layout/src/steps/resolvePagination.ts similarity index 74% rename from packages/layout/src/steps/resolvePagination.js rename to packages/layout/src/steps/resolvePagination.ts index 7203a1cf3..7d396cb04 100644 --- a/packages/layout/src/steps/resolvePagination.js +++ b/packages/layout/src/steps/resolvePagination.ts @@ -1,5 +1,6 @@ import * as P from '@react-pdf/primitives'; -import { isNil, omit, compose } from '@react-pdf/fns'; +import { omit, compose } from '@react-pdf/fns'; +import FontStore from '@react-pdf/font'; import isFixed from '../node/isFixed'; import splitText from '../text/splitText'; @@ -13,20 +14,33 @@ import resolveTextLayout from './resolveTextLayout'; import resolveInheritance from './resolveInheritance'; import { resolvePageDimensions } from './resolveDimensions'; import { resolvePageStyles } from './resolveStyles'; - -const isText = (node) => node.type === P.Text; +import { + DynamicPageProps, + SafeDocumentNode, + SafeLinkNode, + SafeNode, + SafePageNode, + SafeTextNode, + SafeViewNode, + YogaInstance, +} from '../types'; + +const isText = (node: SafeNode): node is SafeTextNode => node.type === P.Text; // Prevent splitting elements by low decimal numbers const SAFETY_THRESHOLD = 0.001; -const assingChildren = (children, node) => +const assingChildren = (children: SafeNode[], node: T): T => Object.assign({}, node, { children }); -const getTop = (node) => node.box?.top || 0; +const getTop = (node: SafeNode) => node.box?.top || 0; -const allFixed = (nodes) => nodes.every(isFixed); +const allFixed = (nodes: SafeNode[]) => nodes.every(isFixed); -const isDynamic = (node) => !isNil(node.props?.render); +const isDynamic = ( + node: SafeNode, +): node is SafeLinkNode | SafeTextNode | SafeViewNode => + node.props && 'render' in node.props; const relayoutPage = compose( resolveTextLayout, @@ -35,15 +49,15 @@ const relayoutPage = compose( resolvePageStyles, ); -const warnUnavailableSpace = (node) => { +const warnUnavailableSpace = (node: SafeNode) => { console.warn( `Node of type ${node.type} can't wrap between pages and it's bigger than available page height`, ); }; -const splitNodes = (height, contentArea, nodes) => { - const currentChildren = []; - const nextChildren = []; +const splitNodes = (height: number, contentArea: number, nodes: SafeNode[]) => { + const currentChildren: SafeNode[] = []; + const nextChildren: SafeNode[] = []; for (let i = 0; i < nodes.length; i += 1) { const child = nodes[i]; @@ -124,13 +138,13 @@ const splitNodes = (height, contentArea, nodes) => { return [currentChildren, nextChildren]; }; -const splitChildren = (height, contentArea, node) => { +const splitChildren = (height: number, contentArea: number, node: SafeNode) => { const children = node.children || []; const availableHeight = height - getTop(node); return splitNodes(availableHeight, contentArea, children); }; -const splitView = (node, height, contentArea) => { +const splitView = (node: SafeNode, height: number, contentArea: number) => { const [currentNode, nextNode] = splitNode(node, height); const [currentChilds, nextChildren] = splitChildren( height, @@ -144,24 +158,27 @@ const splitView = (node, height, contentArea) => { ]; }; -const split = (node, height, contentArea) => +const split = (node: SafeNode, height: number, contentArea: number) => isText(node) ? splitText(node, height) : splitView(node, height, contentArea); -const shouldResolveDynamicNodes = (node) => { +const shouldResolveDynamicNodes = (node: SafeNode) => { const children = node.children || []; return isDynamic(node) || children.some(shouldResolveDynamicNodes); }; -const resolveDynamicNodes = (props, node) => { +const resolveDynamicNodes = (props: DynamicPageProps, node: SafeNode) => { const isNodeDynamic = isDynamic(node); // Call render prop on dynamic nodes and append result to children const resolveChildren = (children = []) => { if (isNodeDynamic) { const res = node.props.render(props); - return createInstances(res) - .filter(Boolean) - .map((n) => resolveDynamicNodes(props, n)); + return ( + createInstances(res) + .filter(Boolean) + // @ts-expect-error rework dynamic nodes. conflicting types + .map((n) => resolveDynamicNodes(props, n)) + ); } return children.map((c) => resolveDynamicNodes(props, c)); @@ -172,12 +189,19 @@ const resolveDynamicNodes = (props, node) => { const box = resetHeight ? { ...node.box, height: 0 } : node.box; const children = resolveChildren(node.children); + + // @ts-expect-error handle text here specifically const lines = isNodeDynamic ? null : node.lines; return Object.assign({}, node, { box, lines, children }); }; -const resolveDynamicPage = (props, page, fontStore, yoga) => { +const resolveDynamicPage = ( + props: DynamicPageProps, + page: SafePageNode, + fontStore: FontStore, + yoga: YogaInstance, +) => { if (shouldResolveDynamicNodes(page)) { const resolvedPage = resolveDynamicNodes(props, page); return relayoutPage(resolvedPage, fontStore, yoga); @@ -186,7 +210,12 @@ const resolveDynamicPage = (props, page, fontStore, yoga) => { return page; }; -const splitPage = (page, pageNumber, fontStore, yoga) => { +const splitPage = ( + page: SafePageNode, + pageNumber: number, + fontStore: FontStore, + yoga: YogaInstance, +): SafePageNode[] => { const wrapArea = getWrapArea(page); const contentArea = getContentArea(page); const dynamicPage = resolveDynamicPage({ pageNumber }, page, fontStore, yoga); @@ -198,7 +227,9 @@ const splitPage = (page, pageNumber, fontStore, yoga) => { dynamicPage.children, ); - const relayout = (node) => relayoutPage(node, fontStore, yoga); + const relayout = (node: SafePageNode): SafePageNode => + // @ts-expect-error rework pagination + relayoutPage(node, fontStore, yoga) as SafePageNode; const currentBox = { ...page.box, height }; const currentPage = relayout( @@ -247,7 +278,12 @@ const dissocSubPageData = (page) => { return omit(['subPageNumber', 'subPageTotalPages'], page); }; -const paginate = (page, pageNumber, fontStore, yoga) => { +const paginate = ( + page: SafePageNode, + pageNumber: number, + fontStore: FontStore, + yoga: YogaInstance, +) => { if (!page) return []; if (page.props?.wrap === false) return [page]; @@ -276,17 +312,20 @@ const paginate = (page, pageNumber, fontStore, yoga) => { * Performs pagination. This is the step responsible of breaking the whole document * into pages following pagiation rules, such as `fixed`, `break` and dynamic nodes. * - * @param {Object} doc node - * @param {Object} fontStore font store - * @returns {Object} layout node + * @param root - Document node + * @param fontStore - Font store + * @returns Layout node */ -const resolvePagination = (doc, fontStore) => { +const resolvePagination = ( + root: SafeDocumentNode, + fontStore: FontStore, +): SafeDocumentNode => { let pages = []; let pageNumber = 1; - for (let i = 0; i < doc.children.length; i += 1) { - const page = doc.children[i]; - let subpages = paginate(page, pageNumber, fontStore, doc.yoga); + for (let i = 0; i < root.children.length; i += 1) { + const page = root.children[i]; + let subpages = paginate(page, pageNumber, fontStore, root.yoga); subpages = assocSubPageData(subpages); pageNumber += subpages.length; @@ -294,10 +333,10 @@ const resolvePagination = (doc, fontStore) => { } pages = pages.map((...args) => - dissocSubPageData(resolvePageIndices(fontStore, doc.yoga, ...args)), + dissocSubPageData(resolvePageIndices(fontStore, root.yoga, ...args)), ); - return assingChildren(pages, doc); + return assingChildren(pages, root); }; export default resolvePagination; diff --git a/packages/layout/src/steps/resolvePercentHeight.js b/packages/layout/src/steps/resolvePercentHeight.ts similarity index 57% rename from packages/layout/src/steps/resolvePercentHeight.js rename to packages/layout/src/steps/resolvePercentHeight.ts index 5d79daa1b..228f361f7 100644 --- a/packages/layout/src/steps/resolvePercentHeight.js +++ b/packages/layout/src/steps/resolvePercentHeight.ts @@ -1,12 +1,14 @@ import { isNil, matchPercent } from '@react-pdf/fns'; +import { SafeDocumentNode, SafeNode, SafePageNode } from '../types'; + /** * Transform percent height into fixed * - * @param {number} height - * @returns {number} height + * @param height + * @returns Height */ -const transformHeight = (pageArea, height) => { +const transformHeight = (pageArea: number, height: number | string) => { const match = matchPercent(height); return match ? match.percent * pageArea : height; }; @@ -14,13 +16,13 @@ const transformHeight = (pageArea, height) => { /** * Get page area (height minus paddings) * - * @param {Object} page - * @returns {number} page area + * @param page + * @returns Page area */ -const getPageArea = (page) => { - const pageHeight = page.style.height; - const pagePaddingTop = page.style?.paddingTop || 0; - const pagePaddingBottom = page.style?.paddingBottom || 0; +const getPageArea = (page: SafePageNode) => { + const pageHeight = page.style.height as number; + const pagePaddingTop = (page.style?.paddingTop || 0) as number; + const pagePaddingBottom = (page.style?.paddingBottom || 0) as number; return pageHeight - pagePaddingTop - pagePaddingBottom; }; @@ -28,11 +30,14 @@ const getPageArea = (page) => { /** * Transform node percent height to fixed * - * @param {Object} page - * @param {Object} node - * @returns {Object} transformed node + * @param page + * @param node + * @returns Transformed node */ -const resolveNodePercentHeight = (page, node) => { +const resolveNodePercentHeight = ( + page: SafePageNode, + node: SafeNode, +): SafeNode => { if (isNil(page.style?.height)) return node; if (isNil(node.style?.height)) return node; @@ -46,13 +51,15 @@ const resolveNodePercentHeight = (page, node) => { /** * Transform page immediate children with percent height to fixed * - * @param {Object} page - * @returns {Object} transformed page + * @param page + * @returns Transformed page */ -const resolvePagePercentHeight = (page) => { +const resolvePagePercentHeight = (page: SafePageNode) => { if (!page.children) return page; - const resolveChild = (child) => resolveNodePercentHeight(page, child); + const resolveChild = (child: SafeNode) => + resolveNodePercentHeight(page, child); + const children = page.children.map(resolveChild); return Object.assign({}, page, { children }); @@ -62,10 +69,10 @@ const resolvePagePercentHeight = (page) => { * Transform all page immediate children with percent height to fixed. * This is needed for computing correct dimensions on pre-pagination layout. * - * @param {Object} root document root - * @returns {Object} transformed document root + * @param root - Document root + * @returns Transformed document root */ -const resolvePercentHeight = (root) => { +const resolvePercentHeight = (root: SafeDocumentNode) => { if (!root.children) return root; const children = root.children.map(resolvePagePercentHeight); diff --git a/packages/layout/src/steps/resolvePercentRadius.js b/packages/layout/src/steps/resolvePercentRadius.ts similarity index 58% rename from packages/layout/src/steps/resolvePercentRadius.js rename to packages/layout/src/steps/resolvePercentRadius.ts index ba38b0e8c..18049714a 100644 --- a/packages/layout/src/steps/resolvePercentRadius.js +++ b/packages/layout/src/steps/resolvePercentRadius.ts @@ -1,33 +1,21 @@ import { evolve, matchPercent } from '@react-pdf/fns'; +import { Box, SafeNode } from '../types'; -/** - * @typedef {Function} ResolveRadius - * @param {string | number} value border radius value - * @returns {number} resolved radius value - */ - -/** - * - * @param {{ width: number, height: number }} container width and height - * @returns {ResolveRadius} resolve radius function - */ -const resolveRadius = (container) => (value) => { +const resolveRadius = (box: Box) => (value: number | `${string}%`) => { if (!value) return undefined; const match = matchPercent(value); - return match - ? match.percent * Math.min(container.width, container.height) - : value; + return match ? match.percent * Math.min(box.width, box.height) : value; }; /** * Transforms percent border radius into fixed values * - * @param {Object} node - * @returns {Object} node + * @param node + * @returns Node */ -const resolvePercentRadius = (node) => { +const resolvePercentRadius = (node: SafeNode): SafeNode => { const style = evolve( { borderTopLeftRadius: resolveRadius(node.box), diff --git a/packages/layout/src/steps/resolveSvg.js b/packages/layout/src/steps/resolveSvg.ts similarity index 51% rename from packages/layout/src/steps/resolveSvg.js rename to packages/layout/src/steps/resolveSvg.ts index 2f4113d8d..1bf9ac0fd 100644 --- a/packages/layout/src/steps/resolveSvg.js +++ b/packages/layout/src/steps/resolveSvg.ts @@ -1,6 +1,14 @@ import * as P from '@react-pdf/primitives'; +import FontStore from '@react-pdf/font'; import resolveStyle, { transformColor } from '@react-pdf/stylesheet'; -import { pick, evolve, compose, mapValues, matchPercent } from '@react-pdf/fns'; +import { + pick, + evolve, + compose, + mapValues, + matchPercent, + parseFloat, +} from '@react-pdf/fns'; import layoutText from '../svg/layoutText'; import replaceDefs from '../svg/replaceDefs'; @@ -8,6 +16,15 @@ import getContainer from '../svg/getContainer'; import parseViewbox from '../svg/parseViewbox'; import inheritProps from '../svg/inheritProps'; import parseAspectRatio from '../svg/parseAspectRatio'; +import { + SafeNode, + SafeSvgNode, + SafeTextInstanceNode, + SafeTextNode, + SafeTspanNode, +} from '../types'; + +type Container = { width: number; height: number }; const STYLE_PROPS = [ 'width', @@ -31,15 +48,14 @@ const STYLE_PROPS = [ const VERTICAL_PROPS = ['y', 'y1', 'y2', 'height', 'cy', 'ry']; const HORIZONTAL_PROPS = ['x', 'x1', 'x2', 'width', 'cx', 'rx']; -const isType = (type) => (node) => node.type === type; - -const isSvg = isType(P.Svg); +const isSvg = (node: SafeNode): node is SafeSvgNode => node.type === P.Svg; -const isText = isType(P.Text); +const isText = (node: SafeNode): node is SafeTextNode => node.type === P.Text; -const isTextInstance = isType(P.TextInstance); +const isTextInstance = (node: SafeNode): node is SafeTextInstanceNode => + node.type === P.TextInstance; -const transformPercent = (container) => (props) => +const transformPercent = (container: Container) => (props) => mapValues(props, (value, key) => { const match = matchPercent(value); @@ -59,57 +75,59 @@ const parsePercent = (value) => { return match ? match.percent : parseFloat(value); }; -const parseTransform = (container) => (value) => { +const parseTransform = (container: Container) => (value) => { return resolveStyle(container, { transform: value }).transform; }; -const parseProps = (container) => (node) => { - let props = transformPercent(container)(node.props); - - props = evolve( - { - x: parseFloat, - x1: parseFloat, - x2: parseFloat, - y: parseFloat, - y1: parseFloat, - y2: parseFloat, - r: parseFloat, - rx: parseFloat, - ry: parseFloat, - cx: parseFloat, - cy: parseFloat, - width: parseFloat, - height: parseFloat, - offset: parsePercent, - fill: transformColor, - opacity: parsePercent, - stroke: transformColor, - stopOpacity: parsePercent, - stopColor: transformColor, - transform: parseTransform(container), - }, - props, - ); - - return Object.assign({}, node, { props }); -}; - -const mergeStyles = (node) => { +const parseProps = + (container: Container) => + (node: SafeNode): SafeNode => { + let props = transformPercent(container)(node.props); + + props = evolve( + { + x: parseFloat, + x1: parseFloat, + x2: parseFloat, + y: parseFloat, + y1: parseFloat, + y2: parseFloat, + r: parseFloat, + rx: parseFloat, + ry: parseFloat, + cx: parseFloat, + cy: parseFloat, + width: parseFloat, + height: parseFloat, + offset: parsePercent, + fill: transformColor, + opacity: parsePercent, + stroke: transformColor, + stopOpacity: parsePercent, + stopColor: transformColor, + transform: parseTransform(container), + }, + props, + ); + + return Object.assign({}, node, { props }); + }; + +const mergeStyles = (node: SafeNode): SafeNode => { const style = node.style || {}; const props = Object.assign({}, style, node.props); return Object.assign({}, node, { props }); }; -const removeNoneValues = (node) => { +const removeNoneValues = (node: SafeNode): SafeNode => { const removeNone = (value) => (value === 'none' ? null : value); const props = mapValues(node.props, removeNone); return Object.assign({}, node, { props }); }; -const pickStyleProps = (node) => { +const pickStyleProps = (node: SafeNode): SafeNode => { const props = node.props || {}; const styleProps = pick(STYLE_PROPS, props); const style = Object.assign({}, styleProps, node.style || {}); @@ -117,7 +135,7 @@ const pickStyleProps = (node) => { return Object.assign({}, node, { style }); }; -const parseSvgProps = (node) => { +const parseSvgProps = (node: SafeSvgNode) => { const props = evolve( { width: parseFloat, @@ -131,13 +149,14 @@ const parseSvgProps = (node) => { return Object.assign({}, node, { props }); }; -const wrapBetweenTspan = (node) => ({ +const wrapBetweenTspan = (node: SafeTextInstanceNode): SafeTspanNode => ({ type: P.Tspan, props: {}, + style: {}, children: [node], }); -const addMissingTspan = (node) => { +const addMissingTspan = (node: SafeNode): SafeNode => { if (!isText(node)) return node; if (!node.children) return node; @@ -149,17 +168,19 @@ const addMissingTspan = (node) => { return Object.assign({}, node, { children }); }; -const parseText = (fontStore) => (node) => { - if (isText(node)) return layoutText(fontStore, node); +const parseText = + (fontStore: FontStore) => + (node: SafeNode): SafeNode => { + if (isText(node)) return layoutText(fontStore, node); - if (!node.children) return node; + if (!node.children) return node; - const children = node.children.map(parseText(fontStore)); + const children = node.children.map(parseText(fontStore)); - return Object.assign({}, node, { children }); -}; + return Object.assign({}, node, { children }); + }; -const resolveSvgNode = (container) => +const resolveSvgNode = (container: Container) => compose( parseProps(container), addMissingTspan, @@ -167,20 +188,22 @@ const resolveSvgNode = (container) => mergeStyles, ); -const resolveChildren = (container) => (node) => { - if (!node.children) return node; +const resolveChildren = + (container: Container) => + (node: SafeNode): SafeNode => { + if (!node.children) return node; - const resolveChild = compose( - resolveChildren(container), - resolveSvgNode(container), - ); + const resolveChild = compose( + resolveChildren(container), + resolveSvgNode(container), + ); - const children = node.children.map(resolveChild); + const children = node.children.map(resolveChild); - return Object.assign({}, node, { children }); -}; + return Object.assign({}, node, { children }); + }; -const resolveSvgRoot = (node, fontStore) => { +const resolveSvgRoot = (node: SafeSvgNode, fontStore: FontStore) => { const container = getContainer(node); return compose( @@ -196,17 +219,16 @@ const resolveSvgRoot = (node, fontStore) => { /** * Pre-process SVG nodes so they can be rendered in the next steps * - * @param {Object} node root node - * @param {Object} fontStore font store - * @returns {Object} root node + * @param node - Root node + * @param fontStore - Font store + * @returns Root node */ -const resolveSvg = (node, fontStore) => { - if (!node.children) return node; +const resolveSvg = (node: SafeNode, fontStore: FontStore) => { + if (!('children' in node)) return node; const resolveChild = (child) => resolveSvg(child, fontStore); const root = isSvg(node) ? resolveSvgRoot(node, fontStore) : node; - - const children = root.children.map(resolveChild); + const children = root.children?.map(resolveChild); return Object.assign({}, root, { children }); }; diff --git a/packages/layout/src/steps/resolveTextLayout.js b/packages/layout/src/steps/resolveTextLayout.ts similarity index 56% rename from packages/layout/src/steps/resolveTextLayout.js rename to packages/layout/src/steps/resolveTextLayout.ts index ef34f7d8f..991fda953 100644 --- a/packages/layout/src/steps/resolveTextLayout.js +++ b/packages/layout/src/steps/resolveTextLayout.ts @@ -1,26 +1,27 @@ import * as P from '@react-pdf/primitives'; +import FontStore from '@react-pdf/font'; import layoutText from '../text/layoutText'; +import { SafeNode, SafeSvgNode, SafeTextNode } from '../types'; -const isType = (type) => (node) => node.type === type; +const isSvg = (node: SafeNode): node is SafeSvgNode => node.type === P.Svg; -const isSvg = isType(P.Svg); +const isText = (node: SafeNode): node is SafeTextNode => node.type === P.Text; -const isText = isType(P.Text); +const shouldIterate = (node: SafeNode) => !isSvg(node) && !isText(node); -const shouldIterate = (node) => !isSvg(node) && !isText(node); - -const shouldLayoutText = (node) => isText(node) && !node.lines; +const shouldLayoutText = (node: SafeNode): node is SafeTextNode => + isText(node) && !node.lines; /** * Performs text layout on text node if wasn't calculated before. * Text layout is usually performed on Yoga's layout process (via setMeasureFunc), * but we need to layout those nodes with fixed width and height. * - * @param {Object} node - * @returns {Object} layout node + * @param node + * @returns Layout node */ -const resolveTextLayout = (node, fontStore) => { +const resolveTextLayout = (node: SafeNode, fontStore: FontStore): SafeNode => { if (shouldLayoutText(node)) { const width = node.box.width - (node.box.paddingRight + node.box.paddingLeft); @@ -33,7 +34,7 @@ const resolveTextLayout = (node, fontStore) => { if (shouldIterate(node)) { if (!node.children) return node; - const mapChild = (child) => resolveTextLayout(child, fontStore); + const mapChild = (child: SafeNode) => resolveTextLayout(child, fontStore); const children = node.children.map(mapChild); diff --git a/packages/layout/src/steps/resolveYoga.ts b/packages/layout/src/steps/resolveYoga.ts index c3f92badb..23ded11fd 100644 --- a/packages/layout/src/steps/resolveYoga.ts +++ b/packages/layout/src/steps/resolveYoga.ts @@ -1,7 +1,7 @@ import { loadYoga } from '../yoga/index'; import { DocumentNode } from '../types'; -const resolveYoga = async (root: DocumentNode) => { +const resolveYoga = async (root: DocumentNode): Promise => { const yoga = await loadYoga(); return Object.assign({}, root, { yoga }); diff --git a/packages/layout/src/steps/resolveZIndex.js b/packages/layout/src/steps/resolveZIndex.js deleted file mode 100644 index c6b84c2c3..000000000 --- a/packages/layout/src/steps/resolveZIndex.js +++ /dev/null @@ -1,36 +0,0 @@ -import * as P from '@react-pdf/primitives'; - -const getZIndex = (node) => node.style.zIndex; - -const shouldSort = (node) => node.type !== P.Document && node.type !== P.Svg; - -const sortZIndex = (a, b) => { - const za = getZIndex(a); - const zb = getZIndex(b); - - if (!za && !zb) return 0; - if (!za) return 1; - if (!zb) return -1; - - return zb - za; -}; - -/** - * Sort children by zIndex value - * - * @param {Object} node - * @returns {Object} node - */ -const resolveZIndex = (node) => { - if (!node.children) return node; - - const sortedChildren = shouldSort(node) - ? node.children.sort(sortZIndex) - : node.children; - - const children = sortedChildren.map(resolveZIndex); - - return Object.assign({}, node, { children }); -}; - -export default resolveZIndex; diff --git a/packages/layout/src/steps/resolveZIndex.ts b/packages/layout/src/steps/resolveZIndex.ts new file mode 100644 index 000000000..c2deb076d --- /dev/null +++ b/packages/layout/src/steps/resolveZIndex.ts @@ -0,0 +1,47 @@ +import * as P from '@react-pdf/primitives'; + +import { SafeDocumentNode, SafeNode } from '../types'; + +const getZIndex = (node: SafeNode) => node.style.zIndex; + +const shouldSort = (node: SafeNode) => + node.type !== P.Document && node.type !== P.Svg; + +const sortZIndex = (a: SafeNode, b: SafeNode) => { + const za = getZIndex(a); + const zb = getZIndex(b); + + if (!za && !zb) return 0; + if (!za) return 1; + if (!zb) return -1; + + return zb - za; +}; + +/** + * Sort children by zIndex value + * + * @param node + * @returns Node + */ +const resolveNodeZIndex = (node: T): T => { + if (!node.children) return node; + + const sortedChildren = shouldSort(node) + ? node.children.sort(sortZIndex) + : node.children; + + const children = sortedChildren.map(resolveNodeZIndex); + + return Object.assign({}, node, { children }); +}; + +/** + * Sort children by zIndex value + * + * @param node + * @returns Node + */ +const resolveZIndex = (root: SafeDocumentNode) => resolveNodeZIndex(root); + +export default resolveZIndex; diff --git a/packages/layout/src/svg/getContainer.js b/packages/layout/src/svg/getContainer.ts similarity index 75% rename from packages/layout/src/svg/getContainer.js rename to packages/layout/src/svg/getContainer.ts index 39c2ad690..e7b396da7 100644 --- a/packages/layout/src/svg/getContainer.js +++ b/packages/layout/src/svg/getContainer.ts @@ -1,6 +1,9 @@ +import { parseFloat } from '@react-pdf/fns'; + import parseViewBox from './parseViewbox'; +import { SafeSvgNode } from '../types'; -const getContainer = (node) => { +const getContainer = (node: SafeSvgNode) => { const viewbox = parseViewBox(node.props.viewBox); if (viewbox) { diff --git a/packages/layout/src/svg/getDefs.js b/packages/layout/src/svg/getDefs.js deleted file mode 100644 index 0a3690e7a..000000000 --- a/packages/layout/src/svg/getDefs.js +++ /dev/null @@ -1,17 +0,0 @@ -import * as P from '@react-pdf/primitives'; - -const isDefs = (node) => node.type === P.Defs; - -const getDefs = (node) => { - const children = node.children || []; - const defs = children.find(isDefs) || {}; - const values = defs.children || []; - - return values.reduce((acc, value) => { - const id = value.props?.id; - if (id) acc[id] = value; - return acc; - }, {}); -}; - -export default getDefs; diff --git a/packages/layout/src/svg/getDefs.ts b/packages/layout/src/svg/getDefs.ts new file mode 100644 index 000000000..877ee44c6 --- /dev/null +++ b/packages/layout/src/svg/getDefs.ts @@ -0,0 +1,19 @@ +import * as P from '@react-pdf/primitives'; + +import { SafeDefs, SafeDefsNode, SafeNode, SafeSvgNode } from '../types'; + +const isDefs = (node: SafeNode): node is SafeDefsNode => node.type === P.Defs; + +const getDefs = (node: SafeSvgNode) => { + const children = node.children || []; + const defs = children.find(isDefs); + const values = defs?.children || []; + + return values.reduce((acc: SafeDefs, value) => { + const id = value.props?.id; + if (id) acc[id] = value; + return acc; + }, {}); +}; + +export default getDefs; diff --git a/packages/layout/src/svg/inheritProps.js b/packages/layout/src/svg/inheritProps.ts similarity index 94% rename from packages/layout/src/svg/inheritProps.js rename to packages/layout/src/svg/inheritProps.ts index 690f2fa53..0684bce65 100644 --- a/packages/layout/src/svg/inheritProps.js +++ b/packages/layout/src/svg/inheritProps.ts @@ -1,5 +1,6 @@ import * as P from '@react-pdf/primitives'; import { pick, without } from '@react-pdf/fns'; +import { SafeNode } from '../types'; const BASE_SVG_INHERITED_PROPS = [ 'x', @@ -47,7 +48,7 @@ const getInheritProps = (node) => { return pick(svgInheritedProps, props); }; -const inheritProps = (node) => { +const inheritProps = (node: SafeNode) => { if (!node.children) return node; const inheritedProps = getInheritProps(node); diff --git a/packages/layout/src/svg/layoutText.js b/packages/layout/src/svg/layoutText.ts similarity index 77% rename from packages/layout/src/svg/layoutText.js rename to packages/layout/src/svg/layoutText.ts index cc2cd287a..45358765f 100644 --- a/packages/layout/src/svg/layoutText.js +++ b/packages/layout/src/svg/layoutText.ts @@ -1,4 +1,5 @@ import * as P from '@react-pdf/primitives'; +import FontStore from '@react-pdf/font'; import layoutEngine, { bidi, linebreaker, @@ -6,13 +7,25 @@ import layoutEngine, { scriptItemizer, wordHyphenation, textDecoration, + fromFragments, + Fragment, + Font, } from '@react-pdf/textkit'; -import fromFragments from '../text/fromFragments'; import transformText from '../text/transformText'; import fontSubstitution from '../text/fontSubstitution'; +import { + SafeNode, + SafeTextNode, + SafeTspanNode, + SafeTextInstanceNode, +} from '../types'; -const isTextInstance = (node) => node.type === P.TextInstance; +const isTspan = (node: SafeNode): node is SafeTspanNode => + node.type === P.Tspan; + +const isTextInstance = (node: SafeNode): node is SafeTextInstanceNode => + node.type === P.TextInstance; const engines = { bidi, @@ -26,10 +39,10 @@ const engines = { const engine = layoutEngine(engines); -const getFragments = (fontStore, instance) => { +const getFragments = (fontStore: FontStore, instance) => { if (!instance) return [{ string: '' }]; - const fragments = []; + const fragments: Fragment[] = []; const { fill = 'black', @@ -48,13 +61,14 @@ const getFragments = (fontStore, instance) => { const fontFamilies = typeof fontFamily === 'string' ? [fontFamily] : [...(fontFamily || [])]; + // TODO: Fix multiple fonts passed const font = fontFamilies.map((fontFamilyName) => { if (typeof fontFamilyName !== 'string') return fontFamilyName; const opts = { fontFamily: fontFamilyName, fontWeight, fontStyle }; - const obj = fontStore ? fontStore.getFont(opts) : null; + const obj = fontStore.getFont(opts); return obj ? obj.data : fontFamilyName; - }); + }) as Font[]; const attributes = { font, @@ -81,24 +95,25 @@ const getFragments = (fontStore, instance) => { if (isTextInstance(child)) { fragments.push({ string: transformText(child.value, textTransform), + // @ts-expect-error custom font substitution engine deals with multiple fonts. unify with textkit attributes, }); } else if (child) { - fragments.push(...getFragments(child)); + fragments.push(...getFragments(fontStore, child)); } } return fragments; }; -const getAttributedString = (fontStore, instance) => +const getAttributedString = (fontStore: FontStore, instance) => fromFragments(getFragments(fontStore, instance)); const AlmostInfinity = 999999999999; const shrinkWhitespaceFactor = { before: -0.5, after: -0.5 }; -const layoutTspan = (fontStore) => (node, xOffset) => { +const layoutTspan = (fontStore: FontStore) => (node, xOffset) => { const attributedString = getAttributedString(fontStore, node); const x = node.props.x === undefined ? xOffset : node.props.x; @@ -117,9 +132,11 @@ const layoutTspan = (fontStore) => (node, xOffset) => { return Object.assign({}, node, { lines }); }; -// Consecutive elements should be joined with a space -const joinTSpanLines = (node) => { +// Consecutive TSpan elements should be joined with a space +const joinTSpanLines = (node: SafeTextNode) => { const children = node.children.map((child, index) => { + if (!isTspan(child)) return child; + const textInstance = child.children[0]; if ( @@ -138,7 +155,7 @@ const joinTSpanLines = (node) => { return Object.assign({}, node, { children }); }; -const layoutText = (fontStore, node) => { +const layoutText = (fontStore: FontStore, node: SafeTextNode) => { if (!node.children) return node; let currentXOffset = node.props?.x || 0; diff --git a/packages/layout/src/svg/measureSvg.js b/packages/layout/src/svg/measureSvg.ts similarity index 59% rename from packages/layout/src/svg/measureSvg.js rename to packages/layout/src/svg/measureSvg.ts index 65de8c0a1..4c9794588 100644 --- a/packages/layout/src/svg/measureSvg.js +++ b/packages/layout/src/svg/measureSvg.ts @@ -1,28 +1,24 @@ import * as Yoga from 'yoga-layout/load'; -const getAspectRatio = (viewbox) => { +import { SafePageNode, SafeSvgNode, Viewbox } from '../types'; + +const getAspectRatio = (viewbox: string | Viewbox) => { if (!viewbox) return null; + if (typeof viewbox === 'string') return null; + return (viewbox.maxX - viewbox.minX) / (viewbox.maxY - viewbox.minY); }; -/** - * @typedef {Function} MeasureSvg - * @param {number} width - * @param {number} widthMode - * @param {number} height - * @param {number} heightMode - * @returns {{ width: number, height: number }} svg width and height - */ - /** * Yoga svg measure function * - * @param {Object} page - * @param {Object} node - * @returns {MeasureSvg} measure svg + * @param page + * @param node + * @returns Measure svg */ const measureCanvas = - (page, node) => (width, widthMode, height, heightMode) => { + (page: SafePageNode, node: SafeSvgNode): Yoga.MeasureFunction => + (width, widthMode, height, heightMode) => { const aspectRatio = getAspectRatio(node.props.viewBox) || 1; if ( diff --git a/packages/layout/src/svg/parseAspectRatio.js b/packages/layout/src/svg/parseAspectRatio.js deleted file mode 100644 index 12a5e3261..000000000 --- a/packages/layout/src/svg/parseAspectRatio.js +++ /dev/null @@ -1,13 +0,0 @@ -const parseAspectRatio = (value) => { - const match = value - .replace(/[\s\r\t\n]+/gm, ' ') - .replace(/^defer\s/, '') - .split(' '); - - const align = match[0] || 'xMidYMid'; - const meetOrSlice = match[1] || 'meet'; - - return { align, meetOrSlice }; -}; - -export default parseAspectRatio; diff --git a/packages/layout/src/svg/parseAspectRatio.ts b/packages/layout/src/svg/parseAspectRatio.ts new file mode 100644 index 000000000..1cf27c94f --- /dev/null +++ b/packages/layout/src/svg/parseAspectRatio.ts @@ -0,0 +1,19 @@ +import { PreserveAspectRatio } from '../types'; + +const parseAspectRatio = (value: string | PreserveAspectRatio) => { + if (typeof value !== 'string') return value; + + const match = value + .replace(/[\s\r\t\n]+/gm, ' ') + .replace(/^defer\s/, '') + .split(' '); + + const align = (match[0] || 'xMidYMid') as PreserveAspectRatio['align']; + + const meetOrSlice = (match[1] || + 'meet') as PreserveAspectRatio['meetOrSlice']; + + return { align, meetOrSlice }; +}; + +export default parseAspectRatio; diff --git a/packages/layout/src/svg/parseViewbox.js b/packages/layout/src/svg/parseViewbox.ts similarity index 56% rename from packages/layout/src/svg/parseViewbox.js rename to packages/layout/src/svg/parseViewbox.ts index 610149126..4a482b849 100644 --- a/packages/layout/src/svg/parseViewbox.js +++ b/packages/layout/src/svg/parseViewbox.ts @@ -1,7 +1,14 @@ -const parseViewbox = (value) => { +import { parseFloat } from '@react-pdf/fns'; +import { Viewbox } from '../types'; + +const parseViewbox = (value?: string | Viewbox) => { if (!value) return null; + if (typeof value !== 'string') return value; + const values = value.split(/[,\s]+/).map(parseFloat); + if (values.length !== 4) return null; + return { minX: values[0], minY: values[1], maxX: values[2], maxY: values[3] }; }; diff --git a/packages/layout/src/svg/replaceDefs.js b/packages/layout/src/svg/replaceDefs.js deleted file mode 100644 index fbaa3732d..000000000 --- a/packages/layout/src/svg/replaceDefs.js +++ /dev/null @@ -1,51 +0,0 @@ -import * as P from '@react-pdf/primitives'; - -import getDefs from './getDefs'; - -const isNotDefs = (node) => node.type !== P.Defs; - -const detachDefs = (node) => { - if (!node.children) return node; - - const children = node.children.filter(isNotDefs); - - return Object.assign({}, node, { children }); -}; - -const URL_REGEX = /url\(['"]?#([^'"]+)['"]?\)/; - -const replaceDef = (defs, value) => { - if (!value) return undefined; - - if (!URL_REGEX.test(value)) return value; - - const match = value.match(URL_REGEX); - - return defs[match[1]]; -}; - -const parseNodeDefs = (defs) => (node) => { - const fill = replaceDef(defs, node.props?.fill); - const clipPath = replaceDef(defs, node.props?.clipPath); - const props = Object.assign({}, node.props, { fill, clipPath }); - const children = node.children - ? node.children.map(parseNodeDefs(defs)) - : undefined; - - return Object.assign({}, node, { props, children }); -}; - -const parseDefs = (root) => { - if (!root.children) return root; - - const defs = getDefs(root); - const children = root.children.map(parseNodeDefs(defs)); - - return Object.assign({}, root, { children }); -}; - -const replaceDefs = (node) => { - return detachDefs(parseDefs(node)); -}; - -export default replaceDefs; diff --git a/packages/layout/src/svg/replaceDefs.ts b/packages/layout/src/svg/replaceDefs.ts new file mode 100644 index 000000000..d2edecdcf --- /dev/null +++ b/packages/layout/src/svg/replaceDefs.ts @@ -0,0 +1,60 @@ +import * as P from '@react-pdf/primitives'; + +import getDefs from './getDefs'; +import { SafeDefs, SafeNode, SafeSvgNode } from '../types'; + +const isNotDefs = (node: SafeNode) => node.type !== P.Defs; + +const detachDefs = (node: SafeSvgNode) => { + if (!node.children) return node; + + const children = node.children.filter(isNotDefs); + + return Object.assign({}, node, { children }); +}; + +const URL_REGEX = /url\(['"]?#([^'"]+)['"]?\)/; + +const replaceDef = (defs: SafeDefs, value: string) => { + if (!value) return undefined; + + if (!URL_REGEX.test(value)) return value; + + const match = value.match(URL_REGEX); + + return defs[match[1]]; +}; + +const parseNodeDefs = + (defs: SafeDefs) => + (node: SafeNode): SafeNode => { + const props = node.props; + + const fill = `fill` in props ? replaceDef(defs, props?.fill) : undefined; + + const clipPath = + `clipPath` in props ? replaceDef(defs, props?.clipPath) : undefined; + + const newProps = Object.assign({}, node.props, { fill, clipPath }); + + const children = node.children + ? node.children.map(parseNodeDefs(defs)) + : undefined; + + return Object.assign({}, node, { props: newProps, children }); + }; + +const parseDefs = (root: SafeSvgNode) => { + if (!root.children) return root; + + const defs = getDefs(root); + const children = root.children.map(parseNodeDefs(defs)); + + return Object.assign({}, root, { children }); +}; + +const replaceDefs = (node: SafeSvgNode) => { + return detachDefs(parseDefs(node)); +}; + +export default replaceDefs; diff --git a/packages/layout/src/text/emoji.js b/packages/layout/src/text/emoji.ts similarity index 71% rename from packages/layout/src/text/emoji.js rename to packages/layout/src/text/emoji.ts index 1a1cea66d..8cda671c0 100644 --- a/packages/layout/src/text/emoji.js +++ b/packages/layout/src/text/emoji.ts @@ -1,21 +1,13 @@ import emojiRegex from 'emoji-regex'; import resolveImage from '@react-pdf/image'; +import { Fragment } from '@react-pdf/textkit'; + +import { EmojiSource } from '../../../types'; // Caches emoji images data const emojis = {}; const regex = emojiRegex(); -const reflect = - (promise) => - (...args) => - promise(...args).then( - (v) => v, - (e) => e, - ); - -// Returns a function to be able to mock resolveImage. -const makeFetchEmojiImage = () => reflect(resolveImage); - /** * When an emoji as no variations, it might still have 2 parts, * the canonical emoji and an empty string. @@ -26,25 +18,30 @@ const makeFetchEmojiImage = () => reflect(resolveImage); * The empty string needs to be removed otherwise the generated * url will be incorect. */ -const _removeVariationSelectors = (x) => x !== '️'; +const removeVariationSelectors = (x: string) => x !== '️'; -const getCodePoints = (string, withVariationSelectors) => +const getCodePoints = ( + string: string, + withVariationSelectors: boolean = false, +) => Array.from(string) - .filter(withVariationSelectors ? () => true : _removeVariationSelectors) + .filter(withVariationSelectors ? () => true : removeVariationSelectors) .map((char) => char.codePointAt(0).toString(16)) .join('-'); -const buildEmojiUrl = (emoji, source) => { - const { url, format, builder, withVariationSelectors } = source; - if (typeof builder === 'function') { - return builder(getCodePoints(emoji, withVariationSelectors)); +const buildEmojiUrl = (emoji: string, source: EmojiSource) => { + console.log(source); + + if ('builder' in source) { + return source.builder(getCodePoints(emoji, source.withVariationSelectors)); } + const { url, format = 'png', withVariationSelectors } = source; return `${url}${getCodePoints(emoji, withVariationSelectors)}.${format}`; }; -export const fetchEmojis = (string, source) => { - if (!source || (!source.url && !source.builder)) return []; +export const fetchEmojis = (string: string, source?: EmojiSource) => { + if (!source) return []; const promises = []; @@ -55,9 +52,9 @@ export const fetchEmojis = (string, source) => { const emojiUrl = buildEmojiUrl(emoji, source); emojis[emoji] = { loading: true }; - const fetchEmojiImage = makeFetchEmojiImage(); + promises.push( - fetchEmojiImage({ uri: emojiUrl }).then((image) => { + resolveImage({ uri: emojiUrl }).then((image) => { emojis[emoji].loading = false; emojis[emoji].data = image.data; }), @@ -68,8 +65,8 @@ export const fetchEmojis = (string, source) => { return promises; }; -export const embedEmojis = (fragments) => { - const result = []; +export const embedEmojis = (fragments: Fragment[]) => { + const result: Fragment[] = []; for (let i = 0; i < fragments.length; i += 1) { const fragment = fragments[i]; @@ -86,7 +83,7 @@ export const embedEmojis = (fragments) => { // correct attachment and object substitution character; if (emojis[emoji] && emojis[emoji].data) { result.push({ - string: chunk.replace(match, String.fromCharCode(0xfffc)), + string: chunk.replace(match[0], String.fromCharCode(0xfffc)), attributes: { ...fragment.attributes, attachment: { diff --git a/packages/layout/src/text/fontSubstitution.js b/packages/layout/src/text/fontSubstitution.ts similarity index 86% rename from packages/layout/src/text/fontSubstitution.js rename to packages/layout/src/text/fontSubstitution.ts index 40b8bafc8..88ac8dc49 100644 --- a/packages/layout/src/text/fontSubstitution.js +++ b/packages/layout/src/text/fontSubstitution.ts @@ -1,4 +1,5 @@ import { last } from '@react-pdf/fns'; +import { AttributedString, Run } from '@react-pdf/textkit'; import StandardFont from './standardFont'; @@ -6,9 +7,9 @@ const fontCache = {}; const IGNORED_CODE_POINTS = [173]; -const getFontSize = (node) => node.attributes.fontSize || 12; +const getFontSize = (run: Run) => run.attributes.fontSize || 12; -const getOrCreateFont = (name) => { +const getOrCreateFont = (name: string) => { if (fontCache[name]) return fontCache[name]; const font = new StandardFont(name); @@ -37,17 +38,18 @@ const pickFontFromFontStack = (codePoint, fontStack, lastFont) => { const fontSubstitution = () => - ({ string, runs }) => { + ({ string, runs }: AttributedString) => { let lastFont = null; let lastFontSize = null; let lastIndex = 0; let index = 0; - const res = []; + const res: Run[] = []; for (let i = 0; i < runs.length; i += 1) { const run = runs[i]; + // @ts-expect-error need to make textkit to accept multiple fonts const defaultFont = run.attributes.font.map((font) => typeof font === 'string' ? getOrCreateFont(font) : font, ); @@ -61,7 +63,7 @@ const fontSubstitution = for (let j = 0; j < chars.length; j += 1) { const char = chars[j]; - const codePoint = char.codePointAt(); + const codePoint = char.codePointAt(0); // If the default font does not have a glyph and the fallback font does, we use it const font = pickFontFromFontStack(codePoint, defaultFont, lastFont); const fontSize = getFontSize(run); @@ -105,7 +107,7 @@ const fontSubstitution = }); } - return { string, runs: res }; + return { string, runs: res } as AttributedString; }; export default fontSubstitution; diff --git a/packages/layout/src/text/fromFragments.js b/packages/layout/src/text/fromFragments.js deleted file mode 100644 index 501819928..000000000 --- a/packages/layout/src/text/fromFragments.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Create attributed string from text fragments - * - * @param {Object[]} fragments fragments - * @returns {Object} attributed string - */ -const fromFragments = (fragments) => { - let offset = 0; - let string = ''; - const runs = []; - - fragments.forEach((fragment) => { - string += fragment.string; - - runs.push({ - start: offset, - end: offset + fragment.string.length, - attributes: fragment.attributes || {}, - }); - - offset += fragment.string.length; - }); - - return { string, runs }; -}; - -export default fromFragments; diff --git a/packages/layout/src/text/getAttributedString.js b/packages/layout/src/text/getAttributedString.ts similarity index 68% rename from packages/layout/src/text/getAttributedString.js rename to packages/layout/src/text/getAttributedString.ts index 917daf7d7..7f007095e 100644 --- a/packages/layout/src/text/getAttributedString.js +++ b/packages/layout/src/text/getAttributedString.ts @@ -1,29 +1,44 @@ import * as P from '@react-pdf/primitives'; +import { Font, Fragment, fromFragments } from '@react-pdf/textkit'; +import FontStore from '@react-pdf/font'; import { embedEmojis } from './emoji'; import ignoreChars from './ignoreChars'; -import fromFragments from './fromFragments'; import transformText from './transformText'; +import { + SafeNode, + SafeTextNode, + SafeImageNode, + SafeTextInstanceNode, + SafeTspanNode, +} from '../types'; const PREPROCESSORS = [ignoreChars, embedEmojis]; -const isImage = (node) => node.type === P.Image; +const isImage = (node: SafeNode): node is SafeImageNode => + node.type === P.Image; -const isTextInstance = (node) => node.type === P.TextInstance; +const isTextInstance = (node: SafeNode): node is SafeTextInstanceNode => + node.type === P.TextInstance; /** * Get textkit fragments of given node object * - * @param {Object} fontStore font store - * @param {Object} instance node - * @param {string} [parentLink] parent link - * @param {number} [level] fragment level - * @returns {Object[]} text fragments + * @param fontStore - Font store + * @param instance - Node + * @param parentLink - Parent link + * @param level - Fragment level + * @returns Text fragments */ -const getFragments = (fontStore, instance, parentLink, level = 0) => { +const getFragments = ( + fontStore: FontStore, + instance: SafeTextNode | SafeTspanNode, + parentLink = null, + level = 0, +) => { if (!instance) return [{ string: '' }]; - let fragments = []; + let fragments: Fragment[] = []; const { color = 'black', @@ -47,13 +62,14 @@ const getFragments = (fontStore, instance, parentLink, level = 0) => { const fontFamilies = typeof fontFamily === 'string' ? [fontFamily] : [...(fontFamily || [])]; + // TODO: Fix multiple fonts passed const font = fontFamilies.map((fontFamilyName) => { if (typeof fontFamilyName !== 'string') return fontFamilyName; const opts = { fontFamily: fontFamilyName, fontWeight, fontStyle }; - const obj = fontStore ? fontStore.getFont(opts) : null; + const obj = fontStore.getFont(opts); return obj ? obj.data : fontFamilyName; - }); + }) as Font[]; // Don't pass main background color to textkit. Will be rendered by the render package instead const backgroundColor = level === 0 ? null : instance.style.backgroundColor; @@ -81,6 +97,7 @@ const getFragments = (fontStore, instance, parentLink, level = 0) => { textDecoration === 'line-through underline', strikeColor: textDecorationColor || color, underlineColor: textDecorationColor || color, + // @ts-expect-error allow this props access link: parentLink || instance.props?.src || instance.props?.href, align: textAlign || (direction === 'rtl' ? 'right' : 'left'), }; @@ -91,11 +108,12 @@ const getFragments = (fontStore, instance, parentLink, level = 0) => { if (isImage(child)) { fragments.push({ string: String.fromCharCode(0xfffc), + // @ts-expect-error custom font substitution engine deals with multiple fonts. unify with textkit attributes: { ...attributes, attachment: { - width: child.style.width || fontSize, - height: child.style.height || fontSize, + width: (child.style.width || fontSize) as number, + height: (child.style.height || fontSize) as number, image: child.image.data, }, }, @@ -103,6 +121,7 @@ const getFragments = (fontStore, instance, parentLink, level = 0) => { } else if (isTextInstance(child)) { fragments.push({ string: transformText(child.value, textTransform), + // @ts-expect-error custom font substitution engine deals with multiple fonts. unify with textkit attributes, }); } else if (child) { @@ -123,11 +142,11 @@ const getFragments = (fontStore, instance, parentLink, level = 0) => { /** * Get textkit attributed string from text node * - * @param {Object} fontStore font store - * @param {Object} instance node - * @returns {Object} attributed string + * @param fontStore - Font store + * @param instance Node + * @returns Attributed string */ -const getAttributedString = (fontStore, instance) => { +const getAttributedString = (fontStore: FontStore, instance: SafeTextNode) => { const fragments = getFragments(fontStore, instance); return fromFragments(fragments); }; diff --git a/packages/layout/src/text/heightAtLineIndex.js b/packages/layout/src/text/heightAtLineIndex.ts similarity index 67% rename from packages/layout/src/text/heightAtLineIndex.js rename to packages/layout/src/text/heightAtLineIndex.ts index e57f4c803..23bae6596 100644 --- a/packages/layout/src/text/heightAtLineIndex.js +++ b/packages/layout/src/text/heightAtLineIndex.ts @@ -1,10 +1,12 @@ +import { SafeTextNode } from '../types'; + /** * Get height for given text line index * - * @param {Object} node - * @param {number} index + * @param node + * @param index */ -const heightAtLineIndex = (node, index) => { +const heightAtLineIndex = (node: SafeTextNode, index: number) => { let counter = 0; if (!node.lines) return counter; diff --git a/packages/layout/src/text/ignoreChars.js b/packages/layout/src/text/ignoreChars.ts similarity index 80% rename from packages/layout/src/text/ignoreChars.js rename to packages/layout/src/text/ignoreChars.ts index 95d5985c9..4ef15ab52 100644 --- a/packages/layout/src/text/ignoreChars.js +++ b/packages/layout/src/text/ignoreChars.ts @@ -1,9 +1,11 @@ +import { Font, Fragment } from '@react-pdf/textkit'; + const IGNORABLE_CODEPOINTS = [ 8232, // LINE_SEPARATOR 8233, // PARAGRAPH_SEPARATOR ]; -const buildSubsetForFont = (font) => +const buildSubsetForFont = (font: Font) => IGNORABLE_CODEPOINTS.reduce((acc, codePoint) => { if ( font && @@ -15,7 +17,7 @@ const buildSubsetForFont = (font) => return [...acc, String.fromCharCode(codePoint)]; }, []); -const ignoreChars = (fragments) => +const ignoreChars = (fragments: Fragment[]): Fragment[] => fragments.map((fragment) => { const charSubset = buildSubsetForFont(fragment.attributes.font); const subsetRegex = new RegExp(charSubset.join('|')); diff --git a/packages/layout/src/text/layoutText.js b/packages/layout/src/text/layoutText.ts similarity index 83% rename from packages/layout/src/text/layoutText.js rename to packages/layout/src/text/layoutText.ts index e3af13f99..9cf17f456 100644 --- a/packages/layout/src/text/layoutText.js +++ b/packages/layout/src/text/layoutText.ts @@ -6,9 +6,11 @@ import layoutEngine, { wordHyphenation, textDecoration, } from '@react-pdf/textkit'; +import FontStore from '@react-pdf/font'; import fontSubstitution from './fontSubstitution'; import getAttributedString from './getAttributedString'; +import { SafeTextNode } from '../types'; const engines = { bidi, @@ -66,13 +68,18 @@ const getLayoutOptions = (fontStore, node) => ({ /** * Get text lines for given node * - * @param {Object} node node - * @param {number} width container width - * @param {number} height container height - * @param {number} fontStore font store - * @returns {Object[]} layout lines + * @param node - Node + * @param width - Container width + * @param height - Container height + * @param fontStore - Font store + * @returns Layout lines */ -const layoutText = (node, width, height, fontStore) => { +const layoutText = ( + node: SafeTextNode, + width: number, + height: number, + fontStore: FontStore, +) => { const attributedString = getAttributedString(fontStore, node); const container = getContainer(width, height, node); const options = getLayoutOptions(fontStore, node); diff --git a/packages/layout/src/text/lineIndexAtHeight.js b/packages/layout/src/text/lineIndexAtHeight.ts similarity index 69% rename from packages/layout/src/text/lineIndexAtHeight.js rename to packages/layout/src/text/lineIndexAtHeight.ts index b49dd9d2b..4099580e5 100644 --- a/packages/layout/src/text/lineIndexAtHeight.js +++ b/packages/layout/src/text/lineIndexAtHeight.ts @@ -1,10 +1,12 @@ +import { SafeTextNode } from '../types'; + /** * Get line index at given height * - * @param {Object} node - * @param {number} height + * @param node + * @param height */ -const lineIndexAtHeight = (node, height) => { +const lineIndexAtHeight = (node: SafeTextNode, height: number) => { let y = 0; if (!node.lines) return 0; diff --git a/packages/layout/src/text/linesHeight.js b/packages/layout/src/text/linesHeight.ts similarity index 57% rename from packages/layout/src/text/linesHeight.js rename to packages/layout/src/text/linesHeight.ts index 057660b4c..8313c324f 100644 --- a/packages/layout/src/text/linesHeight.js +++ b/packages/layout/src/text/linesHeight.ts @@ -1,10 +1,12 @@ +import { SafeTextNode } from '../types'; + /** * Get lines height (if any) * - * @param {Object} node - * @returns {number} lines height + * @param node + * @returns Lines height */ -const linesHeight = (node) => { +const linesHeight = (node: SafeTextNode) => { if (!node.lines) return -1; return node.lines.reduce((acc, line) => acc + line.box.height, 0); }; diff --git a/packages/layout/src/text/linesWidth.js b/packages/layout/src/text/linesWidth.ts similarity index 56% rename from packages/layout/src/text/linesWidth.js rename to packages/layout/src/text/linesWidth.ts index 86f154f7d..d595d60dc 100644 --- a/packages/layout/src/text/linesWidth.js +++ b/packages/layout/src/text/linesWidth.ts @@ -1,10 +1,12 @@ +import { SafeTextNode } from '../types'; + /** * Get lines width (if any) * - * @param {Object} node - * @returns {number} lines width + * @param node + * @returns Lines width */ -const linesWidth = (node) => { +const linesWidth = (node: SafeTextNode) => { if (!node.lines) return 0; return Math.max(0, ...node.lines.map((line) => line.xAdvance)); diff --git a/packages/layout/src/text/measureText.js b/packages/layout/src/text/measureText.js deleted file mode 100644 index e8e214e8e..000000000 --- a/packages/layout/src/text/measureText.js +++ /dev/null @@ -1,49 +0,0 @@ -import * as Yoga from 'yoga-layout/load'; - -import layoutText from './layoutText'; -import linesWidth from './linesWidth'; -import linesHeight from './linesHeight'; - -const ALIGNMENT_FACTORS = { center: 0.5, right: 1 }; - -/** - * @typedef {Function} MeasureText - * @param {number} width - * @param {number} widthMode - * @param {number} height - * @returns {{ width: number, height: number }} text width and height - */ - -/** - * Yoga text measure function - * - * @param {Object} page - * @param {Object} node - * @param {Object} fontStore - * @returns {MeasureText} measure text function - */ -const measureText = (page, node, fontStore) => (width, widthMode, height) => { - if (widthMode === Yoga.MeasureMode.Exactly) { - if (!node.lines) node.lines = layoutText(node, width, height, fontStore); - - return { height: linesHeight(node) }; - } - - if (widthMode === Yoga.MeasureMode.AtMost) { - const alignFactor = ALIGNMENT_FACTORS[node.style?.textAlign] || 0; - - if (!node.lines) { - node.lines = layoutText(node, width, height, fontStore); - node.alignOffset = (width - linesWidth(node)) * alignFactor; // Compensate align in variable width containers - } - - return { - height: linesHeight(node), - width: Math.min(width, linesWidth(node)), - }; - } - - return {}; -}; - -export default measureText; diff --git a/packages/layout/src/text/measureText.ts b/packages/layout/src/text/measureText.ts new file mode 100644 index 000000000..109a88e3e --- /dev/null +++ b/packages/layout/src/text/measureText.ts @@ -0,0 +1,49 @@ +import * as Yoga from 'yoga-layout/load'; +import FontStore from '@react-pdf/font'; + +import layoutText from './layoutText'; +import linesWidth from './linesWidth'; +import linesHeight from './linesHeight'; +import { SafePageNode, SafeTextNode } from '../types'; + +const ALIGNMENT_FACTORS = { center: 0.5, right: 1 }; + +/** + * Yoga text measure function + * + * @param page + * @param node + * @param fontStore + * @returns {MeasureText} measure text function + */ +const measureText = + ( + page: SafePageNode, + node: SafeTextNode, + fontStore: FontStore, + ): Yoga.MeasureFunction => + (width, widthMode, height) => { + if (widthMode === Yoga.MeasureMode.Exactly) { + if (!node.lines) node.lines = layoutText(node, width, height, fontStore); + + return { height: linesHeight(node) }; + } + + if (widthMode === Yoga.MeasureMode.AtMost) { + const alignFactor = ALIGNMENT_FACTORS[node.style?.textAlign] || 0; + + if (!node.lines) { + node.lines = layoutText(node, width, height, fontStore); + node.alignOffset = (width - linesWidth(node)) * alignFactor; // Compensate align in variable width containers + } + + return { + height: linesHeight(node), + width: Math.min(width, linesWidth(node)), + }; + } + + return {}; + }; + +export default measureText; diff --git a/packages/layout/src/text/splitText.js b/packages/layout/src/text/splitText.ts similarity index 79% rename from packages/layout/src/text/splitText.js rename to packages/layout/src/text/splitText.ts index 683bd3c72..848b3b68e 100644 --- a/packages/layout/src/text/splitText.js +++ b/packages/layout/src/text/splitText.ts @@ -1,12 +1,11 @@ -import { get } from '@react-pdf/fns'; - import lineIndexAtHeight from './lineIndexAtHeight'; import heightAtLineIndex from './heightAtLineIndex'; +import { SafeTextNode } from '../types'; -const getLineBreak = (node, height) => { - const top = get(node, ['box', 'top'], 0); - const widows = get(node, ['props', 'widows'], 2); - const orphans = get(node, ['props', 'orphans'], 2); +const getLineBreak = (node: SafeTextNode, height: number) => { + const top = node.box?.top || 0; + const widows = node.props.widows || 2; + const orphans = node.props.orphans || 2; const linesQuantity = node.lines.length; const slicedLine = lineIndexAtHeight(node, height - top); @@ -34,12 +33,12 @@ const getLineBreak = (node, height) => { }; // Also receives contentArea in case it's needed -const splitText = (node, height) => { +const splitText = (node: SafeTextNode, height: number) => { const slicedLineIndex = getLineBreak(node, height); const currentHeight = heightAtLineIndex(node, slicedLineIndex); const nextHeight = node.box.height - currentHeight; - const current = Object.assign({}, node, { + const current: SafeTextNode = Object.assign({}, node, { box: { ...node.box, height: currentHeight, @@ -56,7 +55,7 @@ const splitText = (node, height) => { lines: node.lines.slice(0, slicedLineIndex), }); - const next = Object.assign({}, node, { + const next: SafeTextNode = Object.assign({}, node, { box: { ...node.box, top: 0, diff --git a/packages/layout/src/text/standardFont.js b/packages/layout/src/text/standardFont.ts similarity index 93% rename from packages/layout/src/text/standardFont.js rename to packages/layout/src/text/standardFont.ts index acddac032..becf09bd5 100644 --- a/packages/layout/src/text/standardFont.js +++ b/packages/layout/src/text/standardFont.ts @@ -1,7 +1,10 @@ import { PDFFont } from '@react-pdf/pdfkit'; class StandardFont { - constructor(src) { + name: string; + src: any; + + constructor(src: string) { this.name = src; this.src = PDFFont.open(null, src); } @@ -24,18 +27,19 @@ class StandardFont { }; } - glyphForCodePoint(codePoint) { + glyphForCodePoint(codePoint: number) { const glyph = this.getGlyph(codePoint); glyph.advanceWidth = 400; return glyph; } - getGlyph(id) { + getGlyph(id: number) { return { id, _font: this.src, codePoints: [id], isLigature: false, + advanceWidth: undefined, name: this.src.font.characterToGlyph(id), }; } diff --git a/packages/layout/src/text/transformText.js b/packages/layout/src/text/transformText.ts similarity index 78% rename from packages/layout/src/text/transformText.js rename to packages/layout/src/text/transformText.ts index fc25c8f96..8d7aed337 100644 --- a/packages/layout/src/text/transformText.js +++ b/packages/layout/src/text/transformText.ts @@ -1,4 +1,5 @@ import { capitalize, upperFirst } from '@react-pdf/fns'; +import { SafeStyle } from '@react-pdf/stylesheet'; /** * Apply transformation to text string @@ -7,7 +8,10 @@ import { capitalize, upperFirst } from '@react-pdf/fns'; * @param {string} transformation type * @returns {string} transformed text */ -const transformText = (text, transformation) => { +const transformText = ( + text: string, + transformation: SafeStyle['textTransform'], +) => { switch (transformation) { case 'uppercase': return text.toUpperCase(); diff --git a/packages/layout/src/types.ts b/packages/layout/src/types.ts deleted file mode 100644 index c33b3b673..000000000 --- a/packages/layout/src/types.ts +++ /dev/null @@ -1,311 +0,0 @@ -import * as React from 'react'; -import * as P from '@react-pdf/primitives'; -import { SafeStyle, Style } from '@react-pdf/stylesheet'; -import { HyphenationCallback } from '@react-pdf/font'; - -// Generics - -type YogaNode = { - getComputedPadding: (side: number) => number; -}; - -export type Box = { - width: number; - height?: number; - - // TODO: should be optional? - marginTop?: number; - marginRight?: number; - marginBottom?: number; - marginLeft?: number; - paddingTop?: number; - paddingRight?: number; - paddingBottom?: number; - paddingLeft?: number; -}; - -export interface ExpandedBookmark { - title: string; - top?: number; - left?: number; - zoom?: number; - fit?: true | false; - expanded?: true | false; -} - -export type Bookmark = string | ExpandedBookmark; - -type RenderProp = (props: { - pageNumber: number; - totalPages?: number; - subPageNumber: number; - subPageTotalPages?: number; -}) => React.ReactNode | null | undefined; - -type Safe = Omit< - T, - 'style' | 'children' -> & { - style: [T['style']] extends [never] ? never : SafeStyle; - children?: T['children'] extends Array - ? (U extends any ? Safe : never)[] - : T['children']; -}; - -type NodeProps = { - id?: string; - /** - * Render component in all wrapped pages. - * @see https://react-pdf.org/advanced#fixed-components - */ - fixed?: boolean; - /** - * Force the wrapping algorithm to start a new page when rendering the - * element. - * @see https://react-pdf.org/advanced#page-breaks - */ - break?: boolean; - /** - * Hint that no page wrapping should occur between all sibling elements following the element within n points - * @see https://react-pdf.org/advanced#orphan-&-widow-protection - */ - minPresenceAhead?: number; - bookmark?: Bookmark; -}; - -// Text Instance - -export type TextInstanceNode = { - type: typeof P.TextInstance; - props?: never; - style?: never; - box?: never; - children?: never; - yogaNode?: never; - value: string; -}; - -export type SafeTextInstanceNode = TextInstanceNode; - -// Text - -interface TextProps extends NodeProps { - id?: string; - /** - * Enable/disable page wrapping for element. - * @see https://react-pdf.org/components#page-wrapping - */ - wrap?: boolean; - /** - * Enables debug mode on page bounding box. - * @see https://react-pdf.org/advanced#debugging - */ - debug?: boolean; - render?: RenderProp; - /** - * Override the default hyphenation-callback - * @see https://react-pdf.org/fonts#registerhyphenationcallback - */ - hyphenationCallback?: HyphenationCallback; - /** - * Specifies the minimum number of lines in a text element that must be shown at the bottom of a page or its container. - * @see https://react-pdf.org/advanced#orphan-&-widow-protection - */ - orphans?: number; - /** - * Specifies the minimum number of lines in a text element that must be shown at the top of a page or its container.. - * @see https://react-pdf.org/advanced#orphan-&-widow-protection - */ - widows?: number; -} - -export type TextNode = { - type: typeof P.Text; - props: TextProps; - style?: Style | Style[]; - box?: Box; - yogaNode?: YogaNode; - children?: (TextNode | TextInstanceNode)[]; -}; - -export type SafeTextNode = Safe; - -// Link - -interface LinkProps extends NodeProps { - /** - * Enable/disable page wrapping for element. - * @see https://react-pdf.org/components#page-wrapping - */ - wrap?: boolean; - /** - * Enables debug mode on page bounding box. - * @see https://react-pdf.org/advanced#debugging - */ - debug?: boolean; - href?: string; - src?: string; - render?: RenderProp; -} - -export type LinkNode = { - type: typeof P.Link; - props: LinkProps; - style?: Style | Style[]; - box?: Box; - yogaNode?: YogaNode; - children?: (ViewNode | TextNode | TextInstanceNode)[]; -}; - -export type SafeLinkNode = Safe; - -// View - -interface ViewProps extends NodeProps { - id?: string; - /** - * Enable/disable page wrapping for element. - * @see https://react-pdf.org/components#page-wrapping - */ - wrap?: boolean; - /** - * Enables debug mode on page bounding box. - * @see https://react-pdf.org/advanced#debugging - */ - debug?: boolean; - render?: RenderProp; -} - -export type ViewNode = { - type: typeof P.View; - props: ViewProps; - style?: Style | Style[]; - box?: Box; - yogaNode?: YogaNode; - children?: (ViewNode | TextNode | LinkNode)[]; -}; - -export type SafeViewNode = Safe; - -// Page - -export type Orientation = 'portrait' | 'landscape'; - -export type StandardPageSize = - | '4A0' - | '2A0' - | 'A0' - | 'A1' - | 'A2' - | 'A3' - | 'A4' - | 'A5' - | 'A6' - | 'A7' - | 'A8' - | 'A9' - | 'A10' - | 'B0' - | 'B1' - | 'B2' - | 'B3' - | 'B4' - | 'B5' - | 'B6' - | 'B7' - | 'B8' - | 'B9' - | 'B10' - | 'C0' - | 'C1' - | 'C2' - | 'C3' - | 'C4' - | 'C5' - | 'C6' - | 'C7' - | 'C8' - | 'C9' - | 'C10' - | 'RA0' - | 'RA1' - | 'RA2' - | 'RA3' - | 'RA4' - | 'SRA0' - | 'SRA1' - | 'SRA2' - | 'SRA3' - | 'SRA4' - | 'EXECUTIVE' - | 'FOLIO' - | 'LEGAL' - | 'LETTER' - | 'TABLOID' - | 'ID1'; - -type StaticSize = number | string; - -export type PageSize = - | number - | StandardPageSize - | [StaticSize] - | [StaticSize, StaticSize] - | { width: StaticSize; height?: StaticSize }; - -interface PageProps extends NodeProps { - /** - * Enable page wrapping for this page. - * @see https://react-pdf.org/components#page-wrapping - */ - wrap?: boolean; - /** - * Enables debug mode on page bounding box. - * @see https://react-pdf.org/advanced#debugging - */ - debug?: boolean; - size?: PageSize; - orientation?: Orientation; - dpi?: number; -} - -export type PageNode = { - type: typeof P.Page; - props: PageProps; - style?: Style | Style[]; - box?: Box; - yogaNode?: YogaNode; - children?: (ViewNode | TextNode | LinkNode)[]; -}; - -export type SafePageNode = Safe; - -// Document - -export type DocumentNode = { - type: 'DOCUMENT'; - props: object; - box?: never; - style?: never; - yoga?: unknown; - yogaNode?: never; - children: PageNode[]; -}; - -export type SafeDocumentNode = Safe; - -export type Node = - | DocumentNode - | PageNode - | ViewNode - | LinkNode - | TextNode - | TextInstanceNode; - -export type SafeNode = - | SafeDocumentNode - | SafePageNode - | SafeViewNode - | SafeLinkNode - | SafeTextNode - | SafeTextInstanceNode; diff --git a/packages/layout/src/types/base.ts b/packages/layout/src/types/base.ts new file mode 100644 index 000000000..804453b57 --- /dev/null +++ b/packages/layout/src/types/base.ts @@ -0,0 +1,118 @@ +import { YogaNode } from 'yoga-layout/load'; +import * as React from 'react'; + +export type YogaInstance = { + node: { create: () => YogaNode }; +}; + +export type Box = { + width: number; + height: number; + + top: number; + left: number; + right: number; + bottom: number; + + // TODO: should be optional? + marginTop?: number; + marginRight?: number; + marginBottom?: number; + marginLeft?: number; + paddingTop?: number; + paddingRight?: number; + paddingBottom?: number; + paddingLeft?: number; + borderTopWidth?: number; + borderRightWidth?: number; + borderBottomWidth?: number; + borderLeftWidth?: number; +}; + +export type Origin = { + left: number; + top: number; +}; + +export interface ExpandedBookmark { + title: string; + top?: number; + left?: number; + zoom?: number; + fit?: true | false; + expanded?: true | false; +} + +export type Bookmark = string | ExpandedBookmark; + +export type DynamicPageProps = { + pageNumber: number; + totalPages?: number; + subPageNumber?: number; + subPageTotalPages?: number; +}; + +export type RenderProp = ( + props: DynamicPageProps, +) => React.ReactNode | null | undefined; + +export type NodeProps = { + id?: string; + /** + * Render component in all wrapped pages. + * @see https://react-pdf.org/advanced#fixed-components + */ + fixed?: boolean; + /** + * Force the wrapping algorithm to start a new page when rendering the + * element. + * @see https://react-pdf.org/advanced#page-breaks + */ + break?: boolean; + /** + * Hint that no page wrapping should occur between all sibling elements following the element within n points + * @see https://react-pdf.org/advanced#orphan-&-widow-protection + */ + minPresenceAhead?: number; + /** + * Enables debug mode on page bounding box. + * @see https://react-pdf.org/advanced#debugging + */ + debug?: boolean; + bookmark?: Bookmark; +}; + +export interface SVGPresentationAttributes { + fill?: string; + color?: string; + stroke?: string; + transform?: string; + strokeDasharray?: string; + opacity?: string | number; + strokeWidth?: string | number; + fillOpacity?: string | number; + fillRule?: 'nonzero' | 'evenodd'; + strokeOpacity?: string | number; + textAnchor?: 'start' | 'middle' | 'end'; + strokeLinecap?: 'butt' | 'round' | 'square'; + strokeLinejoin?: 'butt' | 'round' | 'square'; + visibility?: 'visible' | 'hidden' | 'collapse'; + clipPath?: string; + dominantBaseline?: + | 'auto' + | 'middle' + | 'central' + | 'hanging' + | 'mathematical' + | 'text-after-edge' + | 'text-before-edge'; +} + +export interface FormCommonProps extends NodeProps { + name?: string; + required?: boolean; + noExport?: boolean; + readOnly?: boolean; + value?: number | string; + defaultValue?: number | string; +} diff --git a/packages/layout/src/types/canvas.ts b/packages/layout/src/types/canvas.ts new file mode 100644 index 000000000..64bb43711 --- /dev/null +++ b/packages/layout/src/types/canvas.ts @@ -0,0 +1,26 @@ +import * as P from '@react-pdf/primitives'; +import { SafeStyle, Style } from '@react-pdf/stylesheet'; + +import { NodeProps, Origin } from './base'; + +interface CanvasProps extends NodeProps { + paint: ( + painter: any, + availableWidth?: number, + availableHeight?: number, + ) => null; +} + +export type CanvasNode = { + type: typeof P.Canvas; + props: CanvasProps; + style?: Style | Style[]; + box?: never; + origin?: Origin; + yogaNode?: never; + children?: never[]; +}; + +export type SafeCanvasNode = Omit & { + style: SafeStyle; +}; diff --git a/packages/layout/src/types/checkbox.ts b/packages/layout/src/types/checkbox.ts new file mode 100644 index 000000000..2e1598719 --- /dev/null +++ b/packages/layout/src/types/checkbox.ts @@ -0,0 +1,28 @@ +import * as P from '@react-pdf/primitives'; +import { SafeStyle, Style } from '@react-pdf/stylesheet'; +import { YogaNode } from 'yoga-layout/load'; + +import { Box, FormCommonProps, Origin } from './base'; + +interface CheckboxProps extends FormCommonProps { + backgroundColor?: string; + borderColor?: string; + checked?: boolean; + onState?: string; + offState?: string; + xMark?: boolean; +} + +export type CheckboxNode = { + type: typeof P.Checkbox; + props: CheckboxProps; + style?: Style | Style[]; + box?: Box; + origin?: Origin; + yogaNode?: YogaNode; + children?: never[]; +}; + +export type SafeCheckboxNode = Omit & { + style: SafeStyle; +}; diff --git a/packages/layout/src/types/circle.ts b/packages/layout/src/types/circle.ts new file mode 100644 index 000000000..d445fd62c --- /dev/null +++ b/packages/layout/src/types/circle.ts @@ -0,0 +1,25 @@ +import * as P from '@react-pdf/primitives'; +import { SafeStyle, Style } from '@react-pdf/stylesheet'; + +import { Origin, SVGPresentationAttributes } from './base'; + +interface CircleProps extends SVGPresentationAttributes { + style?: SVGPresentationAttributes; + cx?: string | number; + cy?: string | number; + r: string | number; +} + +export type CircleNode = { + type: typeof P.Circle; + props: CircleProps; + style?: Style | Style[]; + box?: never; + origin?: Origin; + yogaNode?: never; + children?: never[]; +}; + +export type SafeCircleNode = Omit & { + style: SafeStyle; +}; diff --git a/packages/layout/src/types/clip-path.ts b/packages/layout/src/types/clip-path.ts new file mode 100644 index 000000000..855624286 --- /dev/null +++ b/packages/layout/src/types/clip-path.ts @@ -0,0 +1,43 @@ +import * as P from '@react-pdf/primitives'; + +import { LineNode, SafeLineNode } from './line'; +import { PolylineNode, SafePolylineNode } from './polyline'; +import { PolygonNode, SafePolygonNode } from './polygon'; +import { PathNode, SafePathNode } from './path'; +import { RectNode, SafeRectNode } from './rect'; +import { CircleNode, SafeCircleNode } from './circle'; +import { EllipseNode, SafeEllipseNode } from './ellipse'; + +interface ClipPathProps { + id?: string; +} + +export type ClipPathNode = { + type: typeof P.ClipPath; + props: ClipPathProps; + style: never; + box?: never; + origin?: never; + yogaNode?: never; + children?: ( + | LineNode + | PolylineNode + | PolygonNode + | PathNode + | RectNode + | CircleNode + | EllipseNode + )[]; +}; + +export type SafeClipPathNode = Omit & { + children?: ( + | SafeLineNode + | SafePolylineNode + | SafePolygonNode + | SafePathNode + | SafeRectNode + | SafeCircleNode + | SafeEllipseNode + )[]; +}; diff --git a/packages/layout/src/types/defs.ts b/packages/layout/src/types/defs.ts new file mode 100644 index 000000000..75e073898 --- /dev/null +++ b/packages/layout/src/types/defs.ts @@ -0,0 +1,26 @@ +import * as P from '@react-pdf/primitives'; +import { ClipPathNode, SafeClipPathNode } from './clip-path'; +import { LinearGradientNode, SafeLinearGradientNode } from './linear-gradient'; +import { RadialGradientNode, SafeRadialGradientNode } from './radial-gradient'; + +export type DefsNode = { + type: typeof P.Defs; + props: never; + style: never; + box?: never; + origin?: never; + yogaNode?: never; + children?: (ClipPathNode | LinearGradientNode | RadialGradientNode)[]; +}; + +export type Defs = Record; + +export type SafeDefsNode = Omit & { + children?: ( + | SafeClipPathNode + | SafeLinearGradientNode + | SafeRadialGradientNode + )[]; +}; + +export type SafeDefs = Record; diff --git a/packages/layout/src/types/document.ts b/packages/layout/src/types/document.ts new file mode 100644 index 000000000..203854d40 --- /dev/null +++ b/packages/layout/src/types/document.ts @@ -0,0 +1,23 @@ +import * as P from '@react-pdf/primitives'; + +import { PageNode, SafePageNode } from './page'; +import { YogaInstance } from './base'; + +export type DocumentProps = { + bookmark?: never; +}; + +export type DocumentNode = { + type: typeof P.Document; + props: DocumentProps; + box?: never; + origin?: never; + style?: never; + yoga?: YogaInstance; + yogaNode?: never; + children: PageNode[]; +}; + +export type SafeDocumentNode = Omit & { + children: SafePageNode[]; +}; diff --git a/packages/layout/src/types/ellipse.ts b/packages/layout/src/types/ellipse.ts new file mode 100644 index 000000000..a5962d957 --- /dev/null +++ b/packages/layout/src/types/ellipse.ts @@ -0,0 +1,26 @@ +import * as P from '@react-pdf/primitives'; +import { SafeStyle, Style } from '@react-pdf/stylesheet'; + +import { SVGPresentationAttributes } from './base'; + +interface EllipseProps extends SVGPresentationAttributes { + style?: SVGPresentationAttributes; + cx?: string | number; + cy?: string | number; + rx: string | number; + ry: string | number; +} + +export type EllipseNode = { + type: typeof P.Ellipse; + props: EllipseProps; + style?: Style | Style[]; + box?: never; + origin?: never; + yogaNode?: never; + children?: never[]; +}; + +export type SafeEllipseNode = Omit & { + style: SafeStyle; +}; diff --git a/packages/layout/src/types/field-set.ts b/packages/layout/src/types/field-set.ts new file mode 100644 index 000000000..8f62e18da --- /dev/null +++ b/packages/layout/src/types/field-set.ts @@ -0,0 +1,27 @@ +import * as P from '@react-pdf/primitives'; +import { SafeStyle, Style } from '@react-pdf/stylesheet'; +import { YogaNode } from 'yoga-layout/load'; + +import { Box, NodeProps, Origin } from './base'; +import { SafeTextNode, TextNode } from './text'; +import { SafeViewNode, ViewNode } from './view'; +import { SafeTextInputNode, TextInputNode } from './text-input'; + +interface FieldSetProps extends NodeProps { + name: string; +} + +export type FieldSetNode = { + type: typeof P.FieldSet; + props: FieldSetProps; + style?: Style | Style[]; + box?: Box; + origin?: Origin; + yogaNode?: YogaNode; + children?: (TextNode | ViewNode | TextInputNode)[]; +}; + +export type SafeFieldSetNode = Omit & { + style: SafeStyle; + children?: (SafeTextNode | SafeViewNode | SafeTextInputNode)[]; +}; diff --git a/packages/layout/src/types/g.ts b/packages/layout/src/types/g.ts new file mode 100644 index 000000000..d09389384 --- /dev/null +++ b/packages/layout/src/types/g.ts @@ -0,0 +1,51 @@ +import { SafeStyle, Style } from '@react-pdf/stylesheet'; +import * as P from '@react-pdf/primitives'; + +import { SVGPresentationAttributes } from './base'; +import { LineNode, SafeLineNode } from './line'; +import { PolylineNode, SafePolylineNode } from './polyline'; +import { PolygonNode, SafePolygonNode } from './polygon'; +import { PathNode, SafePathNode } from './path'; +import { RectNode, SafeRectNode } from './rect'; +import { CircleNode, SafeCircleNode } from './circle'; +import { EllipseNode, SafeEllipseNode } from './ellipse'; +import { SafeTspanNode, TspanNode } from './tspan'; + +interface GProps extends SVGPresentationAttributes { + style?: Style | Style[]; +} + +export type GNode = { + type: typeof P.G; + props: GProps; + style?: Style | Style[]; + box?: never; + origin?: never; + yogaNode?: never; + children?: ( + | LineNode + | PolylineNode + | PolygonNode + | PathNode + | RectNode + | CircleNode + | EllipseNode + | TspanNode + | GNode + )[]; +}; + +export type SafeGNode = Omit & { + style: SafeStyle; + children?: ( + | SafeLineNode + | SafePolylineNode + | SafePolygonNode + | SafePathNode + | SafeRectNode + | SafeCircleNode + | SafeEllipseNode + | SafeTspanNode + | SafeGNode + )[]; +}; diff --git a/packages/layout/src/types/image.ts b/packages/layout/src/types/image.ts new file mode 100644 index 000000000..b5bfa4d15 --- /dev/null +++ b/packages/layout/src/types/image.ts @@ -0,0 +1,73 @@ +import { SafeStyle, Style } from '@react-pdf/stylesheet'; +import * as P from '@react-pdf/primitives'; +import { YogaNode } from 'yoga-layout/load'; + +import { Box, NodeProps, Origin } from './base'; +import { Image } from '@react-pdf/image'; + +type HTTPMethod = 'GET' | 'HEAD' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; + +type SourceURL = string; + +type SourceBuffer = Buffer; + +type SourceBlob = Blob; + +type SourceDataBuffer = { data: Buffer; format: 'png' | 'jpg' }; + +type SourceURLObject = { + uri: string; + method?: HTTPMethod; + body?: any; + headers?: any; + credentials?: 'omit' | 'same-origin' | 'include'; +}; + +type Source = + | SourceURL + | SourceBuffer + | SourceBlob + | SourceDataBuffer + | SourceURLObject + | undefined; + +type SourceFactory = () => Source; + +type SourceAsync = Promise; + +type SourceAsyncFactory = () => Promise; + +export type SourceObject = + | Source + | SourceFactory + | SourceAsync + | SourceAsyncFactory; + +interface BaseImageProps extends NodeProps { + cache?: boolean; +} + +interface ImageWithSrcProp extends BaseImageProps { + src: SourceObject; +} + +interface ImageWithSourceProp extends BaseImageProps { + source: SourceObject; +} + +export type ImageProps = ImageWithSrcProp | ImageWithSourceProp; + +export type ImageNode = { + type: typeof P.Image; + props: ImageProps; + image?: Image; + style?: Style | Style[]; + box?: Box; + origin?: Origin; + yogaNode?: YogaNode; + children?: never[]; +}; + +export type SafeImageNode = Omit & { + style: SafeStyle; +}; diff --git a/packages/layout/src/types/index.ts b/packages/layout/src/types/index.ts new file mode 100644 index 000000000..aef8c25f2 --- /dev/null +++ b/packages/layout/src/types/index.ts @@ -0,0 +1,28 @@ +export * from './base'; +export * from './circle'; +export * from './clip-path'; +export * from './defs'; +export * from './document'; +export * from './ellipse'; +export * from './field-set'; +export * from './g'; +export * from './image'; +export * from './line'; +export * from './linear-gradient'; +export * from './link'; +export * from './node'; +export * from './note'; +export * from './page'; +export * from './path'; +export * from './polygon'; +export * from './polyline'; +export * from './radial-gradient'; +export * from './rect'; +export * from './select'; +export * from './stop'; +export * from './svg'; +export * from './text-instance'; +export * from './text-input'; +export * from './text'; +export * from './tspan'; +export * from './view'; diff --git a/packages/layout/src/types/line.ts b/packages/layout/src/types/line.ts new file mode 100644 index 000000000..03f1221ef --- /dev/null +++ b/packages/layout/src/types/line.ts @@ -0,0 +1,26 @@ +import * as P from '@react-pdf/primitives'; +import { SafeStyle, Style } from '@react-pdf/stylesheet'; + +import { SVGPresentationAttributes } from './base'; + +interface LineProps extends SVGPresentationAttributes { + style?: SVGPresentationAttributes; + x1: string | number; + x2: string | number; + y1: string | number; + y2: string | number; +} + +export type LineNode = { + type: typeof P.Line; + props: LineProps; + style?: Style | Style[]; + box?: never; + origin?: never; + yogaNode?: never; + children?: never[]; +}; + +export type SafeLineNode = Omit & { + style: SafeStyle; +}; diff --git a/packages/layout/src/types/linear-gradient.ts b/packages/layout/src/types/linear-gradient.ts new file mode 100644 index 000000000..1507e2ce6 --- /dev/null +++ b/packages/layout/src/types/linear-gradient.ts @@ -0,0 +1,25 @@ +import * as P from '@react-pdf/primitives'; + +import { SafeStopNode, StopNode } from './stop'; + +interface LinearGradientProps { + id: string; + x1?: string | number; + x2?: string | number; + y1?: string | number; + y2?: string | number; +} + +export type LinearGradientNode = { + type: typeof P.LinearGradient; + props: LinearGradientProps; + style?: never; + box?: never; + origin?: never; + yogaNode?: never; + children?: StopNode[]; +}; + +export type SafeLinearGradientNode = Omit & { + children?: SafeStopNode[]; +}; diff --git a/packages/layout/src/types/link.ts b/packages/layout/src/types/link.ts new file mode 100644 index 000000000..c30972a95 --- /dev/null +++ b/packages/layout/src/types/link.ts @@ -0,0 +1,40 @@ +import { SafeStyle, Style } from '@react-pdf/stylesheet'; +import * as P from '@react-pdf/primitives'; +import { YogaNode } from 'yoga-layout/load'; + +import { Box, NodeProps, Origin, RenderProp } from './base'; +import { ImageNode, SafeImageNode } from './image'; +import { SafeTextNode, TextNode } from './text'; +import { SafeTextInstanceNode, TextInstanceNode } from './text-instance'; +import { SafeViewNode, ViewNode } from './view'; + +interface LinkProps extends NodeProps { + /** + * Enable/disable page wrapping for element. + * @see https://react-pdf.org/components#page-wrapping + */ + wrap?: boolean; + href?: string; + src?: string; + render?: RenderProp; +} + +export type LinkNode = { + type: typeof P.Link; + props: LinkProps; + style?: Style | Style[]; + box?: Box; + origin?: Origin; + yogaNode?: YogaNode; + children?: (ViewNode | ImageNode | TextNode | TextInstanceNode)[]; +}; + +export type SafeLinkNode = Omit & { + style: SafeStyle; + children?: ( + | SafeViewNode + | SafeImageNode + | SafeTextNode + | SafeTextInstanceNode + )[]; +}; diff --git a/packages/layout/src/types/node.ts b/packages/layout/src/types/node.ts new file mode 100644 index 000000000..e16fbf89c --- /dev/null +++ b/packages/layout/src/types/node.ts @@ -0,0 +1,90 @@ +import { CanvasNode, SafeCanvasNode } from './canvas'; +import { CheckboxNode, SafeCheckboxNode } from './checkbox'; +import { CircleNode, SafeCircleNode } from './circle'; +import { ClipPathNode, SafeClipPathNode } from './clip-path'; +import { DefsNode, SafeDefsNode } from './defs'; +import { DocumentNode, SafeDocumentNode } from './document'; +import { EllipseNode, SafeEllipseNode } from './ellipse'; +import { FieldSetNode, SafeFieldSetNode } from './field-set'; +import { GNode, SafeGNode } from './g'; +import { ImageNode, SafeImageNode } from './image'; +import { LineNode, SafeLineNode } from './line'; +import { LinearGradientNode, SafeLinearGradientNode } from './linear-gradient'; +import { LinkNode, SafeLinkNode } from './link'; +import { NoteNode, SafeNoteNode } from './note'; +import { PageNode, SafePageNode } from './page'; +import { PathNode, SafePathNode } from './path'; +import { PolygonNode, SafePolygonNode } from './polygon'; +import { PolylineNode, SafePolylineNode } from './polyline'; +import { RadialGradientNode, SafeRadialGradientNode } from './radial-gradient'; +import { RectNode, SafeRectNode } from './rect'; +import { ListNode, SafeListNode, SafeSelectNode, SelectNode } from './select'; +import { SafeStopNode, StopNode } from './stop'; +import { SafeSvgNode, SvgNode } from './svg'; +import { SafeTextNode, TextNode } from './text'; +import { SafeTextInputNode, TextInputNode } from './text-input'; +import { SafeTextInstanceNode, TextInstanceNode } from './text-instance'; +import { SafeTspanNode, TspanNode } from './tspan'; +import { SafeViewNode, ViewNode } from './view'; + +export type Node = + | DocumentNode + | PageNode + | ImageNode + | SvgNode + | CircleNode + | ClipPathNode + | DefsNode + | EllipseNode + | GNode + | LineNode + | LinearGradientNode + | PathNode + | PolygonNode + | PolylineNode + | RadialGradientNode + | RectNode + | StopNode + | TspanNode + | ViewNode + | LinkNode + | TextNode + | TextInstanceNode + | NoteNode + | CanvasNode + | FieldSetNode + | TextInputNode + | SelectNode + | ListNode + | CheckboxNode; + +export type SafeNode = + | SafeDocumentNode + | SafePageNode + | SafeImageNode + | SafeSvgNode + | SafeCircleNode + | SafeClipPathNode + | SafeDefsNode + | SafeEllipseNode + | SafeGNode + | SafeLineNode + | SafeLinearGradientNode + | SafePathNode + | SafePolygonNode + | SafePolylineNode + | SafeRadialGradientNode + | SafeRectNode + | SafeStopNode + | SafeTspanNode + | SafeViewNode + | SafeLinkNode + | SafeTextNode + | SafeTextInstanceNode + | SafeNoteNode + | SafeCanvasNode + | SafeFieldSetNode + | SafeTextInputNode + | SafeSelectNode + | SafeListNode + | SafeCheckboxNode; diff --git a/packages/layout/src/types/note.ts b/packages/layout/src/types/note.ts new file mode 100644 index 000000000..4b043a85e --- /dev/null +++ b/packages/layout/src/types/note.ts @@ -0,0 +1,20 @@ +import * as P from '@react-pdf/primitives'; +import { SafeStyle, Style } from '@react-pdf/stylesheet'; + +import { NodeProps } from './base'; +import { SafeTextInstanceNode, TextInstanceNode } from './text-instance'; + +export type NoteNode = { + type: typeof P.Note; + props: NodeProps; + style?: Style | Style[]; + box?: never; + origin?: never; + yogaNode?: never; + children?: TextInstanceNode[]; +}; + +export type SafeNoteNode = Omit & { + style: SafeStyle; + children?: SafeTextInstanceNode[]; +}; diff --git a/packages/layout/src/types/page.ts b/packages/layout/src/types/page.ts new file mode 100644 index 000000000..25e8548d4 --- /dev/null +++ b/packages/layout/src/types/page.ts @@ -0,0 +1,129 @@ +import { SafeStyle, Style } from '@react-pdf/stylesheet'; +import * as P from '@react-pdf/primitives'; +import { YogaNode } from 'yoga-layout/load'; + +import type { Box, NodeProps, Origin } from './base'; +import { ImageNode, SafeImageNode } from './image'; +import { SafeViewNode, ViewNode } from './view'; +import { SafeTextNode, TextNode } from './text'; +import { LinkNode, SafeLinkNode } from './link'; +import { CanvasNode, SafeCanvasNode } from './canvas'; +import { FieldSetNode, SafeFieldSetNode } from './field-set'; +import { SafeTextInputNode, TextInputNode } from './text-input'; +import { ListNode, SafeListNode, SafeSelectNode, SelectNode } from './select'; +import { CheckboxNode, SafeCheckboxNode } from './checkbox'; +import { NoteNode, SafeNoteNode } from './note'; + +export type Orientation = 'portrait' | 'landscape'; + +export type StandardPageSize = + | '4A0' + | '2A0' + | 'A0' + | 'A1' + | 'A2' + | 'A3' + | 'A4' + | 'A5' + | 'A6' + | 'A7' + | 'A8' + | 'A9' + | 'A10' + | 'B0' + | 'B1' + | 'B2' + | 'B3' + | 'B4' + | 'B5' + | 'B6' + | 'B7' + | 'B8' + | 'B9' + | 'B10' + | 'C0' + | 'C1' + | 'C2' + | 'C3' + | 'C4' + | 'C5' + | 'C6' + | 'C7' + | 'C8' + | 'C9' + | 'C10' + | 'RA0' + | 'RA1' + | 'RA2' + | 'RA3' + | 'RA4' + | 'SRA0' + | 'SRA1' + | 'SRA2' + | 'SRA3' + | 'SRA4' + | 'EXECUTIVE' + | 'FOLIO' + | 'LEGAL' + | 'LETTER' + | 'TABLOID' + | 'ID1'; + +type StaticSize = number | string; + +export type PageSize = + | number + | StandardPageSize + | [StaticSize] + | [StaticSize, StaticSize] + | { width: StaticSize; height?: StaticSize }; + +interface PageProps extends NodeProps { + /** + * Enable page wrapping for this page. + * @see https://react-pdf.org/components#page-wrapping + */ + wrap?: boolean; + size?: PageSize; + orientation?: Orientation; + dpi?: number; +} + +export type PageNode = { + type: typeof P.Page; + props: PageProps; + style?: Style | Style[]; + box?: Box; + origin?: Origin; + yogaNode?: YogaNode; + children?: ( + | ViewNode + | ImageNode + | TextNode + | LinkNode + | CanvasNode + | FieldSetNode + | TextInputNode + | SelectNode + | ListNode + | CheckboxNode + | NoteNode + )[]; +}; + +export type SafePageNode = Omit & { + style: SafeStyle; + children?: ( + | SafeViewNode + | SafeImageNode + | SafeTextNode + | SafeLinkNode + | SafeCanvasNode + | SafeFieldSetNode + | SafeTextInputNode + | SafeSelectNode + | SafeListNode + | SafeCheckboxNode + | SafeNoteNode + )[]; +}; diff --git a/packages/layout/src/types/path.ts b/packages/layout/src/types/path.ts new file mode 100644 index 000000000..a74e69c78 --- /dev/null +++ b/packages/layout/src/types/path.ts @@ -0,0 +1,23 @@ +import * as P from '@react-pdf/primitives'; +import { SafeStyle, Style } from '@react-pdf/stylesheet'; + +import { SVGPresentationAttributes } from './base'; + +interface PathProps extends SVGPresentationAttributes { + style?: SVGPresentationAttributes; + d: string; +} + +export type PathNode = { + type: typeof P.Path; + props: PathProps; + style?: Style | Style[]; + box?: never; + origin?: never; + yogaNode?: never; + children?: never[]; +}; + +export type SafePathNode = Omit & { + style: SafeStyle; +}; diff --git a/packages/layout/src/types/polygon.ts b/packages/layout/src/types/polygon.ts new file mode 100644 index 000000000..23d07366a --- /dev/null +++ b/packages/layout/src/types/polygon.ts @@ -0,0 +1,23 @@ +import * as P from '@react-pdf/primitives'; +import { SafeStyle, Style } from '@react-pdf/stylesheet'; + +import { SVGPresentationAttributes } from './base'; + +interface PolygonProps extends SVGPresentationAttributes { + style?: SVGPresentationAttributes; + points: string; +} + +export type PolygonNode = { + type: typeof P.Polygon; + props: PolygonProps; + style?: Style | Style[]; + box?: never; + origin?: never; + yogaNode?: never; + children?: never[]; +}; + +export type SafePolygonNode = Omit & { + style: SafeStyle; +}; diff --git a/packages/layout/src/types/polyline.ts b/packages/layout/src/types/polyline.ts new file mode 100644 index 000000000..b40198637 --- /dev/null +++ b/packages/layout/src/types/polyline.ts @@ -0,0 +1,23 @@ +import * as P from '@react-pdf/primitives'; +import { SafeStyle, Style } from '@react-pdf/stylesheet'; + +import { SVGPresentationAttributes } from './base'; + +interface PolylineProps extends SVGPresentationAttributes { + style?: SVGPresentationAttributes; + points: string; +} + +export type PolylineNode = { + type: typeof P.Polyline; + props: PolylineProps; + style?: Style | Style[]; + box?: never; + origin?: never; + yogaNode?: never; + children?: never[]; +}; + +export type SafePolylineNode = Omit & { + style: SafeStyle; +}; diff --git a/packages/layout/src/types/radial-gradient.ts b/packages/layout/src/types/radial-gradient.ts new file mode 100644 index 000000000..2c1ae4937 --- /dev/null +++ b/packages/layout/src/types/radial-gradient.ts @@ -0,0 +1,26 @@ +import * as P from '@react-pdf/primitives'; + +import { SafeStopNode, StopNode } from './stop'; + +interface RadialGradientProps { + id: string; + cx?: string | number; + cy?: string | number; + fr?: string | number; + fx?: string | number; + fy?: string | number; +} + +export type RadialGradientNode = { + type: typeof P.RadialGradient; + props: RadialGradientProps; + style?: never; + box?: never; + origin?: never; + yogaNode?: never; + children?: StopNode[]; +}; + +export type SafeRadialGradientNode = Omit & { + children?: SafeStopNode[]; +}; diff --git a/packages/layout/src/types/rect.ts b/packages/layout/src/types/rect.ts new file mode 100644 index 000000000..84fa5b890 --- /dev/null +++ b/packages/layout/src/types/rect.ts @@ -0,0 +1,28 @@ +import * as P from '@react-pdf/primitives'; +import { SafeStyle, Style } from '@react-pdf/stylesheet'; + +import { SVGPresentationAttributes } from './base'; + +interface RectProps extends SVGPresentationAttributes { + style?: SVGPresentationAttributes; + x?: string | number; + y?: string | number; + width: string | number; + height: string | number; + rx?: string | number; + ry?: string | number; +} + +export type RectNode = { + type: typeof P.Rect; + props: RectProps; + style?: Style | Style[]; + box?: never; + origin?: never; + yogaNode?: never; + children?: never[]; +}; + +export type SafeRectNode = Omit & { + style: SafeStyle; +}; diff --git a/packages/layout/src/types/select.ts b/packages/layout/src/types/select.ts new file mode 100644 index 000000000..7314b7370 --- /dev/null +++ b/packages/layout/src/types/select.ts @@ -0,0 +1,55 @@ +import * as P from '@react-pdf/primitives'; +import { SafeStyle, Style } from '@react-pdf/stylesheet'; +import { YogaNode } from 'yoga-layout/load'; + +import { Box, FormCommonProps, Origin } from './base'; + +interface SelectAndListPropsBase extends FormCommonProps { + sort?: boolean; + edit?: boolean; + multiSelect?: boolean; + noSpell?: boolean; + select?: string[]; +} + +type SelectAndListPropsWithEdit = SelectAndListPropsBase & { + edit: true | false; + noSpell: boolean; +}; + +type SelectAndListPropsWithNoSpell = SelectAndListPropsBase & { + edit: boolean; + noSpell: true | false; +}; + +type SelectAndListProps = + | SelectAndListPropsWithEdit + | SelectAndListPropsWithNoSpell; + +export type SelectNode = { + type: typeof P.Select; + props: SelectAndListProps; + style?: Style | Style[]; + box?: Box; + origin?: Origin; + yogaNode?: never; + children?: never[]; +}; + +export type SafeSelectNode = Omit & { + style: SafeStyle; +}; + +export type ListNode = { + type: typeof P.List; + props: SelectAndListProps; + style?: Style | Style[]; + box?: Box; + origin?: Origin; + yogaNode?: YogaNode; + children?: never[]; +}; + +export type SafeListNode = Omit & { + style: SafeStyle; +}; diff --git a/packages/layout/src/types/stop.ts b/packages/layout/src/types/stop.ts new file mode 100644 index 000000000..e0889c135 --- /dev/null +++ b/packages/layout/src/types/stop.ts @@ -0,0 +1,19 @@ +import * as P from '@react-pdf/primitives'; + +interface StopProps { + offset: string | number; + stopColor: string; + stopOpacity?: string | number; +} + +export type StopNode = { + type: typeof P.Stop; + props: StopProps; + style?: never; + box?: never; + origin?: never; + yogaNode?: never; + children?: never[]; +}; + +export type SafeStopNode = StopNode; diff --git a/packages/layout/src/types/svg.ts b/packages/layout/src/types/svg.ts new file mode 100644 index 000000000..26306787e --- /dev/null +++ b/packages/layout/src/types/svg.ts @@ -0,0 +1,81 @@ +import { SafeStyle, Style } from '@react-pdf/stylesheet'; +import * as P from '@react-pdf/primitives'; +import { YogaNode } from 'yoga-layout/load'; + +import { Box, NodeProps, Origin, SVGPresentationAttributes } from './base'; +import { LineNode, SafeLineNode } from './line'; +import { PolylineNode, SafePolylineNode } from './polyline'; +import { PolygonNode, SafePolygonNode } from './polygon'; +import { PathNode, SafePathNode } from './path'; +import { RectNode, SafeRectNode } from './rect'; +import { CircleNode, SafeCircleNode } from './circle'; +import { EllipseNode, SafeEllipseNode } from './ellipse'; +import { SafeTspanNode, TspanNode } from './tspan'; +import { GNode, SafeGNode } from './g'; +import { DefsNode, SafeDefsNode } from './defs'; + +export type Viewbox = { + minX: number; + minY: number; + maxX: number; + maxY: number; +}; + +export type PreserveAspectRatio = { + align: + | 'none' + | 'xMinYMin' + | 'xMidYMin' + | 'xMaxYMin' + | 'xMinYMid' + | 'xMidYMid' + | 'xMaxYMid' + | 'xMinYMax' + | 'xMidYMax' + | 'xMaxYMax'; + meetOrSlice: 'meet' | 'slice'; +}; + +interface SvgProps extends NodeProps, SVGPresentationAttributes { + width?: string | number; + height?: string | number; + viewBox?: string | Viewbox; + preserveAspectRatio?: string | PreserveAspectRatio; +} + +export type SvgNode = { + type: typeof P.Svg; + props: SvgProps; + style?: Style | Style[]; + box?: Box; + origin?: Origin; + yogaNode?: YogaNode; + children?: ( + | LineNode + | PolylineNode + | PolygonNode + | PathNode + | RectNode + | CircleNode + | EllipseNode + | TspanNode + | GNode + | DefsNode + )[]; +}; + +export type SafeSvgNode = Omit & { + style: SafeStyle; + children?: ( + | SafeLineNode + | SafePolylineNode + | SafePolygonNode + | SafePathNode + | SafeRectNode + | SafeCircleNode + | SafeEllipseNode + | SafeTspanNode + | SafeGNode + | SafeDefsNode + )[]; +}; diff --git a/packages/layout/src/types/text-input.ts b/packages/layout/src/types/text-input.ts new file mode 100644 index 000000000..558e6be65 --- /dev/null +++ b/packages/layout/src/types/text-input.ts @@ -0,0 +1,60 @@ +import * as P from '@react-pdf/primitives'; +import { SafeStyle, Style } from '@react-pdf/stylesheet'; +import { YogaNode } from 'yoga-layout/load'; + +import { Box, FormCommonProps, Origin } from './base'; + +// see http://pdfkit.org/docs/forms.html#text_field_formatting +interface TextInputFormatting { + type: + | 'date' + | 'time' + | 'percent' + | 'number' + | 'zip' + | 'zipPlus4' + | 'phone' + | 'ssn'; + param?: string; + nDec?: number; + sepComma?: boolean; + negStyle?: 'MinusBlack' | 'Red' | 'ParensBlack' | 'ParensRed'; + currency?: string; + currencyPrepend?: boolean; +} +// see http://pdfkit.org/docs/forms.html#text_field_formatting +interface TextInputProps extends FormCommonProps { + align?: 'left' | 'center' | 'right'; + multiline?: boolean; + /** + * The text will be masked (e.g. with asterisks). + */ + password?: boolean; + /** + * If set, text entered in the field is not spell-checked + */ + noSpell?: boolean; + format?: TextInputFormatting; + /** + * Sets the fontSize (default or 0 means auto sizing) + */ + fontSize?: number; + /** + * Sets the maximum length (characters) of the text in the field + */ + maxLength?: number; +} + +export type TextInputNode = { + type: typeof P.TextInput; + props: TextInputProps; + style?: Style | Style[]; + box?: Box; + origin?: Origin; + yogaNode?: YogaNode; + children?: never[]; +}; + +export type SafeTextInputNode = Omit & { + style: SafeStyle; +}; diff --git a/packages/layout/src/types/text-instance.ts b/packages/layout/src/types/text-instance.ts new file mode 100644 index 000000000..83c1f01cc --- /dev/null +++ b/packages/layout/src/types/text-instance.ts @@ -0,0 +1,14 @@ +import * as P from '@react-pdf/primitives'; + +export type TextInstanceNode = { + type: typeof P.TextInstance; + props?: never; + style?: never; + box?: never; + origin?: never; + children?: never[]; + yogaNode?: never; + value: string; +}; + +export type SafeTextInstanceNode = TextInstanceNode; diff --git a/packages/layout/src/types/text.ts b/packages/layout/src/types/text.ts new file mode 100644 index 000000000..6f2256e0d --- /dev/null +++ b/packages/layout/src/types/text.ts @@ -0,0 +1,59 @@ +import * as P from '@react-pdf/primitives'; +import { SafeStyle, Style } from '@react-pdf/stylesheet'; +import { HyphenationCallback } from '@react-pdf/font'; +import { YogaNode } from 'yoga-layout/load'; +import { Paragraph } from '@react-pdf/textkit'; + +import { Box, NodeProps, Origin, RenderProp } from './base'; +import { SafeTextInstanceNode, TextInstanceNode } from './text-instance'; +import { ImageNode, SafeImageNode } from './image'; +import { SafeTspanNode, TspanNode } from './tspan'; + +interface TextProps extends NodeProps { + /** + * Enable/disable page wrapping for element. + * @see https://react-pdf.org/components#page-wrapping + */ + wrap?: boolean; + render?: RenderProp; + /** + * Override the default hyphenation-callback + * @see https://react-pdf.org/fonts#registerhyphenationcallback + */ + hyphenationCallback?: HyphenationCallback; + /** + * Specifies the minimum number of lines in a text element that must be shown at the bottom of a page or its container. + * @see https://react-pdf.org/advanced#orphan-&-widow-protection + */ + orphans?: number; + /** + * Specifies the minimum number of lines in a text element that must be shown at the top of a page or its container.. + * @see https://react-pdf.org/advanced#orphan-&-widow-protection + */ + widows?: number; + // Svg props + x?: number; + y?: number; +} + +export type TextNode = { + type: typeof P.Text; + props: TextProps; + style?: Style | Style[]; + box?: Box; + origin?: Origin; + yogaNode?: YogaNode; + lines?: Paragraph; + alignOffset?: number; // TODO: Remove this + children?: (TextNode | TextInstanceNode | ImageNode | TspanNode)[]; +}; + +export type SafeTextNode = Omit & { + style: SafeStyle; + children?: ( + | SafeTextNode + | SafeTextInstanceNode + | SafeImageNode + | SafeTspanNode + )[]; +}; diff --git a/packages/layout/src/types/tspan.ts b/packages/layout/src/types/tspan.ts new file mode 100644 index 000000000..10b87b658 --- /dev/null +++ b/packages/layout/src/types/tspan.ts @@ -0,0 +1,25 @@ +import * as P from '@react-pdf/primitives'; +import { SafeStyle, Style } from '@react-pdf/stylesheet'; + +import { SVGPresentationAttributes } from './base'; +import { SafeTextInstanceNode, TextInstanceNode } from './text-instance'; + +interface TspanProps extends SVGPresentationAttributes { + x?: string | number; + y?: string | number; +} + +export type TspanNode = { + type: typeof P.Tspan; + props: TspanProps; + style?: Style | Style[]; + box?: never; + origin?: never; + yogaNode?: never; + children?: TextInstanceNode[]; +}; + +export type SafeTspanNode = Omit & { + style: SafeStyle; + children?: SafeTextInstanceNode[]; +}; diff --git a/packages/layout/src/types/view.ts b/packages/layout/src/types/view.ts new file mode 100644 index 000000000..71220483d --- /dev/null +++ b/packages/layout/src/types/view.ts @@ -0,0 +1,63 @@ +import * as P from '@react-pdf/primitives'; +import { SafeStyle, Style } from '@react-pdf/stylesheet'; +import { YogaNode } from 'yoga-layout/load'; + +import { Box, NodeProps, Origin, RenderProp } from './base'; +import { ImageNode, SafeImageNode } from './image'; +import { SafeTextNode, TextNode } from './text'; +import { LinkNode, SafeLinkNode } from './link'; +import { CanvasNode, SafeCanvasNode } from './canvas'; +import { FieldSetNode, SafeFieldSetNode } from './field-set'; +import { SafeTextInputNode, TextInputNode } from './text-input'; +import { ListNode, SafeListNode, SafeSelectNode, SelectNode } from './select'; +import { CheckboxNode } from './checkbox'; +import { NoteNode, SafeNoteNode } from './note'; + +interface ViewProps extends NodeProps { + id?: string; + /** + * Enable/disable page wrapping for element. + * @see https://react-pdf.org/components#page-wrapping + */ + wrap?: boolean; + render?: RenderProp; +} + +export type ViewNode = { + type: typeof P.View; + props: ViewProps; + style?: Style | Style[]; + box?: Box; + origin?: Origin; + yogaNode?: YogaNode; + children?: ( + | ViewNode + | ImageNode + | TextNode + | LinkNode + | CanvasNode + | FieldSetNode + | TextInputNode + | SelectNode + | ListNode + | CheckboxNode + | NoteNode + )[]; +}; + +export type SafeViewNode = Omit & { + style: SafeStyle; + children?: ( + | SafeViewNode + | SafeImageNode + | SafeTextNode + | SafeLinkNode + | SafeCanvasNode + | SafeFieldSetNode + | SafeTextInputNode + | SafeSelectNode + | SafeListNode + | SafeCanvasNode + | SafeNoteNode + )[]; +}; diff --git a/packages/layout/src/yoga/index.js b/packages/layout/src/yoga/index.ts similarity index 79% rename from packages/layout/src/yoga/index.js rename to packages/layout/src/yoga/index.ts index 9d859987c..40099b67d 100644 --- a/packages/layout/src/yoga/index.js +++ b/packages/layout/src/yoga/index.ts @@ -1,6 +1,6 @@ -import { loadYoga as yogaLoadYoga } from 'yoga-layout/load'; +import { type Yoga, loadYoga as yogaLoadYoga } from 'yoga-layout/load'; -let instancePromise; +let instancePromise: Promise; export const loadYoga = async () => { // Yoga WASM binaries must be asynchronously compiled and loaded diff --git a/packages/layout/tests/image/getSource.test.js b/packages/layout/tests/image/getSource.test.js deleted file mode 100644 index db87e2e87..000000000 --- a/packages/layout/tests/image/getSource.test.js +++ /dev/null @@ -1,27 +0,0 @@ -import { describe, expect, test } from 'vitest'; - -import getSource from '../../src/image/getSource'; - -const VALUE = 'gotcha'; - -describe('image getSource', () => { - test('Should get src', () => { - const node = { type: 'IMAGE', props: { src: VALUE } }; - expect(getSource(node)).toEqual(VALUE); - }); - - test('Should get source', () => { - const node = { type: 'IMAGE', props: { source: VALUE } }; - expect(getSource(node)).toEqual(VALUE); - }); - - test('Should get href', () => { - const node = { type: 'IMAGE', props: { href: VALUE } }; - expect(getSource(node)).toEqual(VALUE); - }); - - test('Should get undefined if either present', () => { - const node = { type: 'IMAGE', props: {} }; - expect(getSource(node)).toEqual(undefined); - }); -}); diff --git a/packages/layout/tests/image/getSource.test.ts b/packages/layout/tests/image/getSource.test.ts new file mode 100644 index 000000000..e166354d6 --- /dev/null +++ b/packages/layout/tests/image/getSource.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, test } from 'vitest'; + +import getSource from '../../src/image/getSource'; + +const VALUE = 'gotcha'; + +describe('image getSource', () => { + test('Should get src', () => { + expect( + getSource({ type: 'IMAGE', style: {}, props: { src: VALUE } }), + ).toEqual(VALUE); + }); + + test('Should get source', () => { + expect( + getSource({ type: 'IMAGE', style: {}, props: { source: VALUE } }), + ).toEqual(VALUE); + }); + + test('Should get undefined if either present', () => { + expect(getSource({ type: 'IMAGE', style: {}, props: {} as any })).toEqual( + undefined, + ); + }); +}); diff --git a/packages/layout/tests/image/resolveSource.test.js b/packages/layout/tests/image/resolveSource.test.ts similarity index 100% rename from packages/layout/tests/image/resolveSource.test.js rename to packages/layout/tests/image/resolveSource.test.ts diff --git a/packages/layout/tests/node/getBorderWidth.test.js b/packages/layout/tests/node/getBorderWidth.test.ts similarity index 80% rename from packages/layout/tests/node/getBorderWidth.test.js rename to packages/layout/tests/node/getBorderWidth.test.ts index f877b4812..dfcd0d92e 100644 --- a/packages/layout/tests/node/getBorderWidth.test.js +++ b/packages/layout/tests/node/getBorderWidth.test.ts @@ -13,7 +13,12 @@ const getComputedBorder = (value) => { describe('node getBorderWidth', () => { test('Should return 0 by default if no yoga node available', () => { - const result = getBorderWidth({}); + const result = getBorderWidth({ + type: 'VIEW', + props: {}, + style: {}, + children: [], + }); expect(result).toHaveProperty('borderTopWidth', 0); expect(result).toHaveProperty('borderRightWidth', 0); @@ -23,7 +28,13 @@ describe('node getBorderWidth', () => { test('Should return yoga values if node available', () => { const yogaNode = { getComputedBorder }; - const result = getBorderWidth({ yogaNode }); + const result = getBorderWidth({ + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode, + }); expect(result).toHaveProperty('borderTopWidth', 1); expect(result).toHaveProperty('borderRightWidth', 2); diff --git a/packages/layout/tests/node/getDimension.test.js b/packages/layout/tests/node/getDimension.test.ts similarity index 71% rename from packages/layout/tests/node/getDimension.test.js rename to packages/layout/tests/node/getDimension.test.ts index 0d98d3890..f46df02ad 100644 --- a/packages/layout/tests/node/getDimension.test.js +++ b/packages/layout/tests/node/getDimension.test.ts @@ -7,7 +7,12 @@ const getComputedHeight = () => 20; describe('node getDimension', () => { test('Should return 0 by default if no yoga node available', () => { - const result = getDimension({}); + const result = getDimension({ + type: 'VIEW', + props: {}, + style: {}, + children: [], + }); expect(result).toHaveProperty('width', 0); expect(result).toHaveProperty('height', 0); @@ -15,7 +20,13 @@ describe('node getDimension', () => { test('Should return yoga values if node available', () => { const yogaNode = { getComputedWidth, getComputedHeight }; - const result = getDimension({ yogaNode }); + const result = getDimension({ + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode, + }); expect(result).toHaveProperty('width', 10); expect(result).toHaveProperty('height', 20); diff --git a/packages/layout/tests/node/getMargin.test.js b/packages/layout/tests/node/getMargin.test.ts similarity index 69% rename from packages/layout/tests/node/getMargin.test.js rename to packages/layout/tests/node/getMargin.test.ts index 7a4f4d206..356150663 100644 --- a/packages/layout/tests/node/getMargin.test.js +++ b/packages/layout/tests/node/getMargin.test.ts @@ -13,7 +13,12 @@ const getComputedMargin = (value) => { describe('node getMargin', () => { test('Should return 0 by default if no yoga node available', () => { - const result = getMargin({}); + const result = getMargin({ + type: 'VIEW', + props: {}, + style: {}, + children: [], + }); expect(result).toHaveProperty('marginTop', 0); expect(result).toHaveProperty('marginRight', 0); @@ -23,7 +28,13 @@ describe('node getMargin', () => { test('Should return yoga values if node available', () => { const yogaNode = { getComputedMargin }; - const result = getMargin({ yogaNode }); + const result = getMargin({ + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode, + }); expect(result).toHaveProperty('marginTop', 1); expect(result).toHaveProperty('marginRight', 2); @@ -33,12 +44,22 @@ describe('node getMargin', () => { test('Should return box specific values if available', () => { const result = getMargin({ + type: 'VIEW', + props: {}, + style: {}, box: { + top: 0, + right: 0, + bottom: 0, + left: 0, + width: 100, + height: 200, marginTop: 1, marginRight: 2, marginBottom: 3, marginLeft: 4, }, + children: [], }); expect(result).toHaveProperty('marginTop', 1); @@ -49,15 +70,15 @@ describe('node getMargin', () => { test('Should return style specific values if available', () => { const result = getMargin({ + type: 'VIEW', + props: {}, style: { marginTop: 1, marginRight: 2, marginBottom: 3, marginLeft: 4, - marginVertical: 5, - marginHorizontal: 6, - margin: 7, }, + children: [], }); expect(result).toHaveProperty('marginTop', 1); @@ -65,32 +86,4 @@ describe('node getMargin', () => { expect(result).toHaveProperty('marginBottom', 3); expect(result).toHaveProperty('marginLeft', 4); }); - - test('Should return style axis values if available', () => { - const result = getMargin({ - style: { - marginVertical: 1, - marginHorizontal: 2, - margin: 3, - }, - }); - - expect(result).toHaveProperty('marginTop', 1); - expect(result).toHaveProperty('marginRight', 2); - expect(result).toHaveProperty('marginBottom', 1); - expect(result).toHaveProperty('marginLeft', 2); - }); - - test('Should return generic margin value if available', () => { - const result = getMargin({ - style: { - margin: 1, - }, - }); - - expect(result).toHaveProperty('marginTop', 1); - expect(result).toHaveProperty('marginRight', 1); - expect(result).toHaveProperty('marginBottom', 1); - expect(result).toHaveProperty('marginLeft', 1); - }); }); diff --git a/packages/layout/tests/node/getOrigin.test.js b/packages/layout/tests/node/getOrigin.test.ts similarity index 58% rename from packages/layout/tests/node/getOrigin.test.js rename to packages/layout/tests/node/getOrigin.test.ts index ac6b8bfe6..b899f5b75 100644 --- a/packages/layout/tests/node/getOrigin.test.js +++ b/packages/layout/tests/node/getOrigin.test.ts @@ -3,15 +3,24 @@ import { describe, expect, test } from 'vitest'; import getOrigin from '../../src/node/getOrigin'; describe('node getOrigin', () => { - test('Should return empty object for node without box', () => { - const result = getOrigin({}); + test('Should return null for node without box', () => { + const result = getOrigin({ + type: 'VIEW', + props: {}, + style: {}, + children: [], + }); - expect(result).toEqual({}); + expect(result).toEqual(null); }); test('Should return centered origin by default', () => { const result = getOrigin({ - box: { top: 50, left: 100, width: 300, height: 400 }, + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 50, left: 100, bottom: 0, right: 0, width: 300, height: 400 }, }); expect(result).toHaveProperty('left', 250); @@ -20,8 +29,11 @@ describe('node getOrigin', () => { test('Should return origin adjusted by fixed values', () => { const result = getOrigin({ - box: { top: 50, left: 100, width: 300, height: 400 }, + type: 'VIEW', + props: {}, + children: [], style: { transformOriginX: 100, transformOriginY: 50 }, + box: { top: 50, left: 100, bottom: 0, right: 0, width: 300, height: 400 }, }); expect(result).toHaveProperty('left', 200); @@ -30,8 +42,11 @@ describe('node getOrigin', () => { test('Should return origin adjusted by percent values', () => { const result = getOrigin({ - box: { top: 50, left: 100, width: 300, height: 400 }, + type: 'VIEW', + props: {}, + children: [], style: { transformOriginX: '20%', transformOriginY: '70%' }, + box: { top: 50, left: 100, bottom: 0, right: 0, width: 300, height: 400 }, }); expect(result).toHaveProperty('left', 160); diff --git a/packages/layout/tests/node/getPadding.test.js b/packages/layout/tests/node/getPadding.test.ts similarity index 69% rename from packages/layout/tests/node/getPadding.test.js rename to packages/layout/tests/node/getPadding.test.ts index 4b99c2c2c..509eb0070 100644 --- a/packages/layout/tests/node/getPadding.test.js +++ b/packages/layout/tests/node/getPadding.test.ts @@ -13,7 +13,12 @@ const getComputedPadding = (value) => { describe('node getPadding', () => { test('Should return 0 by default if no yoga node available', () => { - const result = getPadding({}); + const result = getPadding({ + type: 'VIEW', + props: {}, + style: {}, + children: [], + }); expect(result).toHaveProperty('paddingTop', 0); expect(result).toHaveProperty('paddingRight', 0); @@ -23,7 +28,13 @@ describe('node getPadding', () => { test('Should return yoga values if node available', () => { const yogaNode = { getComputedPadding }; - const result = getPadding({ yogaNode }); + const result = getPadding({ + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode, + }); expect(result).toHaveProperty('paddingTop', 1); expect(result).toHaveProperty('paddingRight', 2); @@ -33,7 +44,17 @@ describe('node getPadding', () => { test('Should return box specific values if available', () => { const result = getPadding({ + type: 'VIEW', + props: {}, + style: {}, + children: [], box: { + top: 0, + right: 0, + bottom: 0, + left: 0, + width: 100, + height: 100, paddingTop: 1, paddingRight: 2, paddingBottom: 3, @@ -49,14 +70,14 @@ describe('node getPadding', () => { test('Should return style specific values if available', () => { const result = getPadding({ + type: 'VIEW', + props: {}, + children: [], style: { paddingTop: 1, paddingRight: 2, paddingBottom: 3, paddingLeft: 4, - paddingVertical: 5, - paddingHorizontal: 6, - padding: 7, }, }); @@ -65,32 +86,4 @@ describe('node getPadding', () => { expect(result).toHaveProperty('paddingBottom', 3); expect(result).toHaveProperty('paddingLeft', 4); }); - - test('Should return style axis values if available', () => { - const result = getPadding({ - style: { - paddingVertical: 1, - paddingHorizontal: 2, - padding: 3, - }, - }); - - expect(result).toHaveProperty('paddingTop', 1); - expect(result).toHaveProperty('paddingRight', 2); - expect(result).toHaveProperty('paddingBottom', 1); - expect(result).toHaveProperty('paddingLeft', 2); - }); - - test('Should return generic padding value if available', () => { - const result = getPadding({ - style: { - padding: 1, - }, - }); - - expect(result).toHaveProperty('paddingTop', 1); - expect(result).toHaveProperty('paddingRight', 1); - expect(result).toHaveProperty('paddingBottom', 1); - expect(result).toHaveProperty('paddingLeft', 1); - }); }); diff --git a/packages/layout/tests/node/getPosition.test.js b/packages/layout/tests/node/getPosition.test.ts similarity index 79% rename from packages/layout/tests/node/getPosition.test.js rename to packages/layout/tests/node/getPosition.test.ts index a6db89b3a..ec990cd2f 100644 --- a/packages/layout/tests/node/getPosition.test.js +++ b/packages/layout/tests/node/getPosition.test.ts @@ -9,7 +9,12 @@ const getComputedLeft = () => 40; describe('node getPosition', () => { test('Should return 0 by default if no yoga node available', () => { - const result = getPosition({}); + const result = getPosition({ + type: 'VIEW', + props: {}, + style: {}, + children: [], + }); expect(result).toHaveProperty('top', 0); expect(result).toHaveProperty('right', 0); @@ -24,7 +29,13 @@ describe('node getPosition', () => { getComputedBottom, getComputedLeft, }; - const result = getPosition({ yogaNode }); + const result = getPosition({ + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode, + }); expect(result).toHaveProperty('top', 10); expect(result).toHaveProperty('right', 20); diff --git a/packages/layout/tests/node/removePaddings.test.js b/packages/layout/tests/node/removePaddings.test.js deleted file mode 100644 index 724078753..000000000 --- a/packages/layout/tests/node/removePaddings.test.js +++ /dev/null @@ -1,45 +0,0 @@ -import { describe, expect, test } from 'vitest'; - -import removePaddings from '../../src/node/removePaddings'; - -describe('node removePaddings', () => { - test('Should keep other styles untouched', () => { - const result = removePaddings({ style: { color: 'red' } }); - expect(result.style).toHaveProperty('color', 'red'); - }); - - test('Should remove paddingTop', () => { - const result = removePaddings({ style: { paddingTop: 10 } }); - expect(result.style).not.toHaveProperty('paddingTop'); - }); - - test('Should remove paddingRight', () => { - const result = removePaddings({ style: { paddingRight: 10 } }); - expect(result.style).not.toHaveProperty('paddingRight'); - }); - - test('Should remove paddingBottom', () => { - const result = removePaddings({ style: { paddingBottom: 10 } }); - expect(result.style).not.toHaveProperty('paddingBottom'); - }); - - test('Should remove paddingLeft', () => { - const result = removePaddings({ style: { paddingLeft: 10 } }); - expect(result.style).not.toHaveProperty('paddingLeft'); - }); - - test('Should remove padding shorthand', () => { - const result = removePaddings({ style: { padding: 10 } }); - expect(result.style).not.toHaveProperty('padding'); - }); - - test('Should remove paddingHorizontal shorthand', () => { - const result = removePaddings({ style: { paddingHorizontal: 10 } }); - expect(result.style).not.toHaveProperty('paddingHorizontal'); - }); - - test('Should remove paddingVertical shorthand', () => { - const result = removePaddings({ style: { paddingVertical: 10 } }); - expect(result.style).not.toHaveProperty('paddingVertical'); - }); -}); diff --git a/packages/layout/tests/node/removePaddings.test.ts b/packages/layout/tests/node/removePaddings.test.ts new file mode 100644 index 000000000..f4c8b40e7 --- /dev/null +++ b/packages/layout/tests/node/removePaddings.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, test } from 'vitest'; + +import removePaddings from '../../src/node/removePaddings'; + +describe('node removePaddings', () => { + test('Should keep other styles untouched', () => { + const result = removePaddings({ + type: 'VIEW', + props: {}, + children: [], + style: { color: 'red' }, + }); + expect(result.style).toHaveProperty('color', 'red'); + }); + + test('Should remove paddingTop', () => { + const result = removePaddings({ + type: 'VIEW', + props: {}, + children: [], + style: { paddingTop: 10 }, + }); + expect(result.style).not.toHaveProperty('paddingTop'); + }); + + test('Should remove paddingRight', () => { + const result = removePaddings({ + type: 'VIEW', + props: {}, + children: [], + style: { paddingRight: 10 }, + }); + expect(result.style).not.toHaveProperty('paddingRight'); + }); + + test('Should remove paddingBottom', () => { + const result = removePaddings({ + type: 'VIEW', + props: {}, + children: [], + style: { paddingBottom: 10 }, + }); + expect(result.style).not.toHaveProperty('paddingBottom'); + }); + + test('Should remove paddingLeft', () => { + const result = removePaddings({ + type: 'VIEW', + props: {}, + children: [], + style: { paddingLeft: 10 }, + }); + expect(result.style).not.toHaveProperty('paddingLeft'); + }); + + test('Should remove padding shorthand', () => { + const result = removePaddings({ + type: 'VIEW', + props: {}, + children: [], + style: { padding: 10 } as any, + }); + expect(result.style).not.toHaveProperty('padding'); + }); + + test('Should remove paddingHorizontal shorthand', () => { + const result = removePaddings({ + type: 'VIEW', + props: {}, + children: [], + style: { paddingHorizontal: 10 } as any, + }); + expect(result.style).not.toHaveProperty('paddingHorizontal'); + }); + + test('Should remove paddingVertical shorthand', () => { + const result = removePaddings({ + type: 'VIEW', + props: {}, + children: [], + style: { paddingVertical: 10 } as any, + }); + expect(result.style).not.toHaveProperty('paddingVertical'); + }); +}); diff --git a/packages/layout/tests/node/setAlignContent.test.js b/packages/layout/tests/node/setAlignContent.test.ts similarity index 87% rename from packages/layout/tests/node/setAlignContent.test.js rename to packages/layout/tests/node/setAlignContent.test.ts index e66afe4a5..ec49fd1f4 100644 --- a/packages/layout/tests/node/setAlignContent.test.js +++ b/packages/layout/tests/node/setAlignContent.test.ts @@ -3,17 +3,30 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import * as Yoga from 'yoga-layout/load'; import setAlignContent from '../../src/node/setAlignContent'; +import { SafeNode } from '../../src/types'; describe('node setAlignContent', () => { const mock = vi.fn(); - const node = { yogaNode: { setAlignContent: mock } }; + const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode: { setAlignContent: mock }, + } as SafeNode; beforeEach(() => { mock.mockReset(); }); test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, bottom: 0, left: 0, width: 10, height: 20 }, + } as SafeNode; const result = setAlignContent(null)(emptyNode); expect(result).toBe(emptyNode); diff --git a/packages/layout/tests/node/setAlignItems.test.js b/packages/layout/tests/node/setAlignItems.test.ts similarity index 86% rename from packages/layout/tests/node/setAlignItems.test.js rename to packages/layout/tests/node/setAlignItems.test.ts index dd094b711..46733ad78 100644 --- a/packages/layout/tests/node/setAlignItems.test.js +++ b/packages/layout/tests/node/setAlignItems.test.ts @@ -3,17 +3,30 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import * as Yoga from 'yoga-layout/load'; import setAlignItems from '../../src/node/setAlignItems'; +import { SafeNode } from '../../src/types'; describe('node setAlignItems', () => { const mock = vi.fn(); - const node = { yogaNode: { setAlignItems: mock } }; + const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode: { setAlignItems: mock }, + } as SafeNode; beforeEach(() => { mock.mockReset(); }); test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, left: 0, bottom: 0, width: 10, height: 20 }, + } as SafeNode; const result = setAlignItems(null)(emptyNode); expect(result).toBe(emptyNode); diff --git a/packages/layout/tests/node/setAlignSelf.test.js b/packages/layout/tests/node/setAlignSelf.test.ts similarity index 86% rename from packages/layout/tests/node/setAlignSelf.test.js rename to packages/layout/tests/node/setAlignSelf.test.ts index d01bd0fa4..34f15e602 100644 --- a/packages/layout/tests/node/setAlignSelf.test.js +++ b/packages/layout/tests/node/setAlignSelf.test.ts @@ -3,17 +3,30 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import * as Yoga from 'yoga-layout/load'; import setAlignSelf from '../../src/node/setAlignSelf'; +import { SafeNode } from '../../src/types'; describe('node setAlignSelf', () => { const mock = vi.fn(); - const node = { yogaNode: { setAlignSelf: mock } }; + const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode: { setAlignSelf: mock }, + } as SafeNode; beforeEach(() => { mock.mockReset(); }); test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, left: 0, bottom: 0, width: 10, height: 20 }, + } as SafeNode; const result = setAlignSelf(null)(emptyNode); expect(result).toBe(emptyNode); diff --git a/packages/layout/tests/node/setAspectRatio.test.js b/packages/layout/tests/node/setAspectRatio.test.ts similarity index 62% rename from packages/layout/tests/node/setAspectRatio.test.js rename to packages/layout/tests/node/setAspectRatio.test.ts index 40d9f6626..1fc97aeed 100644 --- a/packages/layout/tests/node/setAspectRatio.test.js +++ b/packages/layout/tests/node/setAspectRatio.test.ts @@ -1,16 +1,29 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import setAspectRatio from '../../src/node/setAspectRatio'; +import { SafeNode } from '../../src/types'; describe('node setAspectRatio', () => { const mock = vi.fn(); - const node = { yogaNode: { setAspectRatio: mock } }; + const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode: { setAspectRatio: mock }, + } as SafeNode; beforeEach(() => { mock.mockReset(); }); test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, left: 0, bottom: 0, width: 10, height: 20 }, + } as SafeNode; const result = setAspectRatio(null)(emptyNode); expect(result).toBe(emptyNode); diff --git a/packages/layout/tests/node/setBorderWidth.test.js b/packages/layout/tests/node/setBorderWidth.test.ts similarity index 89% rename from packages/layout/tests/node/setBorderWidth.test.js rename to packages/layout/tests/node/setBorderWidth.test.ts index 6a350258e..01fb18c21 100644 --- a/packages/layout/tests/node/setBorderWidth.test.js +++ b/packages/layout/tests/node/setBorderWidth.test.ts @@ -8,10 +8,26 @@ import setBorder, { setBorderBottom, setBorderLeft, } from '../../src/node/setBorderWidth'; +import { SafeNode } from '../../src/types'; describe('node setBorderWidth', () => { const mock = vi.fn(); - const node = { yogaNode: { setBorder: mock } }; + + const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode: { setBorder: mock }, + } as SafeNode; + + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, left: 0, bottom: 0, width: 10, height: 20 }, + } as SafeNode; beforeEach(() => { mock.mockReset(); @@ -19,7 +35,6 @@ describe('node setBorderWidth', () => { describe('setBorderTop', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setBorderTop(null)(emptyNode); expect(result).toBe(emptyNode); @@ -41,7 +56,6 @@ describe('node setBorderWidth', () => { describe('setBorderRight', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setBorderRight(null)(emptyNode); expect(result).toBe(emptyNode); @@ -63,7 +77,6 @@ describe('node setBorderWidth', () => { describe('setBorderBottom', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setBorderBottom(null)(emptyNode); expect(result).toBe(emptyNode); @@ -85,7 +98,6 @@ describe('node setBorderWidth', () => { describe('setBorderLeft', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setBorderLeft(null)(emptyNode); expect(result).toBe(emptyNode); @@ -107,7 +119,6 @@ describe('node setBorderWidth', () => { describe('setBorder', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setBorder(null)(emptyNode); expect(result).toBe(emptyNode); @@ -125,7 +136,7 @@ describe('node setBorderWidth', () => { }); test('Should throw error for percent values', () => { - expect(() => setBorder('50%')(node)).toThrow(); + expect(() => setBorder('50%' as any)(node)).toThrow(); }); }); }); diff --git a/packages/layout/tests/node/setDimension.test.js b/packages/layout/tests/node/setDimension.test.ts similarity index 93% rename from packages/layout/tests/node/setDimension.test.js rename to packages/layout/tests/node/setDimension.test.ts index 4e4c2e239..4c6c1cbfb 100644 --- a/packages/layout/tests/node/setDimension.test.js +++ b/packages/layout/tests/node/setDimension.test.ts @@ -7,6 +7,7 @@ import { setMinHeight, setMaxHeight, } from '../../src/node/setDimension'; +import { SafeNode } from '../../src/types'; describe('node setDimension', () => { const mockSetWidth = vi.fn(); @@ -19,6 +20,10 @@ describe('node setDimension', () => { const mockSetMaxHeight = vi.fn(); const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], yogaNode: { setWidth: mockSetWidth, setWidthPercent: mockSetWidthPercent, @@ -29,7 +34,15 @@ describe('node setDimension', () => { setMinHeight: mockSetMinHeight, setMaxHeight: mockSetMaxHeight, }, - }; + } as SafeNode; + + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, bottom: 0, left: 0, width: 10, height: 20 }, + } as SafeNode; beforeEach(() => { mockSetWidth.mockReset(); @@ -44,7 +57,6 @@ describe('node setDimension', () => { describe('setWidth', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setWidth(null)(emptyNode); expect(result).toBe(emptyNode); @@ -71,7 +83,6 @@ describe('node setDimension', () => { describe('setMinWidth', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setMinWidth(null)(emptyNode); expect(result).toBe(emptyNode); @@ -93,7 +104,6 @@ describe('node setDimension', () => { describe('setMaxWidth', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setMaxWidth(null)(emptyNode); expect(result).toBe(emptyNode); @@ -115,7 +125,6 @@ describe('node setDimension', () => { describe('setHeight', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setHeight(null)(emptyNode); expect(result).toBe(emptyNode); @@ -142,7 +151,6 @@ describe('node setDimension', () => { describe('setMinHeight', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setMinWidth(null)(emptyNode); expect(result).toBe(emptyNode); @@ -164,7 +172,6 @@ describe('node setDimension', () => { describe('setMaxHeight', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setMaxHeight(null)(emptyNode); expect(result).toBe(emptyNode); diff --git a/packages/layout/tests/node/setDisplay.test.js b/packages/layout/tests/node/setDisplay.test.ts similarity index 74% rename from packages/layout/tests/node/setDisplay.test.js rename to packages/layout/tests/node/setDisplay.test.ts index d53b5abf9..0aad067fc 100644 --- a/packages/layout/tests/node/setDisplay.test.js +++ b/packages/layout/tests/node/setDisplay.test.ts @@ -3,17 +3,31 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import * as Yoga from 'yoga-layout/load'; import setDisplay from '../../src/node/setDisplay'; +import { SafeNode } from '../../src/types'; describe('node setDisplay', () => { const mock = vi.fn(); - const node = { yogaNode: { setDisplay: mock } }; + const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode: { setDisplay: mock }, + } as SafeNode; beforeEach(() => { mock.mockReset(); }); test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, bottom: 0, left: 0, width: 10, height: 20 }, + } as SafeNode; + const result = setDisplay(null)(emptyNode); expect(result).toBe(emptyNode); diff --git a/packages/layout/tests/node/setFlexBasis.test.js b/packages/layout/tests/node/setFlexBasis.test.ts similarity index 61% rename from packages/layout/tests/node/setFlexBasis.test.js rename to packages/layout/tests/node/setFlexBasis.test.ts index c24c215c5..f9f3e89a6 100644 --- a/packages/layout/tests/node/setFlexBasis.test.js +++ b/packages/layout/tests/node/setFlexBasis.test.ts @@ -1,16 +1,30 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import setFlexBasis from '../../src/node/setFlexBasis'; +import { SafeNode } from '../../src/types'; describe('node setFlexBasis', () => { const mock = vi.fn(); - const node = { yogaNode: { setFlexBasis: mock } }; + const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode: { setFlexBasis: mock }, + } as SafeNode; beforeEach(() => { mock.mockReset(); }); test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, bottom: 0, left: 0, width: 10, height: 20 }, + } as SafeNode; + const result = setFlexBasis(null)(emptyNode); expect(result).toBe(emptyNode); diff --git a/packages/layout/tests/node/setFlexDirection.test.js b/packages/layout/tests/node/setFlexDirection.test.ts similarity index 81% rename from packages/layout/tests/node/setFlexDirection.test.js rename to packages/layout/tests/node/setFlexDirection.test.ts index 0a5917942..5dc9ff1f3 100644 --- a/packages/layout/tests/node/setFlexDirection.test.js +++ b/packages/layout/tests/node/setFlexDirection.test.ts @@ -3,17 +3,31 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import * as Yoga from 'yoga-layout/load'; import setFlexDirection from '../../src/node/setFlexDirection'; +import { SafeNode } from '../../src/types'; describe('node setFlexDirection', () => { const mock = vi.fn(); - const node = { yogaNode: { setFlexDirection: mock } }; + const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode: { setFlexDirection: mock }, + } as SafeNode; beforeEach(() => { mock.mockReset(); }); test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, bottom: 0, left: 0, width: 10, height: 20 }, + } as SafeNode; + const result = setFlexDirection(null)(emptyNode); expect(result).toBe(emptyNode); diff --git a/packages/layout/tests/node/setFlexGrow.test.js b/packages/layout/tests/node/setFlexGrow.test.ts similarity index 68% rename from packages/layout/tests/node/setFlexGrow.test.js rename to packages/layout/tests/node/setFlexGrow.test.ts index f88fc5840..d9c50d57e 100644 --- a/packages/layout/tests/node/setFlexGrow.test.js +++ b/packages/layout/tests/node/setFlexGrow.test.ts @@ -1,16 +1,30 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import setFlexGrow from '../../src/node/setFlexGrow'; +import { SafeNode } from '../../src/types'; describe('node setFlexGrow', () => { const mock = vi.fn(); - const node = { yogaNode: { setFlexGrow: mock } }; + const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode: { setFlexGrow: mock }, + } as SafeNode; beforeEach(() => { mock.mockReset(); }); test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, bottom: 0, left: 0, width: 10, height: 20 }, + } as SafeNode; + const result = setFlexGrow(null)(emptyNode); expect(result).toBe(emptyNode); diff --git a/packages/layout/tests/node/setFlexShrink.test.js b/packages/layout/tests/node/setFlexShrink.test.ts similarity index 68% rename from packages/layout/tests/node/setFlexShrink.test.js rename to packages/layout/tests/node/setFlexShrink.test.ts index 03a910d21..e0a6db1a5 100644 --- a/packages/layout/tests/node/setFlexShrink.test.js +++ b/packages/layout/tests/node/setFlexShrink.test.ts @@ -1,16 +1,30 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import setFlexShrink from '../../src/node/setFlexShrink'; +import { SafeNode } from '../../src/types'; describe('node setFlexShrink', () => { const mock = vi.fn(); - const node = { yogaNode: { setFlexShrink: mock } }; + const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode: { setFlexShrink: mock }, + } as SafeNode; beforeEach(() => { mock.mockReset(); }); test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, bottom: 0, left: 0, width: 10, height: 20 }, + } as SafeNode; + const result = setFlexShrink(null)(emptyNode); expect(result).toBe(emptyNode); diff --git a/packages/layout/tests/node/setFlexWrap.test.js b/packages/layout/tests/node/setFlexWrap.test.ts similarity index 78% rename from packages/layout/tests/node/setFlexWrap.test.js rename to packages/layout/tests/node/setFlexWrap.test.ts index 56a285df6..37d0435fd 100644 --- a/packages/layout/tests/node/setFlexWrap.test.js +++ b/packages/layout/tests/node/setFlexWrap.test.ts @@ -3,17 +3,31 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import * as Yoga from 'yoga-layout/load'; import setFlexWrap from '../../src/node/setFlexWrap'; +import { SafeNode } from '../../src/types'; describe('node setFlexWrap', () => { const mock = vi.fn(); - const node = { yogaNode: { setFlexWrap: mock } }; + const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode: { setFlexWrap: mock }, + } as SafeNode; beforeEach(() => { mock.mockReset(); }); test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, bottom: 0, left: 0, width: 10, height: 20 }, + } as SafeNode; + const result = setFlexWrap(null)(emptyNode); expect(result).toBe(emptyNode); diff --git a/packages/layout/tests/node/setJustifyContent.test.js b/packages/layout/tests/node/setJustifyContent.test.ts similarity index 83% rename from packages/layout/tests/node/setJustifyContent.test.js rename to packages/layout/tests/node/setJustifyContent.test.ts index e517b12e8..420c5155f 100644 --- a/packages/layout/tests/node/setJustifyContent.test.js +++ b/packages/layout/tests/node/setJustifyContent.test.ts @@ -3,17 +3,31 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import * as Yoga from 'yoga-layout/load'; import setJustifyContent from '../../src/node/setJustifyContent'; +import { SafeNode } from '../../src/types'; describe('node setJustifyContent', () => { const mock = vi.fn(); - const node = { yogaNode: { setJustifyContent: mock } }; + const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode: { setJustifyContent: mock }, + } as SafeNode; beforeEach(() => { mock.mockReset(); }); test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, bottom: 0, left: 0, width: 10, height: 20 }, + } as SafeNode; + const result = setJustifyContent(null)(emptyNode); expect(result).toBe(emptyNode); diff --git a/packages/layout/tests/node/setMargin.test.js b/packages/layout/tests/node/setMargin.test.ts similarity index 95% rename from packages/layout/tests/node/setMargin.test.js rename to packages/layout/tests/node/setMargin.test.ts index 488227ece..cf3b0e14e 100644 --- a/packages/layout/tests/node/setMargin.test.js +++ b/packages/layout/tests/node/setMargin.test.ts @@ -8,6 +8,7 @@ import setMargin, { setMarginBottom, setMarginLeft, } from '../../src/node/setMargin'; +import { SafeNode } from '../../src/types'; describe('node setMargin', () => { const mock = vi.fn(); @@ -15,12 +16,24 @@ describe('node setMargin', () => { const mockPercent = vi.fn(); const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], yogaNode: { setMargin: mock, setMarginAuto: mockAuto, setMarginPercent: mockPercent, }, - }; + } as SafeNode; + + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, bottom: 0, left: 0, width: 10, height: 20 }, + } as SafeNode; beforeEach(() => { mock.mockReset(); @@ -30,7 +43,6 @@ describe('node setMargin', () => { describe('setMarginTop', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setMarginTop(null)(emptyNode); expect(result).toBe(emptyNode); @@ -65,7 +77,6 @@ describe('node setMargin', () => { describe('setMarginRight', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setMarginRight(null)(emptyNode); expect(result).toBe(emptyNode); @@ -100,7 +111,6 @@ describe('node setMargin', () => { describe('setMarginBottom', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setMarginBottom(null)(emptyNode); expect(result).toBe(emptyNode); @@ -135,7 +145,6 @@ describe('node setMargin', () => { describe('setMarginLeft', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setMarginLeft(null)(emptyNode); expect(result).toBe(emptyNode); @@ -170,7 +179,6 @@ describe('node setMargin', () => { describe('setMargin', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setMargin(null)(emptyNode); expect(result).toBe(emptyNode); diff --git a/packages/layout/tests/node/setOverflow.test.js b/packages/layout/tests/node/setOverflow.test.ts similarity index 75% rename from packages/layout/tests/node/setOverflow.test.js rename to packages/layout/tests/node/setOverflow.test.ts index 7ff0e8a7f..288564cac 100644 --- a/packages/layout/tests/node/setOverflow.test.js +++ b/packages/layout/tests/node/setOverflow.test.ts @@ -3,17 +3,31 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import * as Yoga from 'yoga-layout/load'; import setOverflow from '../../src/node/setOverflow'; +import { SafeNode } from '../../src/types'; describe('node setOverflow', () => { const mock = vi.fn(); - const node = { yogaNode: { setOverflow: mock } }; + const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode: { setOverflow: mock }, + } as SafeNode; beforeEach(() => { mock.mockReset(); }); test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, bottom: 0, left: 0, width: 10, height: 20 }, + } as SafeNode; + const result = setOverflow(null)(emptyNode); expect(result).toBe(emptyNode); diff --git a/packages/layout/tests/node/setPadding.test.js b/packages/layout/tests/node/setPadding.test.ts similarity index 94% rename from packages/layout/tests/node/setPadding.test.js rename to packages/layout/tests/node/setPadding.test.ts index 3d836f826..19b7d6259 100644 --- a/packages/layout/tests/node/setPadding.test.js +++ b/packages/layout/tests/node/setPadding.test.ts @@ -8,14 +8,27 @@ import setPadding, { setPaddingBottom, setPaddingLeft, } from '../../src/node/setPadding'; +import { SafeNode } from '../../src/types'; describe('node setPadding', () => { const mock = vi.fn(); const mockPercent = vi.fn(); const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], yogaNode: { setPadding: mock, setPaddingPercent: mockPercent }, - }; + } as SafeNode; + + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, bottom: 0, left: 0, width: 10, height: 20 }, + } as SafeNode; beforeEach(() => { mock.mockReset(); @@ -24,7 +37,6 @@ describe('node setPadding', () => { describe('setPaddingTop', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setPaddingTop(null)(emptyNode); expect(result).toBe(emptyNode); @@ -51,7 +63,6 @@ describe('node setPadding', () => { describe('setPaddingRight', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setPaddingRight(null)(emptyNode); expect(result).toBe(emptyNode); @@ -78,7 +89,6 @@ describe('node setPadding', () => { describe('setPaddingBottom', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setPaddingBottom(null)(emptyNode); expect(result).toBe(emptyNode); @@ -105,7 +115,6 @@ describe('node setPadding', () => { describe('setPaddingLeft', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setPaddingLeft(null)(emptyNode); expect(result).toBe(emptyNode); @@ -132,7 +141,6 @@ describe('node setPadding', () => { describe('setPadding', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setPadding(null)(emptyNode); expect(result).toBe(emptyNode); diff --git a/packages/layout/tests/node/setPosition.test.js b/packages/layout/tests/node/setPosition.test.ts similarity index 94% rename from packages/layout/tests/node/setPosition.test.js rename to packages/layout/tests/node/setPosition.test.ts index 4e54e9fd2..8781cd7e1 100644 --- a/packages/layout/tests/node/setPosition.test.js +++ b/packages/layout/tests/node/setPosition.test.ts @@ -8,14 +8,27 @@ import setPosition, { setPositionBottom, setPositionLeft, } from '../../src/node/setPosition'; +import { SafeNode } from '../../src/types'; describe('node setPosition', () => { const mock = vi.fn(); const mockPercent = vi.fn(); const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], yogaNode: { setPosition: mock, setPositionPercent: mockPercent }, - }; + } as SafeNode; + + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, bottom: 0, left: 0, width: 10, height: 20 }, + } as SafeNode; beforeEach(() => { mock.mockReset(); @@ -24,7 +37,6 @@ describe('node setPosition', () => { describe('setPositionTop', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setPositionTop(null)(emptyNode); expect(result).toBe(emptyNode); @@ -51,7 +63,6 @@ describe('node setPosition', () => { describe('setPositionRight', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setPositionRight(null)(emptyNode); expect(result).toBe(emptyNode); @@ -78,7 +89,6 @@ describe('node setPosition', () => { describe('setPositionBottom', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setPositionBottom(null)(emptyNode); expect(result).toBe(emptyNode); @@ -105,7 +115,6 @@ describe('node setPosition', () => { describe('setPositionLeft', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setPositionLeft(null)(emptyNode); expect(result).toBe(emptyNode); @@ -132,7 +141,6 @@ describe('node setPosition', () => { describe('setPosition', () => { test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; const result = setPosition(null)(emptyNode); expect(result).toBe(emptyNode); diff --git a/packages/layout/tests/node/setPositionType.test.js b/packages/layout/tests/node/setPositionType.test.ts similarity index 75% rename from packages/layout/tests/node/setPositionType.test.js rename to packages/layout/tests/node/setPositionType.test.ts index 528793d4b..6f442ed1e 100644 --- a/packages/layout/tests/node/setPositionType.test.js +++ b/packages/layout/tests/node/setPositionType.test.ts @@ -3,17 +3,31 @@ import { beforeEach, describe, expect, test, vi } from 'vitest'; import * as Yoga from 'yoga-layout/load'; import setPositionType from '../../src/node/setPositionType'; +import { SafeNode } from '../../src/types'; describe('node setPositionType', () => { const mock = vi.fn(); - const node = { yogaNode: { setPositionType: mock } }; + const node = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + yogaNode: { setPositionType: mock }, + } as SafeNode; beforeEach(() => { mock.mockReset(); }); test('should return node if no yoga node available', () => { - const emptyNode = { box: { width: 10, height: 20 } }; + const emptyNode = { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 0, right: 0, bottom: 0, left: 0, width: 10, height: 20 }, + } as SafeNode; + const result = setPositionType(null)(emptyNode); expect(result).toBe(emptyNode); diff --git a/packages/layout/tests/node/shouldBreak.test.js b/packages/layout/tests/node/shouldBreak.test.ts similarity index 59% rename from packages/layout/tests/node/shouldBreak.test.js rename to packages/layout/tests/node/shouldBreak.test.ts index d0df84f77..e92947ddf 100644 --- a/packages/layout/tests/node/shouldBreak.test.js +++ b/packages/layout/tests/node/shouldBreak.test.ts @@ -7,7 +7,11 @@ describe('node shouldBreak', () => { test('should not break when the child has enough space on the page', () => { const result = shouldBreak( { - box: { top: 50, height: 400 }, + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { top: 50, right: 0, bottom: 0, left: 0, height: 400, width: 200 }, }, [], 1000, @@ -19,8 +23,11 @@ describe('node shouldBreak', () => { test('should break when the child has enough space on the page', () => { const result = shouldBreak( { - box: { top: 50, height: 400 }, + type: 'VIEW', props: { break: true }, + style: {}, + children: [], + box: { top: 50, right: 0, bottom: 0, left: 0, height: 400, width: 200 }, }, [], 1000, @@ -32,8 +39,18 @@ describe('node shouldBreak', () => { test('should not break when the child can be wrapped', () => { const result = shouldBreak( { - box: { top: 50, height: 1400 }, + type: 'VIEW', props: { wrap: true }, + style: {}, + children: [], + box: { + top: 50, + right: 0, + bottom: 0, + left: 0, + height: 1400, + width: 200, + }, }, [], 1000, @@ -46,8 +63,17 @@ describe('node shouldBreak', () => { const result = shouldBreak( { type: P.Image, - box: { top: 50, height: 1400 }, - props: { wrap: true }, + props: { wrap: true } as any, + style: {}, + children: [], + box: { + top: 50, + right: 0, + bottom: 0, + left: 0, + height: 1400, + width: 200, + }, }, [], 1000, @@ -59,8 +85,18 @@ describe('node shouldBreak', () => { test('should break when the child has wrapping disabled', () => { const result = shouldBreak( { - box: { top: 50, height: 1400 }, + type: 'VIEW', props: { wrap: false }, + style: {}, + children: [], + box: { + top: 50, + right: 0, + bottom: 0, + left: 0, + height: 1400, + width: 200, + }, }, [], 1000, @@ -72,10 +108,39 @@ describe('node shouldBreak', () => { test('should break when minPresenceAhead is large enough and there are overflowing siblings after the child', () => { const result = shouldBreak( { - box: { top: 500, height: 400, marginTop: 0, marginBottom: 0 }, + type: 'VIEW', props: { minPresenceAhead: 400 }, + style: {}, + children: [], + box: { + top: 500, + right: 0, + bottom: 0, + left: 0, + height: 400, + width: 200, + marginTop: 0, + marginBottom: 0, + }, }, - [{ box: { top: 900, height: 200, marginTop: 0, marginBottom: 0 } }], + [ + { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { + top: 900, + right: 0, + bottom: 0, + left: 0, + height: 200, + width: 200, + marginTop: 0, + marginBottom: 0, + }, + }, + ], 1000, ); @@ -85,10 +150,39 @@ describe('node shouldBreak', () => { test('should break when minPresenceAhead is large enough and there are overflowing siblings due to margins after the child', () => { const result = shouldBreak( { - box: { top: 500, height: 400, marginTop: 0, marginBottom: 0 }, + type: 'VIEW', props: { minPresenceAhead: 400 }, + style: {}, + children: [], + box: { + top: 500, + right: 0, + bottom: 0, + left: 0, + height: 400, + width: 200, + marginTop: 0, + marginBottom: 0, + }, }, - [{ box: { top: 1100, height: 0, marginTop: 200, marginBottom: 0 } }], + [ + { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { + top: 1100, + right: 0, + bottom: 0, + left: 0, + height: 0, + width: 200, + marginTop: 200, + marginBottom: 0, + }, + }, + ], 1000, ); @@ -98,10 +192,39 @@ describe('node shouldBreak', () => { test('should not break when minPresenceAhead is not past the page end', () => { const result = shouldBreak( { - box: { top: 500, height: 400, marginTop: 0, marginBottom: 0 }, + type: 'VIEW', props: { minPresenceAhead: 100 }, + style: {}, + children: [], + box: { + top: 500, + right: 0, + bottom: 0, + left: 0, + height: 400, + width: 200, + marginTop: 0, + marginBottom: 0, + }, }, - [{ box: { top: 900, height: 200, marginTop: 0, marginBottom: 0 } }], + [ + { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { + top: 900, + right: 0, + bottom: 0, + left: 0, + height: 200, + width: 200, + marginTop: 0, + marginBottom: 0, + }, + }, + ], 1000, ); @@ -111,10 +234,39 @@ describe('node shouldBreak', () => { test('should not break when the siblings after the child do not overflow past the page end', () => { const result = shouldBreak( { - box: { top: 500, height: 400, marginTop: 0, marginBottom: 0 }, + type: 'VIEW', props: { minPresenceAhead: 400 }, + style: {}, + children: [], + box: { + top: 500, + right: 0, + bottom: 0, + left: 0, + height: 400, + width: 200, + marginTop: 0, + marginBottom: 0, + }, }, - [{ box: { top: 900, height: 100, marginTop: 0, marginBottom: 0 } }], + [ + { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { + top: 900, + right: 0, + bottom: 0, + left: 0, + height: 100, + width: 200, + marginTop: 0, + marginBottom: 0, + }, + }, + ], 1000, ); @@ -124,10 +276,39 @@ describe('node shouldBreak', () => { test('should not break when the siblings after the child do not overflow past the page end, with margins', () => { const result = shouldBreak( { - box: { top: 500, height: 400, marginTop: 0, marginBottom: 0 }, + type: 'VIEW', props: { minPresenceAhead: 400 }, + style: {}, + children: [], + box: { + top: 500, + right: 0, + bottom: 0, + left: 0, + height: 400, + width: 200, + marginTop: 0, + marginBottom: 0, + }, }, - [{ box: { top: 1000, height: 0, marginTop: 100, marginBottom: 0 } }], + [ + { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { + top: 1000, + right: 0, + bottom: 0, + left: 0, + height: 0, + width: 200, + marginTop: 100, + marginBottom: 0, + }, + }, + ], 1000, ); @@ -137,10 +318,39 @@ describe('node shouldBreak', () => { test("should not break when only the last sibling's bottom margin overflows past the page end", () => { const result = shouldBreak( { - box: { top: 500, height: 400, marginTop: 0, marginBottom: 0 }, + type: 'VIEW', props: { minPresenceAhead: 400 }, + style: {}, + children: [], + box: { + top: 500, + right: 0, + bottom: 0, + left: 0, + height: 400, + width: 200, + marginTop: 0, + marginBottom: 0, + }, }, - [{ box: { top: 900, height: 100, marginTop: 0, marginBottom: 100 } }], + [ + { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { + top: 900, + right: 0, + bottom: 0, + left: 0, + height: 100, + width: 200, + marginTop: 0, + marginBottom: 100, + }, + }, + ], 1000, ); @@ -150,10 +360,39 @@ describe('node shouldBreak', () => { test('should not break due to minPresenceAhead when breaking does not improve presence, to avoid infinite loops', () => { const result = shouldBreak( { - box: { top: 500, height: 400, marginTop: 500, marginBottom: 0 }, + type: 'VIEW', props: { minPresenceAhead: 400 }, + style: {}, + children: [], + box: { + top: 500, + right: 0, + bottom: 0, + left: 0, + height: 400, + width: 200, + marginTop: 500, + marginBottom: 0, + }, }, - [{ box: { top: 900, height: 200, marginTop: 0, marginBottom: 0 } }], + [ + { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { + top: 900, + right: 0, + bottom: 0, + left: 0, + height: 200, + width: 200, + marginTop: 0, + marginBottom: 0, + }, + }, + ], 1000, ); @@ -163,10 +402,39 @@ describe('node shouldBreak', () => { test('should never break fixed child', () => { const result = shouldBreak( { - box: { top: 500, height: 400, marginTop: 0, marginBottom: 0 }, + type: 'VIEW', props: { minPresenceAhead: 400, fixed: true }, + style: {}, + children: [], + box: { + top: 500, + right: 0, + bottom: 0, + left: 0, + height: 400, + width: 200, + marginTop: 0, + marginBottom: 0, + }, }, - [{ box: { top: 900, height: 200, marginTop: 0, marginBottom: 0 } }], + [ + { + type: 'VIEW', + props: {}, + style: {}, + children: [], + box: { + top: 900, + right: 0, + bottom: 0, + left: 0, + height: 200, + width: 200, + marginTop: 0, + marginBottom: 0, + }, + }, + ], 1000, ); @@ -176,13 +444,37 @@ describe('node shouldBreak', () => { test('should ignore fixed elements after child', () => { const result = shouldBreak( { - box: { top: 500, height: 400, marginTop: 0, marginBottom: 0 }, + type: 'VIEW', props: { minPresenceAhead: 400, fixed: true }, + style: {}, + children: [], + box: { + top: 500, + right: 0, + bottom: 0, + left: 0, + height: 400, + width: 200, + marginTop: 0, + marginBottom: 0, + }, }, [ { - box: { top: 900, height: 200, marginTop: 0, marginBottom: 0 }, + type: 'VIEW', props: { fixed: true }, + style: {}, + children: [], + box: { + top: 900, + right: 0, + bottom: 0, + left: 0, + height: 200, + width: 200, + marginTop: 0, + marginBottom: 0, + }, }, ], 1000, @@ -195,29 +487,49 @@ describe('node shouldBreak', () => { const result = shouldBreak( { type: 'VIEW', + props: { minPresenceAhead: 100 }, + style: {}, + children: [], box: { top: 30, + right: 0, + bottom: 0, + left: 0, height: 0, + width: 200, marginTop: 0, marginBottom: 0, }, - props: { minPresenceAhead: 100 }, }, [ { type: 'VIEW', + props: {}, + style: {}, + children: [], box: { top: 30, + right: 0, + bottom: 0, + left: 0, height: 70, + width: 200, marginTop: 0, marginBottom: 0, }, }, { type: 'VIEW', + props: {}, + style: {}, + children: [], box: { top: 130, + right: 0, + bottom: 0, + left: 0, height: 0, + width: 200, marginTop: 30, marginBottom: 0, }, @@ -233,29 +545,49 @@ describe('node shouldBreak', () => { const result = shouldBreak( { type: 'VIEW', + props: { minPresenceAhead: 100 }, + style: {}, + children: [], box: { top: 30, + right: 0, + bottom: 0, + left: 0, height: 0, + width: 200, marginTop: 0, marginBottom: 0, }, - props: { minPresenceAhead: 100 }, }, [ { type: 'VIEW', + props: {}, + style: {}, + children: [], box: { top: 30, + right: 0, + bottom: 0, + left: 0, height: 71, + width: 200, marginTop: 0, marginBottom: 0, }, }, { type: 'VIEW', + props: {}, + style: {}, + children: [], box: { top: 131, + right: 0, + bottom: 0, + left: 0, height: 0, + width: 200, marginTop: 30, marginBottom: 0, }, @@ -308,11 +640,6 @@ describe('node shouldBreak', () => { type: 'TEXT_INSTANCE', value: 'En un lugar de la Mancha, de cuyo nombre no quiero acordarme, no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero, adarga antigua, rocín flaco y galgo corredor. Una olla de algo más vaca que carnero, salpicón las más noches, duelos y quebrantos los sábados, lentejas los viernes, algún palomino de añadidura los domingos, consumían las tres partes de su hacienda. El resto della concluían sayo de velarte, calzas de velludo para las fiestas con sus pantuflos de lo mismo, los días de entre semana se honraba con su vellori de lo más fino. Tenía en su casa una ama que pasaba de los cuarenta, y una sobrina que no llegaba a los veinte, y un mozo de campo y plaza, que así ensillaba el rocín como tomaba la podadera. Frisaba la edad de nuestro hidalgo con los cincuenta años, era de complexión recia, seco de carnes, enjuto de rostro; gran madrugador y amigo de la caza. Quieren decir que tenía el sobrenombre de Quijada o Quesada (que en esto hay alguna diferencia en los autores que deste caso escriben), aunque por conjeturas verosímiles se deja entender que se llama Quijana; pero esto importa poco a nuestro cuento; basta que en la narración dél no se salga un punto de la verdad', - style: { - fontFamily: 'Times-Roman', - fontSize: 14, - textAlign: 'justify', - }, }, ], }, @@ -347,7 +674,6 @@ describe('node shouldBreak', () => { { type: 'TEXT_INSTANCE', value: 'Orphans example. Try changing prop value', - style: {}, }, ], }, @@ -390,11 +716,6 @@ describe('node shouldBreak', () => { type: 'TEXT_INSTANCE', value: 'En un lugar de la Mancha, de cuyo nombre no quiero acordarme, no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero, adarga antigua, rocín flaco y galgo corredor. Una olla de algo más vaca que carnero, salpicón las más noches, duelos y quebrantos los sábados, lentejas los viernes, algún palomino de añadidura los domingos, consumían las tres partes de su hacienda. El resto della concluían sayo de velarte, calzas de velludo para las fiestas con sus pantuflos de lo mismo, los días de entre semana se honraba con su vellori de lo más fino. Tenía en su casa una ama que pasaba de los cuarenta, y una sobrina que no llegaba a los veinte, y un mozo de campo y plaza, que así ensillaba el rocín como tomaba la podadera. Frisaba la edad de nuestro hidalgo con los cincuenta años, era de complexión recia, seco de carnes, enjuto de rostro; gran madrugador y amigo de la caza. Quieren decir que tenía el sobrenombre de Quijada o Quesada (que en esto hay alguna diferencia en los autores que deste caso escriben), aunque por conjeturas verosímiles se deja entender que se llama Quijana; pero esto importa poco a nuestro cuento; basta que en la narración dél no se salga un punto de la verdad', - style: { - fontFamily: 'Times-Roman', - fontSize: 14, - textAlign: 'justify', - }, }, ], }, @@ -409,9 +730,16 @@ describe('node shouldBreak', () => { const result = shouldBreak( { type: 'TEXT', + props: {}, + style: {}, + children: [], box: { top: 425.23779296875, + right: 0, + bottom: 0, + left: 0, height: 419.439453125, + width: 200, marginTop: 12, marginBottom: 12, }, @@ -419,9 +747,16 @@ describe('node shouldBreak', () => { [ { type: 'TEXT', + props: {}, + style: {}, + children: [], box: { top: 868.67724609375, + right: 0, + bottom: 0, + left: 0, height: 247.8505859375, + width: 200, marginTop: 12, marginBottom: 12, }, diff --git a/packages/layout/tests/steps/__snapshots__/resolveOrigins.test.js.snap b/packages/layout/tests/steps/__snapshots__/resolveOrigins.test.js.snap index 34d73fd51..266935a98 100644 --- a/packages/layout/tests/steps/__snapshots__/resolveOrigins.test.js.snap +++ b/packages/layout/tests/steps/__snapshots__/resolveOrigins.test.js.snap @@ -6,12 +6,12 @@ exports[`layout resolveOrigins > should not resolve for node without box 1`] = ` { "children": [ { - "origin": {}, + "origin": null, "style": {}, "type": "VIEW", }, ], - "origin": {}, + "origin": null, "style": {}, "type": "PAGE", }, diff --git a/packages/layout/tests/steps/__snapshots__/resolveOrigins.test.ts.snap b/packages/layout/tests/steps/__snapshots__/resolveOrigins.test.ts.snap new file mode 100644 index 000000000..d98e78e0d --- /dev/null +++ b/packages/layout/tests/steps/__snapshots__/resolveOrigins.test.ts.snap @@ -0,0 +1,153 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`layout resolveOrigins > should not resolve for node without box 1`] = ` +{ + "children": [ + { + "children": [ + { + "origin": null, + "props": {}, + "style": {}, + "type": "VIEW", + }, + ], + "origin": null, + "props": {}, + "style": {}, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolveOrigins > should resolve centered origin by default 1`] = ` +{ + "children": [ + { + "box": { + "bottom": 0, + "height": 400, + "left": 100, + "right": 0, + "top": 50, + "width": 300, + }, + "origin": { + "left": 250, + "top": 250, + }, + "props": {}, + "style": {}, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolveOrigins > should resolve origin adjusted by fixed values 1`] = ` +{ + "children": [ + { + "box": { + "bottom": 0, + "height": 400, + "left": 100, + "right": 0, + "top": 50, + "width": 300, + }, + "origin": { + "left": 200, + "top": 100, + }, + "props": {}, + "style": { + "transformOriginX": 100, + "transformOriginY": 50, + }, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolveOrigins > should resolve origin adjusted by percent values 1`] = ` +{ + "children": [ + { + "box": { + "bottom": 0, + "height": 400, + "left": 100, + "right": 0, + "top": 50, + "width": 300, + }, + "origin": { + "left": 160, + "top": 330, + }, + "props": {}, + "style": { + "transformOriginX": "20%", + "transformOriginY": "70%", + }, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolveOrigins > should resolve origins for nested elements 1`] = ` +{ + "children": [ + { + "box": { + "bottom": 0, + "height": 400, + "left": 100, + "right": 0, + "top": 50, + "width": 300, + }, + "children": [ + { + "box": { + "bottom": 0, + "height": 80, + "left": 10, + "right": 0, + "top": 0, + "width": 50, + }, + "origin": { + "left": 35, + "top": 40, + }, + "props": {}, + "style": {}, + "type": "VIEW", + }, + ], + "origin": { + "left": 250, + "top": 250, + }, + "props": {}, + "style": {}, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; diff --git a/packages/layout/tests/steps/__snapshots__/resolvePagePaddings.test.ts.snap b/packages/layout/tests/steps/__snapshots__/resolvePagePaddings.test.ts.snap new file mode 100644 index 000000000..5ca116ee1 --- /dev/null +++ b/packages/layout/tests/steps/__snapshots__/resolvePagePaddings.test.ts.snap @@ -0,0 +1,190 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`layout resolvePagePaddings > Should keep other styles untouched 1`] = ` +{ + "children": [ + { + "props": {}, + "style": { + "color": "red", + }, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolvePagePaddings > Should leave numeric paddingBottom as it is 1`] = ` +{ + "children": [ + { + "props": {}, + "style": { + "paddingBottom": 50, + }, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolvePagePaddings > Should leave numeric paddingLeft as it is 1`] = ` +{ + "children": [ + { + "props": {}, + "style": { + "paddingLeft": 50, + }, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolvePagePaddings > Should leave numeric paddingRight as it is 1`] = ` +{ + "children": [ + { + "props": {}, + "style": { + "paddingRight": 50, + }, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolvePagePaddings > Should leave numeric paddingTop as it is 1`] = ` +{ + "children": [ + { + "props": {}, + "style": { + "paddingTop": 50, + }, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolvePagePaddings > Should resolve percent paddingBottom 1`] = ` +{ + "children": [ + { + "props": {}, + "style": { + "height": 200, + "paddingBottom": 20, + "width": 100, + }, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolvePagePaddings > Should resolve percent paddingLeft 1`] = ` +{ + "children": [ + { + "props": {}, + "style": { + "height": 200, + "paddingLeft": 10, + "width": 100, + }, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolvePagePaddings > Should resolve percent paddingRight 1`] = ` +{ + "children": [ + { + "props": {}, + "style": { + "height": 200, + "paddingRight": 10, + "width": 100, + }, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolvePagePaddings > Should resolve percent paddingTop 1`] = ` +{ + "children": [ + { + "props": {}, + "style": { + "height": 200, + "paddingTop": 20, + "width": 100, + }, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolvePagePaddings > Should resolve several pages 1`] = ` +{ + "children": [ + { + "props": {}, + "style": { + "height": 200, + "paddingTop": 10, + "width": 100, + }, + "type": "PAGE", + }, + { + "props": {}, + "style": { + "height": 200, + "paddingBottom": 20, + "width": 100, + }, + "type": "PAGE", + }, + { + "props": {}, + "style": { + "height": 200, + "paddingLeft": 10, + "paddingRight": 10, + "width": 100, + }, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; diff --git a/packages/layout/tests/steps/__snapshots__/resolvePercentHeight.test.ts.snap b/packages/layout/tests/steps/__snapshots__/resolvePercentHeight.test.ts.snap new file mode 100644 index 000000000..4d29db01a --- /dev/null +++ b/packages/layout/tests/steps/__snapshots__/resolvePercentHeight.test.ts.snap @@ -0,0 +1,73 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`layout resolvePercentHeight > Should keep empty document untouched 1`] = ` +{ + "children": [], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolvePercentHeight > Should keep empty page untouched 1`] = ` +{ + "children": [ + { + "children": [], + "props": {}, + "style": {}, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolvePercentHeight > Should not resolve children if page dont have height 1`] = ` +{ + "children": [ + { + "children": [ + { + "props": {}, + "style": { + "height": "80%", + "width": "60%", + }, + "type": "VIEW", + }, + ], + "props": {}, + "style": {}, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; + +exports[`layout resolvePercentHeight > Should resolve children percent dimensions if page have height 1`] = ` +{ + "children": [ + { + "children": [ + { + "props": {}, + "style": { + "height": 800, + }, + "type": "VIEW", + }, + ], + "props": {}, + "style": { + "height": 1000, + }, + "type": "PAGE", + }, + ], + "props": {}, + "type": "DOCUMENT", +} +`; diff --git a/packages/layout/tests/steps/__snapshots__/resolveStyles.test.ts.snap b/packages/layout/tests/steps/__snapshots__/resolveStyles.test.ts.snap index 72716579e..57314c859 100644 --- a/packages/layout/tests/steps/__snapshots__/resolveStyles.test.ts.snap +++ b/packages/layout/tests/steps/__snapshots__/resolveStyles.test.ts.snap @@ -5,7 +5,11 @@ exports[`layout resolveStyles > Should overide default link styles 1`] = ` "children": [ { "box": { + "bottom": 0, "height": 200, + "left": 0, + "right": 0, + "top": 0, "width": 100, }, "children": [ @@ -33,7 +37,11 @@ exports[`layout resolveStyles > Should overide default link styles with array 1` "children": [ { "box": { + "bottom": 0, "height": 200, + "left": 0, + "right": 0, + "top": 0, "width": 100, }, "children": [ @@ -61,7 +69,11 @@ exports[`layout resolveStyles > Should resolve default link styles 1`] = ` "children": [ { "box": { + "bottom": 0, "height": 200, + "left": 0, + "right": 0, + "top": 0, "width": 100, }, "children": [ @@ -89,7 +101,11 @@ exports[`layout resolveStyles > Should resolve nested node styles 1`] = ` "children": [ { "box": { + "bottom": 0, "height": 200, + "left": 0, + "right": 0, + "top": 0, "width": 100, }, "children": [ @@ -132,7 +148,11 @@ exports[`layout resolveStyles > Should resolve nested node styles array 1`] = ` "children": [ { "box": { + "bottom": 0, "height": 200, + "left": 0, + "right": 0, + "top": 0, "width": 100, }, "children": [ @@ -175,7 +195,11 @@ exports[`layout resolveStyles > Should resolve nested node styles media queries "children": [ { "box": { + "bottom": 0, "height": 200, + "left": 0, + "right": 0, + "top": 0, "width": 100, }, "children": [ diff --git a/packages/layout/tests/steps/resolveInhritance.test.js b/packages/layout/tests/steps/resolveInhritance.test.ts similarity index 74% rename from packages/layout/tests/steps/resolveInhritance.test.js rename to packages/layout/tests/steps/resolveInhritance.test.ts index 4e14e013a..2f5ffbce8 100644 --- a/packages/layout/tests/steps/resolveInhritance.test.js +++ b/packages/layout/tests/steps/resolveInhritance.test.ts @@ -6,75 +6,81 @@ import resolveInheritance from '../../src/steps/resolveInheritance'; describe('layout resolveInheritance', () => { const shouldInherit = (prop) => () => { - const root = { + const result = resolveInheritance({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, style: { [prop]: 'value' }, - children: [{ type: 'VIEW', style: {} }], + children: [{ type: 'VIEW', props: {}, style: {} }], }, ], - }; - const result = resolveInheritance(root); + }); const view = result.children[0].children[0]; expect(view.style).toHaveProperty(prop, 'value'); }; test('Should not inherit invalid property', () => { - const root = { + const result = resolveInheritance({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, style: { backgroundColor: 'value' }, - children: [{ type: 'VIEW', style: {} }], + children: [{ type: 'VIEW', props: {}, style: {} }], }, ], - }; - const result = resolveInheritance(root); + }); const view = result.children[0].children[0]; expect(view.style).not.toHaveProperty('backgroundColor'); }); test('Should not override descendents styles', () => { - const root = { + const result = resolveInheritance({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, style: { color: 'red' }, - children: [{ type: 'VIEW', style: { color: 'green' } }], + children: [{ type: 'VIEW', props: {}, style: { color: 'green' } }], }, ], - }; - const result = resolveInheritance(root); + }); const view = result.children[0].children[0]; expect(view.style).toHaveProperty('color', 'green'); }); test('Should only inherit node descendents', () => { - const root = { + const result = resolveInheritance({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', style: {}, + props: {}, children: [ - { type: 'VIEW', style: {} }, + { type: 'VIEW', props: {}, style: {} }, { type: 'VIEW', + props: {}, style: { color: 'green' }, - children: [{ type: 'VIEW', style: {} }], + children: [{ type: 'VIEW', props: {}, style: {} }], }, ], }, ], - }; - const result = resolveInheritance(root); + }); + const view1 = result.children[0].children[0]; const view2 = result.children[0].children[1]; const subview = view2.children[0]; @@ -85,26 +91,32 @@ describe('layout resolveInheritance', () => { }); test('Should inherit multiple textDecoration properly', () => { - const root = { + const result = resolveInheritance({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, style: {}, children: [ { type: 'TEXT', + props: {}, style: { textDecoration: 'line-through' }, children: [ - { type: 'TEXT', style: { textDecoration: 'underline' } }, + { + type: 'TEXT', + props: {}, + style: { textDecoration: 'underline' }, + }, ], }, ], }, ], - }; + }); - const result = resolveInheritance(root); const text1 = result.children[0].children[0]; const text2 = text1.children[0]; @@ -116,24 +128,33 @@ describe('layout resolveInheritance', () => { }); test('Should inherit background color for text childs', () => { - const root = { + const result = resolveInheritance({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, style: {}, children: [ { type: 'TEXT', + props: {}, style: { backgroundColor: 'red' }, - children: [{ type: 'TEXT' }], + children: [ + { + type: 'TEXT', + props: {}, + style: {}, + children: [{ type: 'TEXT_INSTANCE', value: 'Hello' }], + }, + ], }, ], }, ], - }; + }); - const result = resolveInheritance(root); const text1 = result.children[0].children[0]; const text2 = text1.children[0]; diff --git a/packages/layout/tests/steps/resolveOrigins.test.js b/packages/layout/tests/steps/resolveOrigins.test.ts similarity index 52% rename from packages/layout/tests/steps/resolveOrigins.test.js rename to packages/layout/tests/steps/resolveOrigins.test.ts index 6f771df0f..1f8e05b0f 100644 --- a/packages/layout/tests/steps/resolveOrigins.test.js +++ b/packages/layout/tests/steps/resolveOrigins.test.ts @@ -4,88 +4,129 @@ import resolveOrigins from '../../src/steps/resolveOrigins'; describe('layout resolveOrigins', () => { test('should not resolve for node without box', () => { - const root = { + const result = resolveOrigins({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', style: {}, - children: [{ type: 'VIEW', style: {} }], + props: {}, + children: [{ type: 'VIEW', props: {}, style: {} }], }, ], - }; - const result = resolveOrigins(root); + }); expect(result).toMatchSnapshot(); }); test('should resolve centered origin by default', () => { - const root = { + const result = resolveOrigins({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', style: {}, - box: { top: 50, left: 100, width: 300, height: 400 }, + props: {}, + box: { + top: 50, + left: 100, + bottom: 0, + right: 0, + width: 300, + height: 400, + }, }, ], - }; - const result = resolveOrigins(root); + }); expect(result).toMatchSnapshot(); }); test('should resolve origin adjusted by fixed values', () => { - const root = { + const result = resolveOrigins({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', - box: { top: 50, left: 100, width: 300, height: 400 }, + props: {}, + box: { + top: 50, + left: 100, + bottom: 0, + right: 0, + width: 300, + height: 400, + }, style: { transformOriginX: 100, transformOriginY: 50 }, }, ], - }; - const result = resolveOrigins(root); + }); expect(result).toMatchSnapshot(); }); test('should resolve origin adjusted by percent values', () => { - const root = { + const result = resolveOrigins({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', - box: { top: 50, left: 100, width: 300, height: 400 }, + props: {}, + box: { + top: 50, + left: 100, + bottom: 0, + right: 0, + width: 300, + height: 400, + }, style: { transformOriginX: '20%', transformOriginY: '70%' }, }, ], - }; - const result = resolveOrigins(root); + }); expect(result).toMatchSnapshot(); }); test('should resolve origins for nested elements', () => { - const root = { + const result = resolveOrigins({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', style: {}, - box: { top: 50, left: 100, width: 300, height: 400 }, + props: {}, + box: { + top: 50, + left: 100, + bottom: 0, + right: 0, + width: 300, + height: 400, + }, children: [ { type: 'VIEW', style: {}, - box: { top: 0, left: 10, width: 50, height: 80 }, + props: {}, + box: { + top: 0, + left: 10, + bottom: 0, + right: 0, + width: 50, + height: 80, + }, }, ], }, ], - }; - const result = resolveOrigins(root); + }); expect(result).toMatchSnapshot(); }); diff --git a/packages/layout/tests/steps/resolvePagePaddings.test.js b/packages/layout/tests/steps/resolvePagePaddings.test.ts similarity index 56% rename from packages/layout/tests/steps/resolvePagePaddings.test.js rename to packages/layout/tests/steps/resolvePagePaddings.test.ts index d2ec04fd0..8a9c2816b 100644 --- a/packages/layout/tests/steps/resolvePagePaddings.test.js +++ b/packages/layout/tests/steps/resolvePagePaddings.test.ts @@ -4,139 +4,146 @@ import resolvePagePaddings from '../../src/steps/resolvePagePaddings'; describe('layout resolvePagePaddings', () => { test('Should keep other styles untouched', () => { - const root = { + const result = resolvePagePaddings({ type: 'DOCUMENT', - children: [{ type: 'PAGE', style: { color: 'red' } }], - }; - const result = resolvePagePaddings(root); + props: {}, + children: [{ type: 'PAGE', props: {}, style: { color: 'red' } }], + }); expect(result).toMatchSnapshot(); }); test('Should leave numeric paddingTop as it is', () => { - const root = { + const result = resolvePagePaddings({ type: 'DOCUMENT', - children: [{ type: 'PAGE', style: { paddingTop: 50 } }], - }; - const result = resolvePagePaddings(root); + props: {}, + children: [{ type: 'PAGE', props: {}, style: { paddingTop: 50 } }], + }); expect(result).toMatchSnapshot(); }); test('Should leave numeric paddingRight as it is', () => { - const root = { + const result = resolvePagePaddings({ type: 'DOCUMENT', - children: [{ type: 'PAGE', style: { paddingRight: 50 } }], - }; - const result = resolvePagePaddings(root); + props: {}, + children: [{ type: 'PAGE', props: {}, style: { paddingRight: 50 } }], + }); expect(result).toMatchSnapshot(); }); test('Should leave numeric paddingBottom as it is', () => { - const root = { + const result = resolvePagePaddings({ type: 'DOCUMENT', - children: [{ type: 'PAGE', style: { paddingBottom: 50 } }], - }; - const result = resolvePagePaddings(root); + props: {}, + children: [{ type: 'PAGE', props: {}, style: { paddingBottom: 50 } }], + }); expect(result).toMatchSnapshot(); }); test('Should leave numeric paddingLeft as it is', () => { - const root = { + const result = resolvePagePaddings({ type: 'DOCUMENT', - children: [{ type: 'PAGE', style: { paddingLeft: 50 } }], - }; - const result = resolvePagePaddings(root); + props: {}, + children: [{ type: 'PAGE', props: {}, style: { paddingLeft: 50 } }], + }); expect(result).toMatchSnapshot(); }); test('Should resolve percent paddingTop', () => { - const root = { + const result = resolvePagePaddings({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', - style: { paddingTop: '10%', width: 100, height: 200 }, + props: {}, + style: { paddingTop: '10%' as any, width: 100, height: 200 }, }, ], - }; - const result = resolvePagePaddings(root); + }); expect(result).toMatchSnapshot(); }); test('Should resolve percent paddingRight', () => { - const root = { + const result = resolvePagePaddings({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', - style: { paddingRight: '10%', width: 100, height: 200 }, + props: {}, + style: { paddingRight: '10%' as any, width: 100, height: 200 }, }, ], - }; - const result = resolvePagePaddings(root); + }); expect(result).toMatchSnapshot(); }); test('Should resolve percent paddingBottom', () => { - const root = { + const result = resolvePagePaddings({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', - style: { paddingBottom: '10%', width: 100, height: 200 }, + props: {}, + style: { paddingBottom: '10%' as any, width: 100, height: 200 }, }, ], - }; - const result = resolvePagePaddings(root); + }); expect(result).toMatchSnapshot(); }); test('Should resolve percent paddingLeft', () => { - const root = { + const result = resolvePagePaddings({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', - style: { paddingLeft: '10%', width: 100, height: 200 }, + props: {}, + style: { paddingLeft: '10%' as any, width: 100, height: 200 }, }, ], - }; - const result = resolvePagePaddings(root); + }); expect(result).toMatchSnapshot(); }); test('Should resolve several pages', () => { - const root = { + const result = resolvePagePaddings({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, style: { paddingTop: 10, width: 100, height: 200 }, }, { type: 'PAGE', - style: { paddingBottom: '10%', width: 100, height: 200 }, + props: {}, + style: { paddingBottom: '10%' as any, width: 100, height: 200 }, }, { type: 'PAGE', + props: {}, style: { paddingRight: 10, - paddingLeft: '10%', + paddingLeft: '10%' as any, width: 100, height: 200, }, }, ], - }; - const result = resolvePagePaddings(root); + }); expect(result).toMatchSnapshot(); }); diff --git a/packages/layout/tests/steps/resolvePageSizes.test.js b/packages/layout/tests/steps/resolvePageSizes.test.ts similarity index 83% rename from packages/layout/tests/steps/resolvePageSizes.test.js rename to packages/layout/tests/steps/resolvePageSizes.test.ts index 7811c1eb7..c491f772d 100644 --- a/packages/layout/tests/steps/resolvePageSizes.test.js +++ b/packages/layout/tests/steps/resolvePageSizes.test.ts @@ -4,91 +4,95 @@ import resolvePageSizes from '../../src/steps/resolvePageSizes'; describe('layout resolvePageSizes', () => { test('Should default to A4', () => { - const root = { type: 'DOCUMENT', children: [{ type: 'PAGE', props: {} }] }; - const result = resolvePageSizes(root); + const result = resolvePageSizes({ + type: 'DOCUMENT', + props: {}, + children: [{ type: 'PAGE', props: {} }], + }); expect(result.children[0].style).toHaveProperty('width', 595.28); expect(result.children[0].style).toHaveProperty('height', 841.89); }); test('Should default to portrait A4', () => { - const root = { + const result = resolvePageSizes({ type: 'DOCUMENT', + props: {}, children: [{ type: 'PAGE', props: { orientation: 'portrait' } }], - }; - const result = resolvePageSizes(root); + }); expect(result.children[0].style).toHaveProperty('width', 595.28); expect(result.children[0].style).toHaveProperty('height', 841.89); }); test('Should accept size string', () => { - const root = { + const result = resolvePageSizes({ type: 'DOCUMENT', + props: {}, children: [{ type: 'PAGE', props: { size: 'A2' } }], - }; - const result = resolvePageSizes(root); + }); expect(result.children[0].style).toHaveProperty('width', 1190.55); expect(result.children[0].style).toHaveProperty('height', 1683.78); }); test('Should accept size string in landscape mode', () => { - const root = { + const result = resolvePageSizes({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', props: { size: 'A2', orientation: 'landscape' } }, ], - }; - const result = resolvePageSizes(root); + }); expect(result.children[0].style).toHaveProperty('width', 1683.78); expect(result.children[0].style).toHaveProperty('height', 1190.55); }); test('Should accept size array', () => { - const root = { + const result = resolvePageSizes({ type: 'DOCUMENT', + props: {}, children: [{ type: 'PAGE', props: { size: [100, 200] } }], - }; - const result = resolvePageSizes(root); + }); expect(result.children[0].style).toHaveProperty('width', 100); expect(result.children[0].style).toHaveProperty('height', 200); }); test('Should accept size array in landscape mode', () => { - const root = { + const result = resolvePageSizes({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', props: { size: [100, 200], orientation: 'landscape' }, }, ], - }; - const result = resolvePageSizes(root); + }); expect(result.children[0].style).toHaveProperty('width', 200); expect(result.children[0].style).toHaveProperty('height', 100); }); test('Should accept size object', () => { - const root = { + const result = resolvePageSizes({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', props: { size: { width: 100, height: 200 } } }, ], - }; - const result = resolvePageSizes(root); + }); expect(result.children[0].style).toHaveProperty('width', 100); expect(result.children[0].style).toHaveProperty('height', 200); }); test('Should accept size object in landscape mode', () => { - const root = { + const result = resolvePageSizes({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', @@ -98,47 +102,46 @@ describe('layout resolvePageSizes', () => { }, }, ], - }; - const result = resolvePageSizes(root); + }); expect(result.children[0].style).toHaveProperty('width', 200); expect(result.children[0].style).toHaveProperty('height', 100); }); test('Should accept size number', () => { - const root = { + const result = resolvePageSizes({ type: 'DOCUMENT', + props: {}, children: [{ type: 'PAGE', props: { size: 100 } }], - }; - const result = resolvePageSizes(root); + }); expect(result.children[0].style).toHaveProperty('width', 100); expect(result.children[0].style).toHaveProperty('height', 100); }); test('Should accept size number in landscape mode', () => { - const root = { + const result = resolvePageSizes({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', props: { size: 100, orientation: 'landscape' } }, ], - }; - const result = resolvePageSizes(root); + }); expect(result.children[0].style).toHaveProperty('width', 100); expect(result.children[0].style).toHaveProperty('height', 100); }); test('Should resolve several pages', () => { - const root = { + const result = resolvePageSizes({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', props: {} }, { type: 'PAGE', props: { size: 'A5' } }, { type: 'PAGE', props: { size: { width: 100, height: 200 } } }, ], - }; - const result = resolvePageSizes(root); + }); expect(result.children[0].style).toHaveProperty('width', 595.28); expect(result.children[0].style).toHaveProperty('height', 841.89); @@ -149,8 +152,9 @@ describe('layout resolvePageSizes', () => { }); test('Should flatten page styles', () => { - const root = { + const result = resolvePageSizes({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', @@ -158,8 +162,7 @@ describe('layout resolvePageSizes', () => { style: [{ backgroundColor: 'red' }], }, ], - }; - const result = resolvePageSizes(root); + }); expect(result.children[0].style).toHaveProperty('width', 595.28); expect(result.children[0].style).toHaveProperty('height', 841.89); diff --git a/packages/layout/tests/steps/resolvePagination.test.js b/packages/layout/tests/steps/resolvePagination.test.ts similarity index 76% rename from packages/layout/tests/steps/resolvePagination.test.js rename to packages/layout/tests/steps/resolvePagination.test.ts index a71d077e1..9cf55bdf9 100644 --- a/packages/layout/tests/steps/resolvePagination.test.js +++ b/packages/layout/tests/steps/resolvePagination.test.ts @@ -1,24 +1,29 @@ import { describe, expect, test } from 'vitest'; +import FontStore from '@react-pdf/font'; import { loadYoga } from '../../src/yoga'; - import resolvePagination from '../../src/steps/resolvePagination'; import resolveDimensions from '../../src/steps/resolveDimensions'; +import { SafeDocumentNode } from '../../src/types'; + +const fontStore = new FontStore(); // dimensions is required by pagination step and them are calculated here -const calcLayout = (node) => resolvePagination(resolveDimensions(node)); +const calcLayout = (node: SafeDocumentNode) => + resolvePagination(resolveDimensions(node, fontStore), fontStore); describe('pagination step', () => { test('should stretch absolute block to full page size', async () => { const yoga = await loadYoga(); - const root = { + const layout = calcLayout({ type: 'DOCUMENT', yoga, + props: {}, children: [ { type: 'PAGE', - box: {}, + props: {}, style: { width: 100, height: 100, @@ -26,7 +31,6 @@ describe('pagination step', () => { children: [ { type: 'VIEW', - box: {}, style: { position: 'absolute', width: '50%', @@ -38,7 +42,6 @@ describe('pagination step', () => { }, { type: 'TEXT', - box: {}, style: {}, props: {}, children: [ @@ -51,27 +54,26 @@ describe('pagination step', () => { ], }, ], - }; - - const layout = calcLayout(root); + }); const page = layout.children[0]; - const view = layout.children[0].children[0]; + const view = layout.children[0]!.children![0]; - expect(page.box.height).toBe(100); - expect(view.box.height).toBe(100); + expect(page.box!.height).toBe(100); + expect(view.box!.height).toBe(100); }); test('should force new height for split nodes', async () => { const yoga = await loadYoga(); - const root = { + const layout = calcLayout({ type: 'DOCUMENT', yoga, + props: {}, children: [ { type: 'PAGE', - box: {}, + props: {}, style: { width: 15, height: 60, @@ -80,13 +82,11 @@ describe('pagination step', () => { children: [ { type: 'VIEW', - box: {}, style: {}, props: {}, children: [ { type: 'TEXT', - box: {}, style: {}, props: {}, children: [ @@ -101,27 +101,26 @@ describe('pagination step', () => { ], }, ], - }; + }); - const layout = calcLayout(root); + const view1 = layout.children[0].children![0]; + const view2 = layout.children[1].children![0]; - const view1 = layout.children[0].children[0]; - const view2 = layout.children[1].children[0]; - - expect(view1.box.height).toBe(60); - expect(view2.box.height).not.toBe(60); + expect(view1.box!.height).toBe(60); + expect(view2.box!.height).not.toBe(60); }); test('should force new height for split nodes with fixed height', async () => { const yoga = await loadYoga(); - const root = { + const layout = calcLayout({ type: 'DOCUMENT', yoga, + props: {}, children: [ { type: 'PAGE', - box: {}, + props: {}, style: { width: 5, height: 60, @@ -130,7 +129,6 @@ describe('pagination step', () => { children: [ { type: 'VIEW', - box: {}, style: { height: 130 }, props: {}, children: [], @@ -138,29 +136,27 @@ describe('pagination step', () => { ], }, ], - }; - - const layout = calcLayout(root); + }); - const view1 = layout.children[0].children[0]; - const view2 = layout.children[1].children[0]; - const view3 = layout.children[2].children[0]; + const view1 = layout.children[0].children![0]; + const view2 = layout.children[1].children![0]; + const view3 = layout.children[2].children![0]; - expect(view1.box.height).toBe(60); - expect(view2.box.height).toBe(60); - expect(view3.box.height).toBe(10); + expect(view1.box!.height).toBe(60); + expect(view2.box!.height).toBe(60); + expect(view3.box!.height).toBe(10); }); test('should not wrap page with false wrap prop', async () => { const yoga = await loadYoga(); - const root = { + const layout = calcLayout({ type: 'DOCUMENT', yoga, + props: {}, children: [ { type: 'PAGE', - box: {}, style: { width: 5, height: 60, @@ -171,7 +167,6 @@ describe('pagination step', () => { children: [ { type: 'VIEW', - box: {}, style: { height: 130 }, props: {}, children: [], @@ -179,9 +174,7 @@ describe('pagination step', () => { ], }, ], - }; - - const layout = calcLayout(root); + }); expect(layout.children.length).toBe(1); }); @@ -189,13 +182,14 @@ describe('pagination step', () => { test('should break on a container whose children can not fit on a page', async () => { const yoga = await loadYoga(); - const root = { + const layout = calcLayout({ type: 'DOCUMENT', yoga, + props: {}, children: [ { type: 'PAGE', - box: {}, + props: {}, style: { width: 5, height: 60, @@ -204,7 +198,6 @@ describe('pagination step', () => { children: [ { type: 'VIEW', - box: {}, style: { width: 5, height: 40, @@ -214,7 +207,6 @@ describe('pagination step', () => { }, { type: 'VIEW', - box: {}, style: { width: 5, }, @@ -222,7 +214,6 @@ describe('pagination step', () => { children: [ { type: 'VIEW', - box: {}, style: { height: 40, }, @@ -236,42 +227,40 @@ describe('pagination step', () => { ], }, ], - }; - - const layout = calcLayout(root); + }); const page1 = layout.children[0]; const page2 = layout.children[1]; // Only the first view is displayed on the first page - expect(page1.children.length).toBe(1); + expect(page1.children!.length).toBe(1); // The second page displays the second wrapper, with its full height - expect(page2.children.length).toBe(1); - expect(page2.children[0].box.height).toBe(40); + expect(page2.children!.length).toBe(1); + expect(page2.children![0].box!.height).toBe(40); }); test('should not infinitely loop when splitting pages', async () => { const yoga = await loadYoga(); - const root = { + calcLayout({ type: 'DOCUMENT', yoga, + props: {}, children: [ { type: 'PAGE', - box: {}, + props: {}, style: { height: 400, }, children: [ { type: 'VIEW', - box: {}, style: { height: 401 }, + props: {}, children: [ { type: 'VIEW', - box: {}, style: { height: 400, }, @@ -282,9 +271,7 @@ describe('pagination step', () => { ], }, ], - }; - - calcLayout(root); + }); // If calcLayout returns then we did not hit an infinite loop expect(true).toBe(true); diff --git a/packages/layout/tests/steps/resolvePercentHeight.test.js b/packages/layout/tests/steps/resolvePercentHeight.test.ts similarity index 60% rename from packages/layout/tests/steps/resolvePercentHeight.test.js rename to packages/layout/tests/steps/resolvePercentHeight.test.ts index 8aae0569f..e19a14948 100644 --- a/packages/layout/tests/steps/resolvePercentHeight.test.js +++ b/packages/layout/tests/steps/resolvePercentHeight.test.ts @@ -4,52 +4,57 @@ import resolvePercentHeight from '../../src/steps/resolvePercentHeight'; describe('layout resolvePercentHeight', () => { test('Should keep empty document untouched', () => { - const root = { + const result = resolvePercentHeight({ type: 'DOCUMENT', + props: {}, children: [], - }; - const result = resolvePercentHeight(root); + }); expect(result).toMatchSnapshot(); }); test('Should keep empty page untouched', () => { - const root = { + const result = resolvePercentHeight({ type: 'DOCUMENT', - children: [{ type: 'PAGE' }], - }; - const result = resolvePercentHeight(root); + props: {}, + children: [{ type: 'PAGE', props: {}, style: {}, children: [] }], + }); expect(result).toMatchSnapshot(); }); test('Should not resolve children if page dont have height', () => { - const root = { + const result = resolvePercentHeight({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', - children: [{ type: 'VIEW', style: { width: '60%', height: '80%' } }], + props: {}, + style: {}, + children: [ + { type: 'VIEW', props: {}, style: { width: '60%', height: '80%' } }, + ], }, ], - }; - const result = resolvePercentHeight(root); + }); expect(result).toMatchSnapshot(); }); test('Should resolve children percent dimensions if page have height', () => { - const root = { + const result = resolvePercentHeight({ type: 'DOCUMENT', + props: {}, children: [ { type: 'PAGE', + props: {}, style: { height: 1000 }, - children: [{ type: 'VIEW', style: { height: '80%' } }], + children: [{ type: 'VIEW', props: {}, style: { height: '80%' } }], }, ], - }; - const result = resolvePercentHeight(root); + }); expect(result).toMatchSnapshot(); }); diff --git a/packages/layout/tests/steps/resolveStyles.test.ts b/packages/layout/tests/steps/resolveStyles.test.ts index e0058720f..6b00d834a 100644 --- a/packages/layout/tests/steps/resolveStyles.test.ts +++ b/packages/layout/tests/steps/resolveStyles.test.ts @@ -86,7 +86,14 @@ describe('layout resolveStyles', () => { { type: 'PAGE', props: {}, - box: { width: 100, height: 200 }, + box: { + top: 0, + right: 0, + bottom: 0, + left: 0, + width: 100, + height: 200, + }, children: [ { type: 'VIEW', @@ -114,7 +121,14 @@ describe('layout resolveStyles', () => { { type: 'PAGE', props: {}, - box: { width: 100, height: 200 }, + box: { + top: 0, + right: 0, + bottom: 0, + left: 0, + width: 100, + height: 200, + }, children: [ { type: 'VIEW', @@ -190,7 +204,14 @@ describe('layout resolveStyles', () => { { type: 'PAGE', props: {}, - box: { width: 100, height: 200 }, + box: { + top: 0, + right: 0, + bottom: 0, + left: 0, + width: 100, + height: 200, + }, children: [ { type: 'VIEW', @@ -222,7 +243,14 @@ describe('layout resolveStyles', () => { { type: 'PAGE', props: {}, - box: { width: 100, height: 200 }, + box: { + top: 0, + right: 0, + bottom: 0, + left: 0, + width: 100, + height: 200, + }, children: [ { type: 'LINK', @@ -245,7 +273,14 @@ describe('layout resolveStyles', () => { { type: 'PAGE', props: {}, - box: { width: 100, height: 200 }, + box: { + top: 0, + right: 0, + bottom: 0, + left: 0, + width: 100, + height: 200, + }, children: [ { type: 'LINK', @@ -268,7 +303,14 @@ describe('layout resolveStyles', () => { { type: 'PAGE', props: {}, - box: { width: 100, height: 200 }, + box: { + top: 0, + right: 0, + bottom: 0, + left: 0, + width: 100, + height: 200, + }, children: [ { type: 'LINK', diff --git a/packages/layout/tests/steps/resolveTextLayout.test.js b/packages/layout/tests/steps/resolveTextLayout.test.ts similarity index 73% rename from packages/layout/tests/steps/resolveTextLayout.test.js rename to packages/layout/tests/steps/resolveTextLayout.test.ts index 2cd567c7f..7c2aa161b 100644 --- a/packages/layout/tests/steps/resolveTextLayout.test.js +++ b/packages/layout/tests/steps/resolveTextLayout.test.ts @@ -1,17 +1,25 @@ import { describe, expect, test } from 'vitest'; +import FontStore from '@react-pdf/font'; import { loadYoga } from '../../src/yoga'; import resolveTextLayout from '../../src/steps/resolveTextLayout'; import resolveDimensions from '../../src/steps/resolveDimensions'; +import { SafeDocumentNode } from '../../src/types'; -const getRoot = async (text = 'hello world', styles = {}) => ({ +const fontStore = new FontStore(); + +const getRoot = async ( + text = 'hello world', + styles = {}, +): Promise => ({ type: 'DOCUMENT', + props: {}, yoga: await loadYoga(), children: [ { type: 'PAGE', - box: {}, + props: {}, style: { width: 100, height: 100, @@ -19,7 +27,6 @@ const getRoot = async (text = 'hello world', styles = {}) => ({ children: [ { type: 'TEXT', - box: {}, style: styles, props: {}, children: [ @@ -39,25 +46,25 @@ describe('text layout step', () => { test('should calculate lines for text while resolve dimensions', async () => { const root = await getRoot('text text text'); - const dimensions = resolveDimensions(root); + const dimensions = resolveDimensions(root, fontStore); expect(getText(dimensions).lines).toBeDefined(); }); test('should calculate lines for text width defined height', async () => { const root = await getRoot('text text text', { height: 50 }); - const dimensions = resolveDimensions(root); + const dimensions = resolveDimensions(root, fontStore); expect(getText(dimensions).lines).not.toBeDefined(); - const textLayout = resolveTextLayout(dimensions); + const textLayout = resolveTextLayout(dimensions, fontStore); expect(getText(textLayout).lines).toBeDefined(); }); test('should calculate lines for empty text', async () => { const root = await getRoot(''); - const dimensions = resolveDimensions(root); + const dimensions = resolveDimensions(root, fontStore); expect(getText(dimensions).lines).toBeDefined(); }); diff --git a/packages/layout/tests/text/fontSubstitution.test.js b/packages/layout/tests/text/fontSubstitution.test.ts similarity index 77% rename from packages/layout/tests/text/fontSubstitution.test.js rename to packages/layout/tests/text/fontSubstitution.test.ts index d6cc421df..ecaaf3f79 100644 --- a/packages/layout/tests/text/fontSubstitution.test.js +++ b/packages/layout/tests/text/fontSubstitution.test.ts @@ -12,30 +12,46 @@ describe('FontSubstitution', () => { }); test('should merge consecutive runs with same font', () => { - const run1 = { start: 0, end: 3, attributes: { font: ['Helvetica'] } }; - const run2 = { start: 3, end: 5, attributes: { font: ['Helvetica'] } }; + const run1 = { + start: 0, + end: 3, + attributes: { font: ['Helvetica'] }, + } as any; + + const run2 = { + start: 3, + end: 5, + attributes: { font: ['Helvetica'] }, + } as any; + const string = instance({ string: 'Lorem', runs: [run1, run2] }); expect(string).toHaveProperty('string', 'Lorem'); expect(string.runs).toHaveLength(1); expect(string.runs[0]).toHaveProperty('start', 0); expect(string.runs[0]).toHaveProperty('end', 5); - expect(string.runs[0].attributes.font.name).toBe('Helvetica'); + expect(string.runs[0].attributes.font).toBeTruthy(); }); test('should substitute many runs', () => { - const run1 = { start: 0, end: 3, attributes: { font: ['Courier'] } }; - const run2 = { start: 3, end: 5, attributes: { font: ['Helvetica'] } }; + const run1 = { start: 0, end: 3, attributes: { font: ['Courier'] } } as any; + + const run2 = { + start: 3, + end: 5, + attributes: { font: ['Helvetica'] }, + } as any; + const string = instance({ string: 'Lorem', runs: [run1, run2] }); expect(string).toHaveProperty('string', 'Lorem'); expect(string.runs).toHaveLength(2); expect(string.runs[0]).toHaveProperty('start', 0); expect(string.runs[0]).toHaveProperty('end', 3); - expect(string.runs[0].attributes.font.name).toBe('Courier'); + expect(string.runs[0].attributes.font).toBeTruthy(); expect(string.runs[1]).toHaveProperty('start', 3); expect(string.runs[1]).toHaveProperty('end', 5); - expect(string.runs[1].attributes.font.name).toBe('Helvetica'); + expect(string.runs[1].attributes.font).toBeTruthy(); }); describe('Fallback Font', () => { @@ -51,7 +67,7 @@ describe('FontSubstitution', () => { attributes: { font: ['Courier', SimplifiedChineseFont], }, - }; + } as any; const string = instance({ string: '你', runs: [run] }); @@ -59,9 +75,7 @@ describe('FontSubstitution', () => { expect(string.runs).toHaveLength(1); expect(string.runs[0]).toHaveProperty('start', 0); expect(string.runs[0]).toHaveProperty('end', 1); - expect(string.runs[0].attributes.font.name).toBe( - SimplifiedChineseFont.name, - ); + expect(string.runs[0].attributes.font).toBe(SimplifiedChineseFont); }); test('should split a run when fallback font is used on a portion of the run', () => { @@ -71,7 +85,7 @@ describe('FontSubstitution', () => { attributes: { font: ['Courier', SimplifiedChineseFont], }, - }; + } as any; const string = instance({ string: 'A你', runs: [run] }); @@ -79,12 +93,10 @@ describe('FontSubstitution', () => { expect(string.runs).toHaveLength(2); expect(string.runs[0]).toHaveProperty('start', 0); expect(string.runs[0]).toHaveProperty('end', 1); - expect(string.runs[0].attributes.font.name).toBe('Courier'); + expect(string.runs[0].attributes.font).toBeTruthy(); expect(string.runs[1]).toHaveProperty('start', 1); expect(string.runs[1]).toHaveProperty('end', 2); - expect(string.runs[1].attributes.font.name).toBe( - SimplifiedChineseFont.name, - ); + expect(string.runs[1].attributes.font).toBe(SimplifiedChineseFont); }); }); }); diff --git a/packages/layout/tests/text/fromFragments.test.js b/packages/layout/tests/text/fromFragments.test.js deleted file mode 100644 index 20138480f..000000000 --- a/packages/layout/tests/text/fromFragments.test.js +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, test } from 'vitest'; - -import fromFragments from '../../src/text/fromFragments'; - -describe('attributeString fromFragments operator', () => { - test('should return empty attributed string for no fragments', () => { - const attributedString = fromFragments([]); - - expect(attributedString.string).toBe(''); - expect(attributedString.runs).toHaveLength(0); - }); - - test('should be constructed by one fragment', () => { - const attributedString = fromFragments([{ string: 'Hey' }]); - - expect(attributedString.string).toBe('Hey'); - expect(attributedString.runs[0]).toHaveProperty('start', 0); - expect(attributedString.runs[0]).toHaveProperty('end', 3); - }); - - test('should be constructed by fragments', () => { - const attributedString = fromFragments([ - { string: 'Hey' }, - { string: ' ho' }, - ]); - - expect(attributedString.string).toBe('Hey ho'); - expect(attributedString.runs[0]).toHaveProperty('start', 0); - expect(attributedString.runs[0]).toHaveProperty('end', 3); - expect(attributedString.runs[1]).toHaveProperty('start', 3); - expect(attributedString.runs[1]).toHaveProperty('end', 6); - }); - - test('should preserve fragment attributes', () => { - const attributedString = fromFragments([ - { string: 'Hey', attributes: { attr: 1 } }, - { string: ' ho', attributes: { attr: 2 } }, - ]); - - expect(attributedString.runs[0]).toHaveProperty('attributes', { attr: 1 }); - expect(attributedString.runs[1]).toHaveProperty('attributes', { attr: 2 }); - }); -}); diff --git a/packages/layout/tests/text/heightAtLineIndex.test.js b/packages/layout/tests/text/heightAtLineIndex.test.ts similarity index 62% rename from packages/layout/tests/text/heightAtLineIndex.test.js rename to packages/layout/tests/text/heightAtLineIndex.test.ts index 237824945..82b08c52e 100644 --- a/packages/layout/tests/text/heightAtLineIndex.test.js +++ b/packages/layout/tests/text/heightAtLineIndex.test.ts @@ -1,41 +1,66 @@ import { describe, expect, test } from 'vitest'; import heightAtLineIndex from '../../src/text/heightAtLineIndex'; +import { SafeTextNode } from '../../src/types'; const TEST_LINE = { box: { height: 25 } }; const TEST_LINES = Array(10).fill(TEST_LINE); describe('text heightAtLineIndex', () => { test('Should return 0 if no lines present', () => { - const node = { type: 'TEXT' }; + const node: SafeTextNode = { type: 'TEXT', props: {}, style: {} }; const result = heightAtLineIndex(node, 5); expect(result).toBe(0); }); test('Should return correct height for first line', () => { - const node = { type: 'TEXT', lines: TEST_LINES }; + const node: SafeTextNode = { + type: 'TEXT', + props: {}, + style: {}, + lines: TEST_LINES, + }; + const result = heightAtLineIndex(node, 1); expect(result).toBe(25); }); test('Should return correct height for intermediate line', () => { - const node = { type: 'TEXT', lines: TEST_LINES }; + const node: SafeTextNode = { + type: 'TEXT', + props: {}, + style: {}, + lines: TEST_LINES, + }; + const result = heightAtLineIndex(node, 5); expect(result).toBe(125); }); test('Should return correct height for last line', () => { - const node = { type: 'TEXT', lines: TEST_LINES }; + const node: SafeTextNode = { + type: 'TEXT', + props: {}, + style: {}, + lines: TEST_LINES, + }; + const result = heightAtLineIndex(node, 10); expect(result).toBe(250); }); test('Should return correct height for overflow line', () => { - const node = { type: 'TEXT', lines: TEST_LINES }; + const node: SafeTextNode = { + type: 'TEXT', + props: {}, + style: {}, + lines: TEST_LINES, + }; + const result = heightAtLineIndex(node, 12); expect(result).toBe(250); diff --git a/packages/layout/tests/text/layoutText.test.js b/packages/layout/tests/text/layoutText.test.ts similarity index 63% rename from packages/layout/tests/text/layoutText.test.js rename to packages/layout/tests/text/layoutText.test.ts index 011e96a14..a60abd066 100644 --- a/packages/layout/tests/text/layoutText.test.js +++ b/packages/layout/tests/text/layoutText.test.ts @@ -1,13 +1,21 @@ import { describe, expect, test, vi } from 'vitest'; import * as P from '@react-pdf/primitives'; +import FontStore from '@react-pdf/font'; import layoutText from '../../src/text/layoutText'; +import { SafeTextNode } from '../../src/types'; const TEXT = 'Life can be much broader once you discover one simple fact: Everything around you that you call life was made up by people that were no smarter than you'; -const createTextNode = (value, style = {}, props = {}) => ({ +const fontStore = new FontStore(); + +const createTextNode = ( + value: string, + style = {}, + props = {}, +): SafeTextNode => ({ style, props, type: P.Text, @@ -17,64 +25,64 @@ const createTextNode = (value, style = {}, props = {}) => ({ describe('text layoutText', () => { test('Should render empty text', async () => { const node = createTextNode(''); - const lines = layoutText(node, 1500, 200, null); + const lines = layoutText(node, 1500, 200, fontStore); expect(lines).toHaveLength(0); }); test('Should render aligned left text by default', async () => { const node = createTextNode(TEXT); - const lines = layoutText(node, 1500, 30, null); + const lines = layoutText(node, 1500, 30, fontStore); - expect(lines[0].box.x).toBe(0); + expect(lines[0].box!.x).toBe(0); }); test('Should render aligned left text', async () => { const node = createTextNode(TEXT, { textAlign: 'left' }); - const lines = layoutText(node, 1500, 30, null); + const lines = layoutText(node, 1500, 30, fontStore); - expect(lines[0].box.x).toBe(0); + expect(lines[0].box!.x).toBe(0); }); test('Should render aligned right text', async () => { const node = createTextNode(TEXT, { textAlign: 'right' }); - const lines = layoutText(node, 1500, 30, null); - const textWidth = lines[0].runs[0].xAdvance; + const lines = layoutText(node, 1500, 30, fontStore); + const textWidth = lines[0].runs[0].xAdvance!; - expect(lines[0].box.x).toBe(1500 - textWidth); + expect(lines[0].box!.x).toBe(1500 - textWidth); }); test('Should render aligned center text', async () => { const node = createTextNode(TEXT, { textAlign: 'center' }); - const lines = layoutText(node, 1500, 30, null); - const textWidth = lines[0].runs[0].xAdvance; + const lines = layoutText(node, 1500, 30, fontStore); + const textWidth = lines[0].runs[0].xAdvance!; - expect(lines[0].box.x).toBe((1500 - textWidth) / 2); + expect(lines[0].box!.x).toBe((1500 - textWidth) / 2); }); test('Should render single line justified text aligned to the left', async () => { const node = createTextNode(TEXT, { textAlign: 'justify' }); - const lines = layoutText(node, 1500, 30, null); + const lines = layoutText(node, 1500, 30, fontStore); - expect(lines[0].box.x).toBe(0); + expect(lines[0].box!.x).toBe(0); }); test('Should render multiline justified text correctly aligned', async () => { const containerWidth = 800; const node = createTextNode(TEXT, { textAlign: 'justify' }); - const lines = layoutText(node, containerWidth, 100, null); + const lines = layoutText(node, containerWidth, 100, fontStore); const { positions } = lines[0].runs[0]; - const spaceWidth = positions[positions.length - 1].xAdvance; + const spaceWidth = positions![positions!.length - 1].xAdvance; // First line justified. Last line aligned to the left - expect(lines[0].box.width).toBe(containerWidth + spaceWidth); - expect(lines[1].box.width).not.toBe(containerWidth + spaceWidth); + expect(lines[0].box!.width).toBe(containerWidth + spaceWidth); + expect(lines[1].box!.width).not.toBe(containerWidth + spaceWidth); }); test('Should render maxLines', async () => { const node = createTextNode(TEXT, { maxLines: 2 }); - const lines = layoutText(node, 300, 100, null); + const lines = layoutText(node, 300, 100, fontStore); expect(lines.length).toEqual(2); }); @@ -85,7 +93,7 @@ describe('text layoutText', () => { const hyphenationCallback = vi.fn().mockReturnValue(hyphens); const node = createTextNode(text, {}, { hyphenationCallback }); - const lines = layoutText(node, 50, 100, null); + const lines = layoutText(node, 50, 100, fontStore); expect(lines[0].string).toEqual('really-'); expect(lines[1].string).toEqual('long-'); diff --git a/packages/layout/tests/text/lineIndexAtHeight.test.js b/packages/layout/tests/text/lineIndexAtHeight.test.ts similarity index 61% rename from packages/layout/tests/text/lineIndexAtHeight.test.js rename to packages/layout/tests/text/lineIndexAtHeight.test.ts index a5c7b0a43..d5fdb837f 100644 --- a/packages/layout/tests/text/lineIndexAtHeight.test.js +++ b/packages/layout/tests/text/lineIndexAtHeight.test.ts @@ -1,55 +1,92 @@ import { describe, expect, test } from 'vitest'; import lineIndexAtHeight from '../../src/text/lineIndexAtHeight'; +import { SafeTextNode } from '../../src/types'; const TEST_LINE = { box: { height: 25 } }; const TEST_LINES = Array(10).fill(TEST_LINE); describe('text lineIndexAtHeight', () => { test('Should return 0 if no lines present', () => { - const node = { type: 'TEXT' }; + const node: SafeTextNode = { type: 'TEXT', props: {}, style: {} }; const result = lineIndexAtHeight(node, 5); expect(result).toBe(0); }); test('Should return 0 for height lower than first line', () => { - const node = { type: 'TEXT', lines: TEST_LINES }; + const node: SafeTextNode = { + type: 'TEXT', + props: {}, + style: {}, + lines: TEST_LINES, + }; + const result = lineIndexAtHeight(node, 10); expect(result).toBe(0); }); test('Should return 1 for height higher than first line', () => { - const node = { type: 'TEXT', lines: TEST_LINES }; + const node: SafeTextNode = { + type: 'TEXT', + props: {}, + style: {}, + lines: TEST_LINES, + }; + const result = lineIndexAtHeight(node, 30); expect(result).toBe(1); }); test('Should return correct line index for intermediate line', () => { - const node = { type: 'TEXT', lines: TEST_LINES }; + const node: SafeTextNode = { + type: 'TEXT', + props: {}, + style: {}, + lines: TEST_LINES, + }; + const result = lineIndexAtHeight(node, 85); expect(result).toBe(3); }); test('Should return penultimate line index for height lower than last line', () => { - const node = { type: 'TEXT', lines: TEST_LINES }; + const node: SafeTextNode = { + type: 'TEXT', + props: {}, + style: {}, + lines: TEST_LINES, + }; + const result = lineIndexAtHeight(node, 230); expect(result).toBe(9); }); test('Should return correct line index for last line', () => { - const node = { type: 'TEXT', lines: TEST_LINES }; + const node: SafeTextNode = { + type: 'TEXT', + props: {}, + style: {}, + lines: TEST_LINES, + }; + const result = lineIndexAtHeight(node, 250); expect(result).toBe(10); }); test('Should return correct line index for height higher than last line', () => { - const node = { type: 'TEXT', lines: TEST_LINES }; + const node: SafeTextNode = { + type: 'TEXT', + props: {}, + style: {}, + lines: TEST_LINES, + }; + const result = lineIndexAtHeight(node, 300); expect(result).toBe(10); diff --git a/packages/stylesheet/src/resolve/text.ts b/packages/stylesheet/src/resolve/text.ts index 9cf2287c2..122c040d4 100644 --- a/packages/stylesheet/src/resolve/text.ts +++ b/packages/stylesheet/src/resolve/text.ts @@ -63,6 +63,7 @@ const processLineHeight = (key, value, container, styles) => { }; const handlers = { + direction: processNoopValue, fontFamily: processNoopValue, fontSize: processUnitValue, fontStyle: processNoopValue, diff --git a/packages/stylesheet/src/types.ts b/packages/stylesheet/src/types.ts index 3da75c26a..102c81d53 100644 --- a/packages/stylesheet/src/types.ts +++ b/packages/stylesheet/src/types.ts @@ -6,6 +6,8 @@ export type Container = { orientation?: 'landscape' | 'portrait'; }; +export type Percentage = `${string}%`; + // Borders export type BorderStyleValue = 'dashed' | 'dotted' | 'solid'; @@ -46,10 +48,10 @@ export type BorderSafeStyle = BorderExpandedStyle & { borderRightWidth?: number; borderBottomWidth?: number; borderLeftWidth?: number; - borderTopLeftRadius?: number; - borderTopRightRadius?: number; - borderBottomRightRadius?: number; - borderBottomLeftRadius?: number; + borderTopLeftRadius?: number | Percentage; + borderTopRightRadius?: number | Percentage; + borderBottomRightRadius?: number | Percentage; + borderBottomLeftRadius?: number | Percentage; }; export type BorderStyle = BorderShorthandStyle & BorderExpandedStyle; @@ -169,8 +171,8 @@ export type TransformExpandedStyle = { }; export type TransformSafeStyle = TransformExpandedStyle & { - transformOriginX?: number; - transformOriginY?: number; + transformOriginX?: number | Percentage; + transformOriginY?: number | Percentage; }; export type TransformStyle = TransformShorthandStyle & TransformExpandedStyle; @@ -218,12 +220,12 @@ export type DimensionStyle = { export type DimensionExpandedStyle = DimensionStyle; export type DimensionSafeStyle = DimensionExpandedStyle & { - height?: number; - maxHeight?: number; - maxWidth?: number; - minHeight?: number; - minWidth?: number; - width?: number; + height?: number | Percentage; + maxHeight?: number | Percentage; + maxWidth?: number | Percentage; + minHeight?: number | Percentage; + minWidth?: number | Percentage; + width?: number | Percentage; }; // Colors @@ -275,11 +277,17 @@ export type TextDecoration = export type TextDecorationStyle = 'dashed' | 'dotted' | 'solid' | string; -export type TextTransform = 'capitalize' | 'lowercase' | 'uppercase' | 'none'; +export type TextTransform = + | 'capitalize' + | 'lowercase' + | 'uppercase' + | 'upperfirst' + | 'none'; export type VerticalAlign = 'sub' | 'super'; export type TextStyle = { + direction?: 'ltr' | 'rtl'; fontSize?: number | string; fontFamily?: string | string[]; fontStyle?: FontStyle; @@ -322,10 +330,10 @@ export type MarginExpandedStyle = { }; export type MarginSafeStyle = MarginExpandedStyle & { - marginTop?: number; - marginRight?: number; - marginBottom?: number; - marginLeft?: number; + marginTop?: number | Percentage; + marginRight?: number | Percentage; + marginBottom?: number | Percentage; + marginLeft?: number | Percentage; }; export type MarginStyle = MarginShorthandStyle & MarginExpandedStyle; @@ -346,10 +354,10 @@ export type PaddingExpandedStyle = { }; export type PaddingSafeStyle = PaddingExpandedStyle & { - paddingTop?: number; - paddingRight?: number; - paddingBottom?: number; - paddingLeft?: number; + paddingTop?: number | Percentage; + paddingRight?: number | Percentage; + paddingBottom?: number | Percentage; + paddingLeft?: number | Percentage; }; export type PaddingStyle = PaddingShorthandStyle & PaddingExpandedStyle; diff --git a/packages/stylesheet/tests/dimensions.test.ts b/packages/stylesheet/tests/dimensions.test.ts index a74600cd8..5fea0c922 100644 --- a/packages/stylesheet/tests/dimensions.test.ts +++ b/packages/stylesheet/tests/dimensions.test.ts @@ -43,6 +43,12 @@ describe('resolve stylesheet dimensions', () => { expect(styles.width).toBe(200); }); + test('should resolve width percentage dimensions', () => { + const styles = resolveStyle({ width: '50%' }); + + expect(styles.width).toBe('50%'); + }); + test('should resolve min/max width in dimensions', () => { const styles = resolveStyle({ minWidth: '1in', maxWidth: '2in' }); @@ -78,6 +84,13 @@ describe('resolve stylesheet dimensions', () => { expect(styles.maxWidth).toBe(80); }); + test('should resolve min/max width percent dimensions', () => { + const styles = resolveStyle({ minWidth: '50%', maxWidth: '20%' }); + + expect(styles.minWidth).toBe('50%'); + expect(styles.maxWidth).toBe('20%'); + }); + test('should resolve height dimensions', () => { const styles = resolveStyle({ height: '1in' }); @@ -114,6 +127,12 @@ describe('resolve stylesheet dimensions', () => { expect(styles.height).toBe(200); }); + test('should resolve height percentage dimensions', () => { + const styles = resolveStyle({ height: '50%' }); + + expect(styles.height).toBe('50%'); + }); + test('should resolve min/max height in dimensions', () => { const styles = resolveStyle({ minWidth: '1in', maxWidth: '2in' }); @@ -148,4 +167,11 @@ describe('resolve stylesheet dimensions', () => { expect(styles.minHeight).toBe(200); expect(styles.maxHeight).toBe(80); }); + + test('should resolve min/max height percentage dimensions', () => { + const styles = resolveStyle({ minHeight: '50%', maxHeight: '20%' }); + + expect(styles.minHeight).toBe('50%'); + expect(styles.maxHeight).toBe('20%'); + }); }); diff --git a/packages/textkit/package.json b/packages/textkit/package.json index af48e1a4f..d5e49cf3f 100644 --- a/packages/textkit/package.json +++ b/packages/textkit/package.json @@ -28,5 +28,8 @@ "bidi-js": "^1.0.2", "hyphen": "^1.6.4", "unicode-properties": "^1.4.1" + }, + "devDependencies": { + "@types/fontkit": "^2.0.7" } } diff --git a/packages/textkit/src/glyph/fromCodePoint.ts b/packages/textkit/src/glyph/fromCodePoint.ts index 1d58fafcd..4c5bb9c72 100644 --- a/packages/textkit/src/glyph/fromCodePoint.ts +++ b/packages/textkit/src/glyph/fromCodePoint.ts @@ -1,5 +1,4 @@ import { Font } from '../types'; - /** * Get glyph for a given code point * diff --git a/packages/textkit/src/index.ts b/packages/textkit/src/index.ts index 8e2b6c184..bb4be5c43 100644 --- a/packages/textkit/src/index.ts +++ b/packages/textkit/src/index.ts @@ -6,6 +6,7 @@ import textDecoration from './engines/textDecoration'; import scriptItemizer from './engines/scriptItemizer'; import wordHyphenation from './engines/wordHyphenation'; import fontSubstitution from './engines/fontSubstitution'; +import fromFragments from './attributedString/fromFragments'; export { bidi, @@ -15,6 +16,9 @@ export { scriptItemizer, wordHyphenation, fontSubstitution, + fromFragments, }; +export * from './types'; + export default layoutEngine; diff --git a/packages/textkit/src/layout/finalizeFragments.ts b/packages/textkit/src/layout/finalizeFragments.ts index 8dd74a8ba..f5dc92f1d 100644 --- a/packages/textkit/src/layout/finalizeFragments.ts +++ b/packages/textkit/src/layout/finalizeFragments.ts @@ -34,10 +34,10 @@ const getOverflowRight = (line: AttributedString) => { /** * Ignore whitespace at the start and end of a line for alignment * - * @param {Object} line - * @returns {Object} line + * @param line + * @returns Line */ -const adjustOverflow = (line: AttributedString) => { +const adjustOverflow = (line: AttributedString): AttributedString => { const overflowLeft = getOverflowLeft(line); const overflowRight = getOverflowRight(line); @@ -51,9 +51,9 @@ const adjustOverflow = (line: AttributedString) => { /** * Performs line justification by calling appropiate engine * - * @param {Object} engines engines - * @param {Object} options layout options - * @param {string} align text align + * @param engines - Engines + * @param options - Layout options + * @param align - Text align */ const justifyLine = ( engines: Engines, @@ -78,7 +78,7 @@ const justifyLine = ( }; }; -const finalizeLine = (line: AttributedString) => { +const finalizeLine = (line: AttributedString): AttributedString => { let lineAscent = 0; let lineDescent = 0; let lineHeight = 0; @@ -148,7 +148,7 @@ const finalizeFragments = (engines: Engines, options: LayoutOptions) => { * @param paragraphs - Paragraphs * @returns Paragraphs */ - return (paragraphs: Paragraph[]) => { + return (paragraphs: Paragraph[]): Paragraph[] => { const blockFinalizer = finalizeBlock(engines, options); return paragraphs.map((paragraph) => paragraph.map(blockFinalizer)); }; diff --git a/packages/textkit/src/types.ts b/packages/textkit/src/types.ts index c08b9dfe1..3e25d8f58 100644 --- a/packages/textkit/src/types.ts +++ b/packages/textkit/src/types.ts @@ -1,3 +1,5 @@ +import type { Font as FontkitFont, Glyph as FontkitGlyph } from 'fontkit'; + import { Factor as JustificationFactor } from './engines/justification/types'; export type Coordinate = { @@ -18,13 +20,7 @@ export type Container = Rect & { excludeRects?: Rect[]; }; -export type Glyph = { - id: number; - advanceWidth: number; - codePoints: number[]; - isMark?: boolean; - isLigature?: boolean; -}; +export type Glyph = FontkitGlyph; export type Position = { xAdvance: number; @@ -38,45 +34,12 @@ export type Attachment = { y?: number; width?: number; height?: number; + yOffset?: number; + image?: Buffer; }; -export type BBox = { - minX: number; - minY: number; - maxX: number; - maxY: number; - width: number; - height: number; -}; - -export type GlyphRun = { - glyphs: Glyph[]; - positions: Position[]; - stringIndices: number[]; - script: string; - language: string; - direction: string; - features: object; - advanceWidth: number; - advanceHeight: number; - bbox: BBox; -}; - -export type Font = { - ascent?: number; - descent?: number; - height?: number; - unitsPerEm?: number; - lineGap?: number; +export type Font = FontkitFont & { encode?: (string: string) => number[]; - glyphForCodePoint?: (codePoint: number) => Glyph; - layout?: ( - string: string, - userFeatures?: unknown, - script?: unknown, - language?: unknown, - direction?: 'rtl' | 'ltr', - ) => GlyphRun; }; export type Attributes = { @@ -129,6 +92,7 @@ export type Run = { glyphIndices?: number[]; glyphs?: Glyph[]; positions?: Position[]; + xAdvance?: number; }; export type DecorationLine = { @@ -148,6 +112,7 @@ export type AttributedString = { // TODO: Remove these properties overflowLeft?: number; overflowRight?: number; + xAdvance?: number; }; export type Fragment = { diff --git a/packages/textkit/tests/internal/font.ts b/packages/textkit/tests/internal/font.ts index 8f7526a63..3ee0a7c06 100644 --- a/packages/textkit/tests/internal/font.ts +++ b/packages/textkit/tests/internal/font.ts @@ -1,4 +1,6 @@ -import { Font, Glyph, GlyphRun } from '../../src/types'; +import { GlyphRun } from 'fontkit'; + +import { Font, Glyph } from '../../src/types'; const shortLigature = { id: 64257, codePoints: [102, 105], advanceWidth: 10 }; From 973d9c8056c902f92b7e00910b9dfb90ad91ff21 Mon Sep 17 00:00:00 2001 From: Diego Muracciole Date: Mon, 24 Feb 2025 01:25:25 +0100 Subject: [PATCH 3/3] chore: add changeset --- .changeset/short-cups-care.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .changeset/short-cups-care.md diff --git a/.changeset/short-cups-care.md b/.changeset/short-cups-care.md new file mode 100644 index 000000000..cea4b1530 --- /dev/null +++ b/.changeset/short-cups-care.md @@ -0,0 +1,10 @@ +--- +"@react-pdf/layout": minor +"@react-pdf/stylesheet": patch +"@react-pdf/textkit": patch +"@react-pdf/image": patch +"@react-pdf/font": patch +"@react-pdf/fns": patch +--- + +refactor: convert layout package to TS