diff --git a/.changeset/taffy-layout-engine.md b/.changeset/taffy-layout-engine.md new file mode 100644 index 000000000..0269509eb --- /dev/null +++ b/.changeset/taffy-layout-engine.md @@ -0,0 +1,7 @@ +--- +'@react-pdf/layout': minor +--- + +Replace the Yoga layout engine with Taffy (`taffy-layout`). + +Layout defaults are kept Yoga-compatible (column direction, `align-content: flex-start`, items shrink to 0), so most documents render identically. Notable changes: long texts in row containers no longer overlap, `flex: 1` items inside auto-height columns get their content height instead of collapsing, and images/SVGs without an explicit size inside rows take their intrinsic size instead of the full row width. `position: static` is treated as `relative`. The WASM module is shipped as a separate `.wasm` asset that bundlers must be able to resolve via `new URL(..., import.meta.url)`. diff --git a/.prettierignore b/.prettierignore index 2fa6fd605..96b22e3a2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,2 @@ -yoga-layout lib packages/pdfkit diff --git a/CLAUDE.md b/CLAUDE.md index 45f7ce2e6..be92ce84f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,7 +62,7 @@ When making changes, always include tests. Add tests in the same location where ### Code Style - ESLint 9 with TypeScript parser, Prettier (single quotes), enforced via Husky pre-commit hooks -- Packages `yoga` and everything under `apps` are excluded from linting +- Everything under `apps` is excluded from linting ## Previewing PDFs diff --git a/apps/site/app/(home)/page.tsx b/apps/site/app/(home)/page.tsx index bd3a99fbe..560abbd2f 100644 --- a/apps/site/app/(home)/page.tsx +++ b/apps/site/app/(home)/page.tsx @@ -9,7 +9,7 @@ const FEATURES = [ }, { title: 'Layout you already know', - body: 'Yoga brings flexbox to the page: rows, columns, wrapping and absolute positioning.', + body: 'Taffy brings flexbox to the page: rows, columns, wrapping and absolute positioning.', }, { title: 'Beyond text', diff --git a/apps/site/content/docs/v4/tailwind.mdx b/apps/site/content/docs/v4/tailwind.mdx index f661eb5b3..3ecc1a85b 100644 --- a/apps/site/content/docs/v4/tailwind.mdx +++ b/apps/site/content/docs/v4/tailwind.mdx @@ -111,8 +111,8 @@ State variants — `hover:`, `focus:`, `dark:`, `group-*`, `peer-*` — describe - Supports the CSS properties that make sense in a PDF context and are supported by react-pdf — see [valid CSS properties](/docs/v4/styling#valid-css-properties). - Uses `pt` as the internal unit ([valid units](/docs/v4/styling#valid-units)), with `1rem = 12pt` by default. Change it with `ptPerRem`. -- react-pdf uses [Yoga](https://yogalayout.dev/) for layout, so some defaults differ from the web — `flex-direction` defaults to `column`, for example. Add `flex-row` where you need it. +- react-pdf keeps Yoga-style layout defaults (now implemented with [Taffy](https://github.com/DioxusLabs/taffy)), so some differ from the web — `flex-direction` defaults to `column`, for example. Add `flex-row` where you need it. - Line heights are emitted unitless, since react-pdf only supports unitless `lineHeight`. - `aspect-auto` and `line-clamp-none` warn as unsupported. react-pdf has no style value meaning "no aspect ratio" or "no clamp" — leaving the utility off is the reset. -- Intrinsic sizing (`w-fit`, `h-min`, `max-w-max`, …), `max-w-none` / `max-h-none`, and lengths in units react-pdf can't parse (`max-w-prose` is `65ch`) warn as unsupported. Yoga has no equivalent, and passing the value through would throw while laying out the document. +- Intrinsic sizing (`w-fit`, `h-min`, `max-w-max`, …), `max-w-none` / `max-h-none`, and lengths in units react-pdf can't parse (`max-w-prose` is `65ch`) warn as unsupported. react-pdf has no equivalent, and passing the value through would throw while laying out the document. - `float-*` and `clear-*` map to react-pdf's [float support](/docs/v4/floats), which is newer and has rough edges: setting `lineHeight` on floated content breaks text wrap, and parents don't grow to contain their floats. diff --git a/eslint.config.ts b/eslint.config.ts index ddbf9c1dc..bcc9ec385 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -20,7 +20,6 @@ const ignoreConfig = { '**/dist', '**/lib', '**/node_modules', - '**/packages/yoga/**', '**/apps/**', ], }; diff --git a/packages/layout/README.md b/packages/layout/README.md index 9078c4dc2..ac3ab31ab 100644 --- a/packages/layout/README.md +++ b/packages/layout/README.md @@ -4,7 +4,7 @@ # @react-pdf/layout -Layout engine for react-pdf. Takes the document element tree produced by the reconciler and resolves it into an absolutely positioned page tree: it parses styles, measures text with textkit, computes flexbox layout with Yoga, resolves images and SVG, and paginates content into fixed-size pages ready for rendering. +Layout engine for react-pdf. Takes the document element tree produced by the reconciler and resolves it into an absolutely positioned page tree: it parses styles, measures text with textkit, computes flexbox layout with Taffy, resolves images and SVG, and paginates content into fixed-size pages ready for rendering. ## Installation @@ -20,7 +20,7 @@ import layout from '@react-pdf/layout'; const layoutTree = await layout(documentTree, fontStore); ``` -The default export is an async function that composes the individual resolution steps (styles, inheritance, page sizes, Yoga layout, text layout, pagination, and so on) over a document root node and returns the fully resolved tree consumed by `@react-pdf/render`. +The default export is an async function that composes the individual resolution steps (styles, inheritance, page sizes, flexbox layout, text layout, pagination, and so on) over a document root node and returns the fully resolved tree consumed by `@react-pdf/render`. ## License diff --git a/packages/layout/globals.d.ts b/packages/layout/globals.d.ts deleted file mode 100644 index 931254228..000000000 --- a/packages/layout/globals.d.ts +++ /dev/null @@ -1,246 +0,0 @@ -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; - setGapPercent(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 4e6c84aa3..34aec9987 100644 --- a/packages/layout/package.json +++ b/packages/layout/package.json @@ -29,7 +29,7 @@ "@react-pdf/types": "^2.14.0", "emoji-regex-xs": "^1.0.0", "queue": "^6.0.1", - "yoga-layout": "^3.2.1" + "taffy-layout": "^3.0.0" }, "files": [ "lib" diff --git a/packages/layout/src/canvas/measureCanvas.ts b/packages/layout/src/canvas/measureCanvas.ts index 57b192a04..845cdf917 100644 --- a/packages/layout/src/canvas/measureCanvas.ts +++ b/packages/layout/src/canvas/measureCanvas.ts @@ -1,4 +1,4 @@ -import { MeasureFunction } from 'yoga-layout/load'; +import { MeasureFunction } from '../taffy/measure'; import getMargin from '../node/getMargin'; import getPadding from '../node/getPadding'; import isHeightAuto from '../page/isHeightAuto'; @@ -103,7 +103,7 @@ const measureCtx = () => { */ /** - * Yoga canvas measure function + * Canvas measure function * * @param {Object} page * @param {Object} node diff --git a/packages/layout/src/image/measureImage.ts b/packages/layout/src/image/measureImage.ts index 17e8d7c92..9dbed335f 100644 --- a/packages/layout/src/image/measureImage.ts +++ b/packages/layout/src/image/measureImage.ts @@ -1,4 +1,4 @@ -import * as Yoga from 'yoga-layout/load'; +import { MeasureFunction, MeasureMode } from '../taffy/measure'; import getRatio from './getRatio'; import getMargin from '../node/getMargin'; @@ -9,14 +9,14 @@ import { SafeImageNode, SafePageNode } from '../types'; const SAFETY_HEIGHT = 10; /** - * Yoga image measure function + * Image measure function * * @param page - Page * @param node - Node * @returns Measure image */ const measureImage = - (page: SafePageNode, node: SafeImageNode): Yoga.MeasureFunction => + (page: SafePageNode, node: SafeImageNode): MeasureFunction => (width, widthMode, height, heightMode) => { const imageRatio = getRatio(node); const imageMargin = getMargin(node); @@ -36,33 +36,29 @@ const measureImage = if (!node.image) return { width: 0, height: 0 }; if ( - widthMode === Yoga.MeasureMode.Exactly && - heightMode === Yoga.MeasureMode.Undefined + widthMode === MeasureMode.Exactly && + heightMode === 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) + heightMode === MeasureMode.Exactly && + (widthMode === MeasureMode.AtMost || widthMode === MeasureMode.Undefined) ) { return { width: Math.min(height * imageRatio, width) }; } if ( - widthMode === Yoga.MeasureMode.Exactly && - heightMode === Yoga.MeasureMode.AtMost + widthMode === MeasureMode.Exactly && + heightMode === MeasureMode.AtMost ) { const scaledHeight = width / imageRatio; return { height: Math.min(height, pageArea, scaledHeight) }; } - if ( - widthMode === Yoga.MeasureMode.AtMost && - heightMode === Yoga.MeasureMode.AtMost - ) { + if (widthMode === MeasureMode.AtMost && heightMode === MeasureMode.AtMost) { if (imageRatio > 1) { return { width, @@ -76,6 +72,20 @@ const measureImage = }; } + // Unconstrained width (Taffy asks for the max-content size when sizing + // flex items in rows): use the image's intrinsic size, clamped by the + // available height. Yoga never asked; it offered the container width. + if (widthMode === MeasureMode.Undefined) { + const intrinsicWidth = width === 0 ? 0 : node.image.width; + const maxHeight = + heightMode === MeasureMode.AtMost + ? Math.min(height, pageArea) + : pageArea; + const scaledHeight = Math.min(intrinsicWidth / imageRatio, maxHeight); + + return { width: scaledHeight * imageRatio, height: scaledHeight }; + } + return { height, width }; }; diff --git a/packages/layout/src/index.ts b/packages/layout/src/index.ts index 071f4b69f..a2d980b7c 100644 --- a/packages/layout/src/index.ts +++ b/packages/layout/src/index.ts @@ -1,7 +1,7 @@ import { asyncCompose } from '@react-pdf/fns'; import resolveSvg from './steps/resolveSvg'; -import resolveYoga from './steps/resolveYoga'; +import resolveTaffy from './steps/resolveTaffy'; import resolveZIndex from './steps/resolveZIndex'; import resolveAssets from './steps/resolveAssets'; import resolveStyles from './steps/resolveStyles'; @@ -55,7 +55,7 @@ const layout = asyncCompose( resolveBookmarks, resolvePageTemplates, resolvePageSizes, - resolveYoga, + resolveTaffy, ); export * from './types'; diff --git a/packages/layout/src/node/getBorderWidth.ts b/packages/layout/src/node/getBorderWidth.ts deleted file mode 100644 index f45836e7a..000000000 --- a/packages/layout/src/node/getBorderWidth.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as Yoga from 'yoga-layout/load'; - -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 node - * @returns Border widths - */ -const getBorderWidth = (node: SafeNode) => { - const { yogaNode } = node; - - return { - borderTopWidth: getComputedBorder(yogaNode, Yoga.Edge.Top), - borderRightWidth: getComputedBorder(yogaNode, Yoga.Edge.Right), - borderBottomWidth: getComputedBorder(yogaNode, Yoga.Edge.Bottom), - borderLeftWidth: getComputedBorder(yogaNode, Yoga.Edge.Left), - }; -}; - -export default getBorderWidth; diff --git a/packages/layout/src/node/getDimension.ts b/packages/layout/src/node/getDimension.ts deleted file mode 100644 index 7c6885521..000000000 --- a/packages/layout/src/node/getDimension.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { SafeNode } from '../types'; - -const DEFAULT_DIMENSION = { - width: 0, - height: 0, -}; - -/** - * Get Yoga computed dimensions. Zero otherwise - * - * @param node - * @returns Dimensions - */ -const getDimension = (node: SafeNode) => { - const { yogaNode } = node; - - if (!yogaNode) return DEFAULT_DIMENSION; - - return { - width: yogaNode.getComputedWidth(), - height: yogaNode.getComputedHeight(), - }; -}; - -export default getDimension; diff --git a/packages/layout/src/node/getMargin.ts b/packages/layout/src/node/getMargin.ts index 7e615df45..4ee47c88d 100644 --- a/packages/layout/src/node/getMargin.ts +++ b/packages/layout/src/node/getMargin.ts @@ -1,14 +1,8 @@ -import * as Yoga from 'yoga-layout/load'; - import { SafeNode } from '../types'; -const getComputedMargin = (node: SafeNode, edge: Yoga.Edge) => { - const { yogaNode } = node; - return yogaNode ? yogaNode.getComputedMargin(edge) : null; -}; - /** - * Get Yoga computed magins. Zero otherwise + * Get node margins from its box, falling back to style values. Zero otherwise. + * Used while measuring, before the layout engine has produced a box. * * @param node * @returns Margins @@ -16,29 +10,10 @@ const getComputedMargin = (node: SafeNode, edge: Yoga.Edge) => { const getMargin = (node: SafeNode) => { const { style, box } = node; - const marginTop = - getComputedMargin(node, Yoga.Edge.Top) || - box?.marginTop || - style?.marginTop || - 0; - - const marginRight = - getComputedMargin(node, Yoga.Edge.Right) || - box?.marginRight || - style?.marginRight || - 0; - - const marginBottom = - getComputedMargin(node, Yoga.Edge.Bottom) || - box?.marginBottom || - style?.marginBottom || - 0; - - const marginLeft = - getComputedMargin(node, Yoga.Edge.Left) || - box?.marginLeft || - style?.marginLeft || - 0; + const marginTop = box?.marginTop || style?.marginTop || 0; + const marginRight = box?.marginRight || style?.marginRight || 0; + const marginBottom = box?.marginBottom || style?.marginBottom || 0; + const marginLeft = box?.marginLeft || style?.marginLeft || 0; return { marginTop, marginRight, marginBottom, marginLeft }; }; diff --git a/packages/layout/src/node/getPadding.ts b/packages/layout/src/node/getPadding.ts index 221954375..09869bada 100644 --- a/packages/layout/src/node/getPadding.ts +++ b/packages/layout/src/node/getPadding.ts @@ -1,14 +1,8 @@ -import * as Yoga from 'yoga-layout/load'; - import { SafeNode } from '../types'; -const getComputedPadding = (node: SafeNode, edge: Yoga.Edge) => { - const { yogaNode } = node; - return yogaNode ? yogaNode.getComputedPadding(edge) : null; -}; - /** - * Get Yoga computed paddings. Zero otherwise + * Get node paddings from its box, falling back to style values. Zero otherwise. + * Used while measuring, before the layout engine has produced a box. * * @param node * @returns paddings @@ -16,29 +10,10 @@ const getComputedPadding = (node: SafeNode, edge: Yoga.Edge) => { const getPadding = (node: SafeNode) => { const { style, box } = node; - const paddingTop = - getComputedPadding(node, Yoga.Edge.Top) || - box?.paddingTop || - style?.paddingTop || - 0; - - const paddingRight = - getComputedPadding(node, Yoga.Edge.Right) || - box?.paddingRight || - style?.paddingRight || - 0; - - const paddingBottom = - getComputedPadding(node, Yoga.Edge.Bottom) || - box?.paddingBottom || - style?.paddingBottom || - 0; - - const paddingLeft = - getComputedPadding(node, Yoga.Edge.Left) || - box?.paddingLeft || - style?.paddingLeft || - 0; + const paddingTop = box?.paddingTop || style?.paddingTop || 0; + const paddingRight = box?.paddingRight || style?.paddingRight || 0; + const paddingBottom = box?.paddingBottom || style?.paddingBottom || 0; + const paddingLeft = box?.paddingLeft || style?.paddingLeft || 0; return { paddingTop, paddingRight, paddingBottom, paddingLeft }; }; diff --git a/packages/layout/src/node/getPosition.ts b/packages/layout/src/node/getPosition.ts deleted file mode 100644 index 986df5db3..000000000 --- a/packages/layout/src/node/getPosition.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { SafeNode } from '../types'; - -/** - * Get Yoga computed position. Zero otherwise - * - * @param node - * @returns Position - */ -const getPosition = (node: SafeNode) => { - const { yogaNode } = node; - - return { - top: yogaNode?.getComputedTop() || 0, - right: yogaNode?.getComputedRight() || 0, - bottom: yogaNode?.getComputedBottom() || 0, - left: yogaNode?.getComputedLeft() || 0, - }; -}; - -export default getPosition; diff --git a/packages/layout/src/node/removePaddings.ts b/packages/layout/src/node/removePaddings.ts index 19b971e29..968f34796 100644 --- a/packages/layout/src/node/removePaddings.ts +++ b/packages/layout/src/node/removePaddings.ts @@ -1,6 +1,5 @@ import { omit } from '@react-pdf/fns'; -import setPadding from './setPadding'; import { SafeNode } from '../types'; const PADDING_PROPS = [ @@ -21,11 +20,7 @@ const PADDING_PROPS = [ */ const removePaddings = (node: SafeNode) => { const style = omit(PADDING_PROPS, node.style || {}); - const newNode: SafeNode = Object.assign({}, node, { style }); - - setPadding(0)(newNode); - - return newNode; + return Object.assign({}, node, { style }) as SafeNode; }; export default removePaddings; diff --git a/packages/layout/src/node/setAlign.ts b/packages/layout/src/node/setAlign.ts deleted file mode 100644 index 03b768659..000000000 --- a/packages/layout/src/node/setAlign.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as Yoga from 'yoga-layout/load'; -import { upperFirst } from '@react-pdf/fns'; -import { SafeNode } from '../types'; - -const ALIGN = { - 'flex-start': Yoga.Align.FlexStart, - center: Yoga.Align.Center, - 'flex-end': Yoga.Align.FlexEnd, - stretch: Yoga.Align.Stretch, - baseline: Yoga.Align.Baseline, - 'space-between': Yoga.Align.SpaceBetween, - 'space-around': Yoga.Align.SpaceAround, - 'space-evenly': Yoga.Align.SpaceEvenly, -}; - -/** - * Set generic align attribute to node's Yoga instance - * - * @param attr - Specific align property - * @param value - Specific align value - * @param node - Node - * @returns Node - */ -const setAlign = (attr: string) => (value: any) => (node: SafeNode) => { - const { yogaNode } = node; - const defaultValue = attr === 'items' ? Yoga.Align.Stretch : Yoga.Align.Auto; - - if (yogaNode) { - const align = ALIGN[value] || defaultValue; - - yogaNode[`setAlign${upperFirst(attr)}`](align); - } - - return node; -}; - -export default setAlign; diff --git a/packages/layout/src/node/setAlignContent.ts b/packages/layout/src/node/setAlignContent.ts deleted file mode 100644 index b7c24d977..000000000 --- a/packages/layout/src/node/setAlignContent.ts +++ /dev/null @@ -1,12 +0,0 @@ -import setAlign from './setAlign'; - -/** - * Set align content attribute to node's Yoga instance - * - * @param align - Value - * @param node - Instance - * @returns Node instance - */ -const setAlignContent = setAlign('content'); - -export default setAlignContent; diff --git a/packages/layout/src/node/setAlignItems.ts b/packages/layout/src/node/setAlignItems.ts deleted file mode 100644 index cb4da9dba..000000000 --- a/packages/layout/src/node/setAlignItems.ts +++ /dev/null @@ -1,12 +0,0 @@ -import setAlign from './setAlign'; - -/** - * Set align items attribute to node's Yoga instance - * - * @param align - Value - * @param node - Node instance - * @returns Node instance - */ -const setAlignItems = setAlign('items'); - -export default setAlignItems; diff --git a/packages/layout/src/node/setAlignSelf.ts b/packages/layout/src/node/setAlignSelf.ts deleted file mode 100644 index 938a89060..000000000 --- a/packages/layout/src/node/setAlignSelf.ts +++ /dev/null @@ -1,12 +0,0 @@ -import setAlign from './setAlign'; - -/** - * Set align self attribute to node's Yoga instance - * - * @param align - Value - * @param node - Node instance - * @returns Node instance - */ -const setAlignSelf = setAlign('self'); - -export default setAlignSelf; diff --git a/packages/layout/src/node/setAspectRatio.ts b/packages/layout/src/node/setAspectRatio.ts deleted file mode 100644 index d81abee58..000000000 --- a/packages/layout/src/node/setAspectRatio.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { isNil } from '@react-pdf/fns'; -import { SafeNode } from '../types'; - -/** - * Set aspect ratio attribute to node's Yoga instance - * - * @param value - Ratio - * @returns Node instance - */ -const setAspectRatio = (value?: number | null) => (node: SafeNode) => { - const { yogaNode } = node; - - if (!isNil(value) && yogaNode) { - yogaNode.setAspectRatio(value); - } - - return node; -}; - -export default setAspectRatio; diff --git a/packages/layout/src/node/setBorderWidth.ts b/packages/layout/src/node/setBorderWidth.ts deleted file mode 100644 index 603a6a9e7..000000000 --- a/packages/layout/src/node/setBorderWidth.ts +++ /dev/null @@ -1,57 +0,0 @@ -import * as Yoga from 'yoga-layout/load'; - -import setYogaValue from './setYogaValue'; -import { SafeNode } from '../types'; - -/** - * Set border top attribute to node's Yoga 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 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 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 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 width - Border width - * @returns Node instance wrapper - */ -export const setBorder = (width?: number | null) => (node: SafeNode) => { - setBorderTop(width)(node); - setBorderRight(width)(node); - setBorderBottom(width)(node); - setBorderLeft(width)(node); - - return node; -}; - -export default setBorder; diff --git a/packages/layout/src/node/setDimension.ts b/packages/layout/src/node/setDimension.ts deleted file mode 100644 index 9c7f9bd1a..000000000 --- a/packages/layout/src/node/setDimension.ts +++ /dev/null @@ -1,55 +0,0 @@ -import setYogaValue from './setYogaValue'; - -/** - * Set width to node's Yoga 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 min - Width - * @param node - Node instance - * @returns Node instance - */ -export const setMinWidth = setYogaValue('minWidth'); - -/** - * Set max width to node's Yoga instance - * - * @param max - Width - * @param node - Node instance - * @returns Node instance - */ -export const setMaxWidth = setYogaValue('maxWidth'); - -/** - * Set height to node's Yoga 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 min - Height - * @param node - Node instance - * @returns Node instance - */ -export const setMinHeight = setYogaValue('minHeight'); - -/** - * Set max height to node's Yoga instance - * - * @param max - Height - * @param node - Node instance - * @returns Node instance - */ -export const setMaxHeight = setYogaValue('maxHeight'); diff --git a/packages/layout/src/node/setDisplay.ts b/packages/layout/src/node/setDisplay.ts deleted file mode 100644 index 08f873007..000000000 --- a/packages/layout/src/node/setDisplay.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as Yoga from 'yoga-layout/load'; -import { SafeNode } from '../types'; - -/** - * Set display attribute to node's Yoga instance - * - * @param value - Display - * @returns Node instance wrapper - */ -const setDisplay = (value?: string | null) => (node: SafeNode) => { - const { yogaNode } = node; - - if (yogaNode) { - yogaNode.setDisplay( - value === 'none' ? Yoga.Display.None : Yoga.Display.Flex, - ); - } - - return node; -}; - -export default setDisplay; diff --git a/packages/layout/src/node/setFlexBasis.ts b/packages/layout/src/node/setFlexBasis.ts deleted file mode 100644 index 47d657f38..000000000 --- a/packages/layout/src/node/setFlexBasis.ts +++ /dev/null @@ -1,12 +0,0 @@ -import setYogaValue from './setYogaValue'; - -/** - * Set flex basis attribute to node's Yoga instance - * - * @param flex - Basis value - * @param node - Node instance - * @returns Node instance - */ -const setFlexBasis = setYogaValue('flexBasis'); - -export default setFlexBasis; diff --git a/packages/layout/src/node/setFlexDirection.ts b/packages/layout/src/node/setFlexDirection.ts deleted file mode 100644 index f572d0dc2..000000000 --- a/packages/layout/src/node/setFlexDirection.ts +++ /dev/null @@ -1,28 +0,0 @@ -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, -}; - -/** - * Set flex direction attribute to node's Yoga instance - * - * @param value - Flex direction value - * @returns Node instance wrapper - */ -const setFlexDirection = (value?: string | null) => (node: SafeNode) => { - const { yogaNode } = node; - - if (yogaNode) { - const flexDirection = FLEX_DIRECTIONS[value] || Yoga.FlexDirection.Column; - yogaNode.setFlexDirection(flexDirection); - } - - return node; -}; - -export default setFlexDirection; diff --git a/packages/layout/src/node/setFlexGrow.ts b/packages/layout/src/node/setFlexGrow.ts deleted file mode 100644 index 9ee51ebc6..000000000 --- a/packages/layout/src/node/setFlexGrow.ts +++ /dev/null @@ -1,14 +0,0 @@ -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.ts b/packages/layout/src/node/setFlexShrink.ts deleted file mode 100644 index f9bdff0b1..000000000 --- a/packages/layout/src/node/setFlexShrink.ts +++ /dev/null @@ -1,14 +0,0 @@ -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.ts b/packages/layout/src/node/setFlexWrap.ts deleted file mode 100644 index 23380652f..000000000 --- a/packages/layout/src/node/setFlexWrap.ts +++ /dev/null @@ -1,26 +0,0 @@ -import * as Yoga from 'yoga-layout/load'; -import { SafeNode } from '../types'; - -const FLEX_WRAP = { - wrap: Yoga.Wrap.Wrap, - 'wrap-reverse': Yoga.Wrap.WrapReverse, -}; - -/** - * Set flex wrap attribute to node's Yoga instance - * - * @param value - Flex wrap value - * @returns Node instance wrapper - */ -const setFlexWrap = (value?: string | null) => (node: SafeNode) => { - const { yogaNode } = node; - - if (yogaNode) { - const flexWrap: Yoga.Wrap = FLEX_WRAP[value] || Yoga.Wrap.NoWrap; - yogaNode.setFlexWrap(flexWrap); - } - - return node; -}; - -export default setFlexWrap; diff --git a/packages/layout/src/node/setGap.ts b/packages/layout/src/node/setGap.ts deleted file mode 100644 index c3a195b44..000000000 --- a/packages/layout/src/node/setGap.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as Yoga from 'yoga-layout/load'; - -import setYogaValue from './setYogaValue'; - -/** - * Set rowGap value to node's Yoga instance - * - * @param value - Gap value - * @returns Node instance wrapper - */ -export const setRowGap = setYogaValue('gap', Yoga.Gutter.Row); - -/** - * Set columnGap value to node's Yoga instance - * - * @param value - Gap value - * @returns Node instance wrapper - */ -export const setColumnGap = setYogaValue('gap', Yoga.Gutter.Column); diff --git a/packages/layout/src/node/setJustifyContent.ts b/packages/layout/src/node/setJustifyContent.ts deleted file mode 100644 index 0aba1b903..000000000 --- a/packages/layout/src/node/setJustifyContent.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as Yoga from 'yoga-layout/load'; -import { isNil } from '@react-pdf/fns'; -import { SafeNode } from '../types'; - -const JUSTIFY_CONTENT = { - center: Yoga.Justify.Center, - 'flex-end': Yoga.Justify.FlexEnd, - 'space-between': Yoga.Justify.SpaceBetween, - 'space-around': Yoga.Justify.SpaceAround, - 'space-evenly': Yoga.Justify.SpaceEvenly, -}; - -/** - * Set justify content attribute to node's Yoga instance - * - * @param value - Justify content value - * @returns Node instance wrapper - */ -const setJustifyContent = (value?: string | null) => (node: SafeNode) => { - const { yogaNode } = node; - - if (!isNil(value) && yogaNode) { - const justifyContent = JUSTIFY_CONTENT[value] || Yoga.Justify.FlexStart; - yogaNode.setJustifyContent(justifyContent); - } - - return node; -}; - -export default setJustifyContent; diff --git a/packages/layout/src/node/setMargin.ts b/packages/layout/src/node/setMargin.ts deleted file mode 100644 index 9a2b0dea9..000000000 --- a/packages/layout/src/node/setMargin.ts +++ /dev/null @@ -1,58 +0,0 @@ -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.ts b/packages/layout/src/node/setOverflow.ts deleted file mode 100644 index ed74d5e6c..000000000 --- a/packages/layout/src/node/setOverflow.ts +++ /dev/null @@ -1,27 +0,0 @@ -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, -}; - -/** - * Set overflow attribute to node's Yoga instance - * - * @param value - Overflow value - * @returns Node instance wrapper - */ -const setOverflow = (value?: string | null) => (node: SafeNode) => { - const { yogaNode } = node; - - if (!isNil(value) && yogaNode) { - const overflow = OVERFLOW[value] || Yoga.Overflow.Visible; - yogaNode.setOverflow(overflow); - } - - return node; -}; - -export default setOverflow; diff --git a/packages/layout/src/node/setPadding.ts b/packages/layout/src/node/setPadding.ts deleted file mode 100644 index 55d3cf79d..000000000 --- a/packages/layout/src/node/setPadding.ts +++ /dev/null @@ -1,58 +0,0 @@ -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.ts b/packages/layout/src/node/setPosition.ts deleted file mode 100644 index cf41dcfde..000000000 --- a/packages/layout/src/node/setPosition.ts +++ /dev/null @@ -1,58 +0,0 @@ -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.ts b/packages/layout/src/node/setPositionType.ts deleted file mode 100644 index 3230eb1b6..000000000 --- a/packages/layout/src/node/setPositionType.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as Yoga from 'yoga-layout/load'; -import { isNil } from '@react-pdf/fns'; -import { SafeNode } from '../types'; - -const POSITION = { - absolute: Yoga.PositionType.Absolute, - relative: Yoga.PositionType.Relative, - static: Yoga.PositionType.Static, -}; - -/** - * Set position type attribute to node's Yoga instance - * - * @param value - Position position type - * @returns Node instance - */ -const setPositionType = (value?: string | null) => (node: SafeNode) => { - const { yogaNode } = node; - - if (!isNil(value) && yogaNode) { - yogaNode.setPositionType(POSITION[value]); - } - - return node; -}; - -export default setPositionType; diff --git a/packages/layout/src/node/setYogaValue.ts b/packages/layout/src/node/setYogaValue.ts deleted file mode 100644 index 3db301755..000000000 --- a/packages/layout/src/node/setYogaValue.ts +++ /dev/null @@ -1,52 +0,0 @@ -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 | Yoga.Gutter) => - (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/toTaffyStyle.ts b/packages/layout/src/node/toTaffyStyle.ts new file mode 100644 index 000000000..e35edb9a9 --- /dev/null +++ b/packages/layout/src/node/toTaffyStyle.ts @@ -0,0 +1,160 @@ +import { isNil, matchPercent } from '@react-pdf/fns'; +import * as P from '@react-pdf/primitives'; +import { + AlignContent, + AlignItems, + AlignSelf, + Dimension, + Display, + FlexDirection, + FlexWrap, + JustifyContent, + LengthPercentage, + Overflow, + Position, + StylePropertyValues, +} from 'taffy-layout'; + +import { SafeNode } from '../types'; + +type Value = string | number | null | undefined; + +/** + * Normalise a react-pdf style value into a Taffy length or percentage. + * Numbers pass through, `'50%'` stays `'50%'`, anything else is dropped. + */ +const length = (value: Value): LengthPercentage | undefined => { + if (typeof value === 'number') + return Number.isFinite(value) ? value : undefined; + if (isNil(value)) return undefined; + + const percent = matchPercent(value); + + return percent ? `${percent.value}%` : undefined; +}; + +/** Like `length`, but also accepts `'auto'` */ +const dimension = (value: Value): Dimension | undefined => + value === 'auto' ? 'auto' : length(value); + +const ALIGN_ITEMS: Record = { + 'flex-start': AlignItems.FlexStart, + center: AlignItems.Center, + 'flex-end': AlignItems.FlexEnd, + stretch: AlignItems.Stretch, + baseline: AlignItems.Baseline, +}; + +const ALIGN_SELF: Record = { + auto: AlignSelf.Auto, + 'flex-start': AlignSelf.FlexStart, + center: AlignSelf.Center, + 'flex-end': AlignSelf.FlexEnd, + stretch: AlignSelf.Stretch, + baseline: AlignSelf.Baseline, +}; + +const ALIGN_CONTENT: Record = { + 'flex-start': AlignContent.FlexStart, + center: AlignContent.Center, + 'flex-end': AlignContent.FlexEnd, + stretch: AlignContent.Stretch, + 'space-between': AlignContent.SpaceBetween, + 'space-around': AlignContent.SpaceAround, + 'space-evenly': AlignContent.SpaceEvenly, +}; + +const JUSTIFY_CONTENT: Record = { + 'flex-start': JustifyContent.FlexStart, + center: JustifyContent.Center, + 'flex-end': JustifyContent.FlexEnd, + 'space-between': JustifyContent.SpaceBetween, + 'space-around': JustifyContent.SpaceAround, + 'space-evenly': JustifyContent.SpaceEvenly, +}; + +const FLEX_DIRECTION: Record = { + row: FlexDirection.Row, + 'row-reverse': FlexDirection.RowReverse, + column: FlexDirection.Column, + 'column-reverse': FlexDirection.ColumnReverse, +}; + +const FLEX_WRAP: Record = { + nowrap: FlexWrap.NoWrap, + wrap: FlexWrap.Wrap, + 'wrap-reverse': FlexWrap.WrapReverse, +}; + +const OVERFLOW: Record = { + hidden: Overflow.Hidden, +}; + +/** + * Map a node's resolved style onto Taffy style properties. + * + * Defaults are chosen to match what Yoga (without web defaults) used to do, + * so existing documents keep their layout: + * - `flexDirection: column`, `alignContent: flex-start` + * - `minWidth`/`minHeight` default to `0` instead of Taffy's content-based + * automatic minimum, so flex items shrink like they did under Yoga. + * + * @param node - Node with resolved `style` (and `box` for pages) + * @returns Taffy style properties + */ +const toTaffyStyle = (node: SafeNode): StylePropertyValues => { + const style = node.style || {}; + const position = style.position; + const isAbsolute = position === 'absolute'; + const props: StylePropertyValues = { + display: style.display === 'none' ? Display.None : Display.Flex, + position: isAbsolute ? Position.Absolute : Position.Relative, + flexDirection: FLEX_DIRECTION[style.flexDirection] ?? FlexDirection.Column, + flexWrap: FLEX_WRAP[style.flexWrap] ?? FlexWrap.NoWrap, + alignItems: ALIGN_ITEMS[style.alignItems] ?? AlignItems.Stretch, + alignSelf: ALIGN_SELF[style.alignSelf], + alignContent: ALIGN_CONTENT[style.alignContent] ?? AlignContent.FlexStart, + justifyContent: + JUSTIFY_CONTENT[style.justifyContent] ?? JustifyContent.FlexStart, + overflowX: OVERFLOW[style.overflow] ?? Overflow.Visible, + overflowY: OVERFLOW[style.overflow] ?? Overflow.Visible, + flexGrow: isNil(style.flexGrow) ? 0 : Number(style.flexGrow), + flexShrink: isNil(style.flexShrink) ? 1 : Number(style.flexShrink), + flexBasis: dimension(style.flexBasis) ?? 'auto', + aspectRatio: isNil(style.aspectRatio) ? undefined : style.aspectRatio, + width: dimension(style.width) ?? 'auto', + height: + dimension(node.type === P.Page ? node.box?.height : style.height) ?? + 'auto', + minWidth: length(style.minWidth) ?? 0, + minHeight: length(style.minHeight) ?? 0, + maxWidth: length(style.maxWidth) ?? 'auto', + maxHeight: length(style.maxHeight) ?? 'auto', + marginTop: dimension(style.marginTop) ?? 0, + marginRight: dimension(style.marginRight) ?? 0, + marginBottom: dimension(style.marginBottom) ?? 0, + marginLeft: dimension(style.marginLeft) ?? 0, + paddingTop: length(style.paddingTop) ?? 0, + paddingRight: length(style.paddingRight) ?? 0, + paddingBottom: length(style.paddingBottom) ?? 0, + paddingLeft: length(style.paddingLeft) ?? 0, + borderTop: length(style.borderTopWidth) ?? 0, + borderRight: length(style.borderRightWidth) ?? 0, + borderBottom: length(style.borderBottomWidth) ?? 0, + borderLeft: length(style.borderLeftWidth) ?? 0, + rowGap: length(style.rowGap) ?? 0, + columnGap: length(style.columnGap) ?? 0, + }; + + // Yoga ignored insets on `position: static` nodes + if (position !== 'static') { + props.top = dimension(style.top) ?? 'auto'; + props.right = dimension(style.right) ?? 'auto'; + props.bottom = dimension(style.bottom) ?? 'auto'; + props.left = dimension(style.left) ?? 'auto'; + } + + return props; +}; + +export default toTaffyStyle; diff --git a/packages/layout/src/paginate/fromPage.ts b/packages/layout/src/paginate/fromPage.ts index 8e7228bfa..6cf0acebb 100644 --- a/packages/layout/src/paginate/fromPage.ts +++ b/packages/layout/src/paginate/fromPage.ts @@ -15,7 +15,7 @@ const marginTop = (node: SafeNode) => numeric(node.box?.marginTop); const marginBottom = (node: SafeNode) => numeric(node.box?.marginBottom); -// A container that starts and ends here keeps its subtree untouched: the yoga +// A container that starts and ends here keeps its subtree untouched: the layout // boxes below it are still valid, and out-of-flow children the engine never // saw stay where they are. const isWhole = (placed: PlacedNode) => diff --git a/packages/layout/src/paginate/index.ts b/packages/layout/src/paginate/index.ts index 9b0de5dd9..08409e002 100644 --- a/packages/layout/src/paginate/index.ts +++ b/packages/layout/src/paginate/index.ts @@ -19,7 +19,6 @@ import { SafeDocumentNode, SafeNode, SafePageNode, - YogaInstance, } from '../types'; import { PageCtx } from './types'; @@ -34,7 +33,6 @@ type Totals = { totalPages: number; subTotals: number[] } | null; type Ctx = { totals: Totals; fontStore: FontStore; - yoga: YogaInstance; }; // Walk from `root` to `target` accumulating parent-relative tops and lefts @@ -95,12 +93,12 @@ const validateRegion = ( const splitPage = ( page: SafePageNode, props: PageCtx['props'], - { fontStore, yoga }: Ctx, + { fontStore }: Ctx, ): SafePageNode[] => { const template: PageLayout | undefined = (page.props as any)?.layout; const box = { ...page.box, height: page.style.height as number }; - const pageCtx: PageCtx = { props, page, fontStore, yoga }; + const pageCtx: PageCtx = { props, page, fontStore }; const content = collectContent(page as unknown as SafeNode); const paginator = createPaginator(toFlow(content, pageCtx)); @@ -113,7 +111,7 @@ const splitPage = ( const measureRegion = (pageNumber: number) => { const nodes = instantiate(props(pageNumber), [probeElement() as any]); const fake = { ...page, box, children: nodes }; - const laid = relayoutPage(fake as any, fontStore, yoga) as SafePageNode; + const laid = relayoutPage(fake as any, fontStore) as SafePageNode; const probe = findProbe(laid)!; return absoluteBox(laid, probe)!; @@ -147,7 +145,7 @@ const splitPage = ( children: nodes, } as SafePageNode; - pages.push(relayoutPage(built as any, fontStore, yoga) as SafePageNode); + pages.push(relayoutPage(built as any, fontStore) as SafePageNode); pageNumber += 1; } @@ -206,7 +204,7 @@ const resolvePagination = ( root: SafeDocumentNode, fontStore: FontStore, ): SafeDocumentNode => { - const ctx1 = { totals: null, fontStore, yoga: root.yoga }; + const ctx1 = { totals: null, fontStore }; const round1 = paginateDocument(root, ctx1); if (!needsTotalsRound(root)) return round1.root; diff --git a/packages/layout/src/paginate/toFlow.ts b/packages/layout/src/paginate/toFlow.ts index 01de2b391..3d34e17c7 100644 --- a/packages/layout/src/paginate/toFlow.ts +++ b/packages/layout/src/paginate/toFlow.ts @@ -85,7 +85,7 @@ const leafOf = (node: SafeNode): LeafNode => ({ }); // Re-render a dynamic subtree and measure it at the width the first pass -// gave it. A throwaway page runs it through the standard style and yoga +// gave it. A throwaway page runs it through the standard style and layout // steps; like the first pass, it has no height constraint so content can // be any length. const measure = ( @@ -113,11 +113,7 @@ const measure = ( children: [rendered], }; - const laid = relayoutPage( - fake as any, - ctx.fontStore, - ctx.yoga, - ) as SafePageNode; + const laid = relayoutPage(fake as any, ctx.fontStore) as SafePageNode; const measured = laid.children![0] as SafeNode; return { ...measured, box: { ...measured.box, top: 0 } } as SafeNode; @@ -195,7 +191,7 @@ const toItem = (node: SafeNode, ctx: PageCtx): FlowNode => { } }; -// Page content as public flow nodes: geometry copied from yoga boxes, react-pdf +// Page content as public flow nodes: geometry copied from layout boxes, react-pdf // props mapped to flags, splitting and dynamic re-rendering left as closures. const toFlow = (nodes: SafeNode[], ctx: PageCtx): FlowNode[] => nodes.map((child) => { diff --git a/packages/layout/src/paginate/types.ts b/packages/layout/src/paginate/types.ts index 059a5d7c2..9ca9acce7 100644 --- a/packages/layout/src/paginate/types.ts +++ b/packages/layout/src/paginate/types.ts @@ -1,6 +1,6 @@ import FontStore from '@react-pdf/font'; -import { DynamicPageProps, SafePageNode, YogaInstance } from '../types'; +import { DynamicPageProps, SafePageNode } from '../types'; // Per-page pagination context: `props` maps an engine page number to the // render props for that page, and the rest lets dynamic nodes re-measure @@ -9,5 +9,4 @@ export type PageCtx = { props: (enginePageNumber: number) => DynamicPageProps; page: SafePageNode; fontStore: FontStore; - yoga: YogaInstance; }; diff --git a/packages/layout/src/steps/relayoutPage.ts b/packages/layout/src/steps/relayoutPage.ts index 535a0365d..6e6a0de74 100644 --- a/packages/layout/src/steps/relayoutPage.ts +++ b/packages/layout/src/steps/relayoutPage.ts @@ -6,7 +6,7 @@ import resolveInheritance from './resolveInheritance'; import { resolvePageDimensions } from './resolveDimensions'; import { resolvePageStyles } from './resolveStyles'; -// Run a page back through the style and yoga steps. Used wherever a page's +// Run a page back through the style and layout steps. Used wherever a page's // contents changed after the first pass: dynamic content that rendered, or a // finished page being given its real height. const relayoutPage = compose( diff --git a/packages/layout/src/steps/resolveDimensions.ts b/packages/layout/src/steps/resolveDimensions.ts index 3db12abb4..f9ff8e43c 100644 --- a/packages/layout/src/steps/resolveDimensions.ts +++ b/packages/layout/src/steps/resolveDimensions.ts @@ -1,68 +1,30 @@ import * as P from '@react-pdf/primitives'; -import { isNil, compose } from '@react-pdf/fns'; +import { isNil } from '@react-pdf/fns'; import FontStore from '@react-pdf/font'; +import { Style, TaffyTree } from 'taffy-layout'; +import toTaffyStyle from '../node/toTaffyStyle'; import getMargin from '../node/getMargin'; import getPadding from '../node/getPadding'; -import getPosition from '../node/getPosition'; -import getDimension from '../node/getDimension'; -import getBorderWidth from '../node/getBorderWidth'; -import setDisplay from '../node/setDisplay'; -import setOverflow from '../node/setOverflow'; -import setFlexWrap from '../node/setFlexWrap'; -import setFlexGrow from '../node/setFlexGrow'; -import setFlexBasis from '../node/setFlexBasis'; -import setAlignSelf from '../node/setAlignSelf'; -import setAlignItems from '../node/setAlignItems'; -import setFlexShrink from '../node/setFlexShrink'; -import setAspectRatio from '../node/setAspectRatio'; -import setAlignContent from '../node/setAlignContent'; -import setPositionType from '../node/setPositionType'; -import setFlexDirection from '../node/setFlexDirection'; -import setJustifyContent from '../node/setJustifyContent'; -import { - setMarginTop, - setMarginRight, - setMarginBottom, - setMarginLeft, -} from '../node/setMargin'; -import { - setPaddingTop, - setPaddingRight, - setPaddingBottom, - setPaddingLeft, -} from '../node/setPadding'; -import { - setBorderTop, - setBorderRight, - setBorderBottom, - setBorderLeft, -} from '../node/setBorderWidth'; -import { - setPositionTop, - setPositionRight, - setPositionBottom, - setPositionLeft, -} from '../node/setPosition'; -import { - setWidth, - setHeight, - setMinWidth, - setMaxWidth, - setMinHeight, - setMaxHeight, -} from '../node/setDimension'; -import { setRowGap, setColumnGap } from '../node/setGap'; import measureSvg from '../svg/measureSvg'; import measureText from '../text/measureText'; +import { getLinesLayoutWidth } from '../text/layoutText'; import measureImage from '../image/measureImage'; import measureCanvas from '../canvas/measureCanvas'; +import { + createMeasureDispatch, + MeasureFunction, + toNodeMeasure, +} from '../taffy/measure'; import { Box, + SafeCanvasNode, SafeDocumentNode, + SafeImageNode, SafeNode, SafePageNode, - YogaInstance, + SafeSvgNode, + SafeTextNode, } from '../types'; const isType = (type) => (node) => node.type === type; @@ -70,226 +32,191 @@ const isType = (type) => (node) => node.type === type; const isSvg = isType(P.Svg); const isText = isType(P.Text); const isNote = isType(P.Note); -const isPage = isType(P.Page); const isImage = isType(P.Image); const isCanvas = isType(P.Canvas); const isTextInstance = isType(P.TextInstance); -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 node - */ -const setYogaValues = (node: SafeNode) => { - compose( - setNodeHeight(node), - setWidth(node.style.width), - setMinWidth(node.style.minWidth), - setMaxWidth(node.style.maxWidth), - setMinHeight(node.style.minHeight), - setMaxHeight(node.style.maxHeight), - setMarginTop(node.style.marginTop), - setMarginRight(node.style.marginRight), - setMarginBottom(node.style.marginBottom), - setMarginLeft(node.style.marginLeft), - setPaddingTop(node.style.paddingTop), - setPaddingRight(node.style.paddingRight), - setPaddingBottom(node.style.paddingBottom), - setPaddingLeft(node.style.paddingLeft), - setPositionType(node.style.position), - setPositionTop(node.style.top), - setPositionRight(node.style.right), - setPositionBottom(node.style.bottom), - setPositionLeft(node.style.left), - setBorderTop(node.style.borderTopWidth), - setBorderRight(node.style.borderRightWidth), - setBorderBottom(node.style.borderBottomWidth), - setBorderLeft(node.style.borderLeftWidth), - setDisplay(node.style.display), - setFlexDirection(node.style.flexDirection), - setAlignSelf(node.style.alignSelf), - setAlignContent(node.style.alignContent), - setAlignItems(node.style.alignItems), - setJustifyContent(node.style.justifyContent), - setFlexWrap(node.style.flexWrap), - setOverflow(node.style.overflow), - setAspectRatio(node.style.aspectRatio), - setFlexBasis(node.style.flexBasis), - setFlexGrow(node.style.flexGrow), - setFlexShrink(node.style.flexShrink), - setRowGap(node.style.rowGap), - setColumnGap(node.style.columnGap), - )(node); -}; +const MAX_CONTENT = { width: 'max-content', height: 'max-content' } as const; -/** - * Inserts child into parent' yoga node - * - * @param parent parent - * @returns Insert yoga nodes - */ -const insertYogaNodes = (parent) => (child) => { - parent.insertChild(child.yogaNode, parent.getChildCount()); - return child; -}; +const ALIGNMENT_FACTORS = { center: 0.5, right: 1 }; -const setMeasureFunc = (node, page, fontStore) => { - const { yogaNode } = node; - - if (isText(node)) { - yogaNode.setMeasureFunc(measureText(page, node, fontStore)); - } - - if (isImage(node)) { - yogaNode.setMeasureFunc(measureImage(page, node)); - } - - if (isCanvas(node)) { - yogaNode.setMeasureFunc(measureCanvas(page, node)); - } - - if (isSvg(node)) { - yogaNode.setMeasureFunc(measureSvg(page, node)); - } - - return node; -}; - -const isLayoutElement = (node) => +const isLayoutElement = (node: SafeNode) => !isText(node) && !isNote(node) && !isSvg(node); -/** - * @typedef {Function} CreateYogaNodes - * @param {Object} node - * @returns {Object} node with appended yoga node - */ +const getMeasureFunc = ( + node: SafeNode, + page: SafePageNode, + fontStore: FontStore, +): MeasureFunction | undefined => { + if (isText(node)) return measureText(page, node as SafeTextNode, fontStore); + if (isImage(node)) return measureImage(page, node as SafeImageNode); + if (isCanvas(node)) return measureCanvas(page, node as SafeCanvasNode); + if (isSvg(node)) return measureSvg(page, node as SafeSvgNode); + return undefined; +}; /** - * Creates and add yoga node to document tree - * Handles measure function for text and image nodes - * - * @returns Create yoga nodes + * Creates a Taffy node for every layout node of the page subtree, storing + * the id on `taffyNode`. Text, image, canvas and svg nodes carry their + * measure function as node context. */ -const createYogaNodes = - (page: SafePageNode, fontStore: FontStore, yoga: YogaInstance) => - (node: SafeNode) => { - const yogaNode = yoga.node.create(); +const createTaffyNodes = + (tree: TaffyTree, page: SafePageNode, fontStore: FontStore) => + (node: SafeNode): SafeNode => { + const result: SafeNode = Object.assign({}, node); + const style = new Style(toTaffyStyle(result)); + const measure = getMeasureFunc(result, page, fontStore); - const result = Object.assign({}, node, { yogaNode }); + const taffyNode = measure + ? tree.newLeafWithContext(style, toNodeMeasure(measure)) + : tree.newLeaf(style); - setYogaValues(result); + style.free(); + result.taffyNode = taffyNode; if (isLayoutElement(node) && node.children) { - const resolveChild = compose( - insertYogaNodes(yogaNode), - createYogaNodes(page, fontStore, yoga), - ); + const create = createTaffyNodes(tree, page, fontStore); - result.children = node.children.map(resolveChild); + result.children = (node.children as SafeNode[]).map((child) => { + const resolved = create(child); + tree.addChild(taffyNode, resolved.taffyNode!); + return resolved; + }) as any; } - setMeasureFunc(result, page, fontStore); - return result; }; /** - * Performs yoga calculation - * - * @param page - Page node - * @returns Page node + * Text lines are broken against the width Taffy offered while measuring, + * but the node's box may end up narrower (shrink-wrapped). textkit already + * aligned the lines inside the wider container, so the renderer shifts them + * back by the difference. */ -const calculateLayout = (page: SafePageNode) => { - page.yogaNode.calculateLayout(); - return page; +const getAlignOffset = (node: SafeTextNode, box: Box) => { + if (!node.lines?.length || node.exclusions?.length) return 0; + + const factor = ALIGNMENT_FACTORS[node.style?.textAlign] || 0; + if (!factor) return 0; + + const layoutWidth = getLinesLayoutWidth(node.lines); + if (layoutWidth === undefined) return 0; + + const contentWidth = box.width - box.paddingLeft - box.paddingRight; + + return Math.max(0, layoutWidth - contentWidth) * factor; }; /** - * Saves Yoga layout result into 'box' attribute of node + * Saves Taffy layout result into 'box' attribute of node and drops the + * Taffy node id. * - * @param node + * @param tree - Taffy tree + * @param node - Node + * @param parent - Parent box size, for the right/bottom offsets * @returns Node with box data */ -const persistDimensions = (node: SafeNode) => { +const persistDimensions = ( + tree: TaffyTree, + node: SafeNode, + parent?: { width: number; height: number }, +): SafeNode => { if (isTextInstance(node)) return node; - const box: Box = Object.assign( - getPadding(node), - getMargin(node), - getBorderWidth(node), - getPosition(node), - getDimension(node), - ); + // Descendants of text and svg nodes have no layout node of their own + if (isNil(node.taffyNode)) { + const box = Object.assign( + { width: 0, height: 0, top: 0, left: 0, right: 0, bottom: 0 }, + getPadding(node), + getMargin(node), + ) as Box; + const newNode: SafeNode = Object.assign({}, node, { box }); - const newNode = Object.assign({}, node, { box }); + if (!node.children) return newNode; - if (!node.children) return newNode; + const children = node.children.map((child) => + persistDimensions(tree, child), + ); - const children = node.children.map(persistDimensions); + return Object.assign({}, newNode, { children }); + } - return Object.assign({}, newNode, { children }); -}; + const layout = tree.getLayout(node.taffyNode); + + const box: Box = { + width: layout.width, + height: layout.height, + top: layout.y, + left: layout.x, + right: parent ? parent.width - layout.x - layout.width : 0, + bottom: parent ? parent.height - layout.y - layout.height : 0, + marginTop: layout.marginTop, + marginRight: layout.marginRight, + marginBottom: layout.marginBottom, + marginLeft: layout.marginLeft, + paddingTop: layout.paddingTop, + paddingRight: layout.paddingRight, + paddingBottom: layout.paddingBottom, + paddingLeft: layout.paddingLeft, + borderTopWidth: layout.borderTop, + borderRightWidth: layout.borderRight, + borderBottomWidth: layout.borderBottom, + borderLeftWidth: layout.borderLeft, + }; -/** - * Removes yoga node from document tree - * - * @param node - * @returns Node without yoga node - */ -const destroyYogaNodes = (node: SafeNode): SafeNode => { - const newNode = Object.assign({}, node); + layout.free(); - delete newNode.yogaNode; + const newNode: SafeNode = Object.assign({}, node, { box }); + delete newNode.taffyNode; + + if (isText(newNode)) { + (newNode as SafeTextNode).alignOffset = getAlignOffset( + newNode as SafeTextNode, + box, + ); + } if (!node.children) return newNode; - const children = node.children.map(destroyYogaNodes); + const children = node.children.map((child) => + persistDimensions(tree, child, box), + ); return Object.assign({}, newNode, { children }); }; /** - * Free yoga node from document tree - * - * @param node - * @returns Node without yoga node - */ -const freeYogaNodes = (node: SafeNode) => { - if (node.yogaNode) node.yogaNode.freeRecursive(); - return node; -}; - -/** - * Calculates page object layout using Yoga. + * Calculates page object layout using Taffy. * Takes node values from 'box' and 'style' attributes, and persist them back into 'box' - * Destroy yoga values at the end. * * @param page - Object + * @param fontStore - Font store * @returns Page object with correct 'box' layout attributes */ export const resolvePageDimensions = ( page: SafePageNode, fontStore: FontStore, - yoga: YogaInstance, ) => { if (isNil(page)) return null; - return compose( - destroyYogaNodes, - freeYogaNodes, - persistDimensions, - calculateLayout, - createYogaNodes(page, fontStore, yoga), - )(page); + const tree = new TaffyTree(); + tree.disableRounding(); + + try { + const withNodes = createTaffyNodes(tree, page, fontStore)(page); + const { dispatch, rethrow } = createMeasureDispatch(); + + tree.computeLayoutWithMeasure(withNodes.taffyNode!, MAX_CONTENT, dispatch); + rethrow(); + + return persistDimensions(tree, withNodes) as SafePageNode; + } finally { + tree.free(); + } }; /** - * Calculates root object layout using Yoga. + * Calculates root object layout using Taffy. * * @param node - Root object * @param fontStore - Font store @@ -299,7 +226,7 @@ const resolveDimensions = (node: SafeDocumentNode, fontStore: FontStore) => { if (!node.children) return node; const resolveChild = (child: SafePageNode) => - resolvePageDimensions(child, fontStore, node.yoga); + resolvePageDimensions(child, fontStore); const children = node.children.map(resolveChild); diff --git a/packages/layout/src/steps/resolveFloats.ts b/packages/layout/src/steps/resolveFloats.ts index 4982f1a74..c548b3c3b 100644 --- a/packages/layout/src/steps/resolveFloats.ts +++ b/packages/layout/src/steps/resolveFloats.ts @@ -76,7 +76,7 @@ const getFloatLeft = (node: SafeNode, parentWidth: number): number => { /** * Position float element to the left or right edge of parent. - * Note: Yoga already applies marginTop to box.top for absolute positioned elements. + * Note: The layout engine already applies marginTop to box.top for absolute positioned elements. * Callers guarantee node.box — resolveFloats skips boxless children. */ const positionFloatElement = ( @@ -290,7 +290,7 @@ const resolveFloats = (node: T): T => { children.push(resolveFloats(processedChild)); } - // Clearance moved in-flow children down after yoga ran; grow the container + // Clearance moved in-flow children down after layout ran; grow the container // to keep containing them, like CSS clearance does. const box = clearOffset > 0 && node.box diff --git a/packages/layout/src/steps/resolvePagePaddings.ts b/packages/layout/src/steps/resolvePagePaddings.ts index 1d7f78a05..47a4ab28a 100644 --- a/packages/layout/src/steps/resolvePagePaddings.ts +++ b/packages/layout/src/steps/resolvePagePaddings.ts @@ -53,7 +53,7 @@ const resolvePagePaddings = (page: SafePageNode): SafePageNode => { /** * Translates all pages percentage paddings in fixed ones - * This has to be computed from pages calculated size and not by Yoga + * This has to be computed from pages calculated size and not by the layout engine * because at this point we didn't performed pagination yet. * * @param root - Document root diff --git a/packages/layout/src/steps/resolvePageTemplates.ts b/packages/layout/src/steps/resolvePageTemplates.ts index ac612f12e..4bb0d7ebf 100644 --- a/packages/layout/src/steps/resolvePageTemplates.ts +++ b/packages/layout/src/steps/resolvePageTemplates.ts @@ -35,8 +35,8 @@ const resolvePageTemplates = (root: any) => { // TODO: content identity could be structural instead of tag-based — a // single P.Fragment node holding the page content, made transparent to - // layout (display: contents semantics: no yoga node, no box, children - // hoisted to the parent). Requires teaching yoga mapping, resolvers, + // layout (display: contents semantics: no layout node, no box, children + // hoisted to the parent). Requires teaching the style mapping, resolvers, // fromPage and render about a boxless node. Worth it if content ever // needs richer identity (marks, named regions, TOC anchors). const content = (page.children || []).map(tagContent); diff --git a/packages/layout/src/steps/resolvePagination.ts b/packages/layout/src/steps/resolvePagination.ts index 9e679ecc6..6900624bf 100644 --- a/packages/layout/src/steps/resolvePagination.ts +++ b/packages/layout/src/steps/resolvePagination.ts @@ -18,7 +18,6 @@ import { SafePageNode, SafeTextNode, SafeViewNode, - YogaInstance, } from '../types'; const isText = (node: SafeNode): node is SafeTextNode => node.type === P.Text; @@ -191,11 +190,10 @@ const resolveDynamicPage = ( props: DynamicPageProps, page: SafePageNode, fontStore: FontStore, - yoga: YogaInstance, ) => { if (shouldResolveDynamicNodes(page)) { const resolvedPage = resolveDynamicNodes(props, page); - return relayoutPage(resolvedPage, fontStore, yoga); + return relayoutPage(resolvedPage, fontStore); } return page; @@ -205,11 +203,10 @@ 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); + const dynamicPage = resolveDynamicPage({ pageNumber }, page, fontStore); const height = page.style.height; const [currentChilds, nextChilds] = splitNodes( @@ -220,7 +217,7 @@ const splitPage = ( const relayout = (node: SafePageNode): SafePageNode => // @ts-expect-error rework pagination - relayoutPage(node, fontStore, yoga) as SafePageNode; + relayoutPage(node, fontStore) as SafePageNode; const currentBox = { ...page.box, height }; const currentPage = relayout( @@ -244,7 +241,7 @@ const splitPage = ( return [currentPage, nextPage]; }; -const resolvePageIndices = (fontStore, yoga, page, pageNumber, pages) => { +const resolvePageIndices = (fontStore, page, pageNumber, pages) => { const totalPages = pages.length; const props = { @@ -254,7 +251,7 @@ const resolvePageIndices = (fontStore, yoga, page, pageNumber, pages) => { subPageTotalPages: page.subPageTotalPages, }; - return resolveDynamicPage(props, page, fontStore, yoga); + return resolveDynamicPage(props, page, fontStore); }; const assocSubPageData = (subpages) => { @@ -273,24 +270,18 @@ const paginate = ( page: SafePageNode, pageNumber: number, fontStore: FontStore, - yoga: YogaInstance, ) => { if (!page) return []; if (page.props?.wrap === false) return [page]; - let splittedPage = splitPage(page, pageNumber, fontStore, yoga); + let splittedPage = splitPage(page, pageNumber, fontStore); const pages = [splittedPage[0]]; let nextPage = splittedPage[1]; while (nextPage !== null) { - splittedPage = splitPage( - nextPage, - pageNumber + pages.length, - fontStore, - yoga, - ); + splittedPage = splitPage(nextPage, pageNumber + pages.length, fontStore); pages.push(splittedPage[0]); nextPage = splittedPage[1]; @@ -316,7 +307,7 @@ const resolvePagination = ( for (let i = 0; i < root.children.length; i += 1) { const page = root.children[i]; - let subpages = paginate(page, pageNumber, fontStore, root.yoga); + let subpages = paginate(page, pageNumber, fontStore); subpages = assocSubPageData(subpages); pageNumber += subpages.length; @@ -324,7 +315,7 @@ const resolvePagination = ( } pages = pages.map((...args) => - dissocSubPageData(resolvePageIndices(fontStore, root.yoga, ...args)), + dissocSubPageData(resolvePageIndices(fontStore, ...args)), ); return assingChildren(pages, root); diff --git a/packages/layout/src/steps/resolveStyles.ts b/packages/layout/src/steps/resolveStyles.ts index 102d4840e..7fbf5beef 100644 --- a/packages/layout/src/steps/resolveStyles.ts +++ b/packages/layout/src/steps/resolveStyles.ts @@ -30,7 +30,7 @@ const computeStyle = (container: Container, node: Node) => { const style = stylesheet(container, baseStyle); - // Floats are out of flow; flipping to absolute here keeps yoga and pagination float-unaware + // Floats are out of flow; flipping to absolute here keeps layout and pagination float-unaware if (style.float === 'left' || style.float === 'right') { style.position = 'absolute'; } @@ -47,18 +47,33 @@ const computeStyle = (container: Container, node: Node) => { const resolveNodeStyles = (container: Container) => (node: Node): SafeNode => { - const style = computeStyle(container, node); + // Page relayout runs this step again over already resolved nodes (only + // freshly rendered dynamic nodes are new). Resolving twice is not safe: + // a unitless lineHeight is multiplied by the font size on every pass. + const stylesResolved = (node as SafeNode).stylesResolved ?? false; + const style = stylesResolved + ? (node.style as SafeNode['style']) + : computeStyle(container, node); // Split fragments re-enter through page relayout; keep their mark. const wasSplit = (node as SafeNode).wasSplit ?? false; if (!node.children) { - return Object.assign({}, node, { style, wasSplit }) as SafeNode; + return Object.assign({}, node, { + style, + wasSplit, + stylesResolved: true, + }) as SafeNode; } const children = node.children.map(resolveNodeStyles(container)); - return Object.assign({}, node, { style, wasSplit, children }) as SafeNode; + return Object.assign({}, node, { + style, + wasSplit, + stylesResolved: true, + children, + }) as SafeNode; }; /** diff --git a/packages/layout/src/steps/resolveSvg.ts b/packages/layout/src/steps/resolveSvg.ts index 89e7cdd62..c576bbc25 100644 --- a/packages/layout/src/steps/resolveSvg.ts +++ b/packages/layout/src/steps/resolveSvg.ts @@ -432,7 +432,6 @@ function convertToSvgNode(imageNode: SafeImageNode): SafeSvgNode { style: { ...imageNode.style, width, height }, box: imageNode.box, origin: imageNode.origin, - yogaNode: imageNode.yogaNode, wasSplit: false, children: image.data.children.map(convertParsedNode), }; diff --git a/packages/layout/src/steps/resolveTaffy.ts b/packages/layout/src/steps/resolveTaffy.ts new file mode 100644 index 000000000..9c19243b1 --- /dev/null +++ b/packages/layout/src/steps/resolveTaffy.ts @@ -0,0 +1,14 @@ +import { loadTaffy } from '../taffy/index'; +import { DocumentNode } from '../types'; + +/** + * Make sure the Taffy WASM module is loaded before layout starts. + * Runs first in the layout pipeline. + */ +const resolveTaffy = async (root: DocumentNode): Promise => { + await loadTaffy(); + + return root; +}; + +export default resolveTaffy; diff --git a/packages/layout/src/steps/resolveTextLayout.ts b/packages/layout/src/steps/resolveTextLayout.ts index 4b0860192..1d1d0422f 100644 --- a/packages/layout/src/steps/resolveTextLayout.ts +++ b/packages/layout/src/steps/resolveTextLayout.ts @@ -19,7 +19,7 @@ const shouldLayoutText = (node: SafeNode): node is SafeTextNode => /** * Performs text layout on text node if wasn't calculated before. - * Text layout is usually performed on Yoga's layout process (via setMeasureFunc), + * Text layout is usually performed during Taffy's layout process (via the measure function), * but we need to layout those nodes with fixed width and height. * * @param node diff --git a/packages/layout/src/steps/resolveYoga.ts b/packages/layout/src/steps/resolveYoga.ts deleted file mode 100644 index 23ded11fd..000000000 --- a/packages/layout/src/steps/resolveYoga.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { loadYoga } from '../yoga/index'; -import { DocumentNode } from '../types'; - -const resolveYoga = async (root: DocumentNode): Promise => { - const yoga = await loadYoga(); - - return Object.assign({}, root, { yoga }); -}; - -export default resolveYoga; diff --git a/packages/layout/src/svg/measureSvg.ts b/packages/layout/src/svg/measureSvg.ts index 4c9794588..e0c3c5fc2 100644 --- a/packages/layout/src/svg/measureSvg.ts +++ b/packages/layout/src/svg/measureSvg.ts @@ -1,4 +1,4 @@ -import * as Yoga from 'yoga-layout/load'; +import { MeasureFunction, MeasureMode } from '../taffy/measure'; import { SafePageNode, SafeSvgNode, Viewbox } from '../types'; @@ -10,28 +10,37 @@ const getAspectRatio = (viewbox: string | Viewbox) => { }; /** - * Yoga svg measure function + * Svg measure function * * @param page * @param node * @returns Measure svg */ const measureCanvas = - (page: SafePageNode, node: SafeSvgNode): Yoga.MeasureFunction => + (page: SafePageNode, node: SafeSvgNode): MeasureFunction => (width, widthMode, height, heightMode) => { const aspectRatio = getAspectRatio(node.props.viewBox) || 1; - if ( - widthMode === Yoga.MeasureMode.Exactly || - widthMode === Yoga.MeasureMode.AtMost - ) { + if (widthMode === MeasureMode.Exactly || widthMode === MeasureMode.AtMost) { return { width, height: width / aspectRatio }; } - if (heightMode === Yoga.MeasureMode.Exactly) { + if (heightMode === MeasureMode.Exactly) { return { width: height * aspectRatio }; } + // Unconstrained width (max-content): the viewBox is the intrinsic size + const viewBox = node.props.viewBox; + if ( + widthMode === MeasureMode.Undefined && + width !== 0 && + viewBox && + typeof viewBox !== 'string' + ) { + const intrinsicWidth = viewBox.maxX - viewBox.minX; + return { width: intrinsicWidth, height: intrinsicWidth / aspectRatio }; + } + return {}; }; diff --git a/packages/layout/src/taffy/index.ts b/packages/layout/src/taffy/index.ts new file mode 100644 index 000000000..8de6dfefb --- /dev/null +++ b/packages/layout/src/taffy/index.ts @@ -0,0 +1,11 @@ +import { loadTaffy as taffyLoad } from 'taffy-layout'; + +let instancePromise: Promise | undefined; + +/** + * Compile and instantiate the Taffy WASM module once per process. + * After this resolves, `TaffyTree` and `Style` from `taffy-layout` are usable. + */ +export const loadTaffy = async (): Promise => { + await (instancePromise ??= taffyLoad()); +}; diff --git a/packages/layout/src/taffy/measure.ts b/packages/layout/src/taffy/measure.ts new file mode 100644 index 000000000..79bde1d3d --- /dev/null +++ b/packages/layout/src/taffy/measure.ts @@ -0,0 +1,93 @@ +import type { + AvailableSpace, + MeasureFunction as TaffyMeasureFunction, + Size, +} from 'taffy-layout'; + +/** + * Sizing constraint for one axis, in the vocabulary the measure functions + * were written against. + */ +export enum MeasureMode { + /** No constraint: `width` is `Infinity` (max-content) or `0` (min-content) */ + Undefined = 0, + /** The axis is already decided; `width` is its final size */ + Exactly = 1, + /** Free to shrink-wrap up to `width` */ + AtMost = 2, +} + +export type MeasureFunction = ( + width: number, + widthMode: MeasureMode, + height: number, + heightMode: MeasureMode, +) => { width?: number; height?: number }; + +/** Content measure callback as stored in a Taffy node's context */ +export type NodeMeasure = ( + known: Size, + available: Size, +) => Size; + +const constraint = ( + known: number | undefined, + available: AvailableSpace, +): [number, MeasureMode] => { + if (known !== undefined) return [known, MeasureMode.Exactly]; + if (typeof available === 'number') return [available, MeasureMode.AtMost]; + return [available === 'min-content' ? 0 : Infinity, MeasureMode.Undefined]; +}; + +const finite = (value?: number) => (Number.isFinite(value) ? value! : 0); + +/** + * Adapt a measure function to Taffy's `(knownDimensions, availableSpace)` + * calling convention. + */ +export const toNodeMeasure = + (measure: MeasureFunction): NodeMeasure => + (known, available) => { + const [width, widthMode] = constraint(known.width, available.width); + const [height, heightMode] = constraint(known.height, available.height); + const size = measure(width, widthMode, height, heightMode); + + return { width: finite(size.width), height: finite(size.height) }; + }; + +const ZERO: Size = { width: 0, height: 0 }; + +/** + * Build the single measure callback Taffy expects for a `computeLayout` + * call. Each leaf carries its own `NodeMeasure` as node context. Errors + * thrown by a measure function are captured and rethrown by `rethrow`, + * because the binding would otherwise swallow them as a zero size. + */ +export const createMeasureDispatch = () => { + let error: unknown; + + const dispatch: TaffyMeasureFunction = ( + known, + available, + _node, + context, + style, + ) => { + style.free(); + + if (error || typeof context !== 'function') return ZERO; + + try { + return (context as NodeMeasure)(known, available); + } catch (e) { + error = e; + return ZERO; + } + }; + + const rethrow = () => { + if (error) throw error; + }; + + return { dispatch, rethrow }; +}; diff --git a/packages/layout/src/text/layoutText.ts b/packages/layout/src/text/layoutText.ts index 816f82da1..d02e5c4f8 100644 --- a/packages/layout/src/text/layoutText.ts +++ b/packages/layout/src/text/layoutText.ts @@ -25,6 +25,20 @@ const engines = { const engine = layoutEngine(engines); +// Container width each line was broken against. Keyed by line object so it +// survives `splitText` slicing the lines array; textkit's `line.box.width` +// cannot be used because it grows with trailing whitespace. +const layoutWidths = new WeakMap(); + +/** + * Width the given lines were laid out against, if `layoutText` produced them + * + * @param lines - Lines + * @returns Container width, or undefined if unknown + */ +export const getLinesLayoutWidth = (lines?: object[]) => + lines?.length ? layoutWidths.get(lines[0]) : undefined; + const getMaxLines = (node) => node.style?.maxLines; const getTextOverflow = (node) => node.style?.textOverflow; @@ -113,8 +127,11 @@ const layoutText = ( const container = getContainer(width, height, node); const options = getLayoutOptions(fontStore, node); const lines = engine(attributedString, container, options); + const flat = lines.reduce((acc, line) => [...acc, ...line], []); + + flat.forEach((line) => layoutWidths.set(line, width)); - return lines.reduce((acc, line) => [...acc, ...line], []); + return flat; }; export default layoutText; diff --git a/packages/layout/src/text/measureText.ts b/packages/layout/src/text/measureText.ts index ec808c9cf..48c858756 100644 --- a/packages/layout/src/text/measureText.ts +++ b/packages/layout/src/text/measureText.ts @@ -1,49 +1,122 @@ -import * as Yoga from 'yoga-layout/load'; import FontStore from '@react-pdf/font'; -import layoutText from './layoutText'; +import layoutText, { getLinesLayoutWidth } from './layoutText'; import linesWidth from './linesWidth'; import linesHeight from './linesHeight'; +import { MeasureFunction, MeasureMode } from '../taffy/measure'; import { SafePageNode, SafeTextNode } from '../types'; -const ALIGNMENT_FACTORS = { center: 0.5, right: 1 }; +// Wide enough that any text lays out on a single line, finite so textkit's +// alignment arithmetic stays finite. +const MAX_CONTENT_WIDTH = 1e6; + +// Taffy computes in f32, so sizes come back a few ulps off the f64 values +// textkit produced; a hundredth of a point is far below anything visible. +const EPSILON = 0.01; + +// A box that ends up a hair narrower than the text it holds (flex shrink +// resolving in f32) keeps its lines rather than re-breaking a line that +// visually fits; this much overflow is invisible. +const OVERFLOW_TOLERANCE = 1; + +/** + * Whether the lines laid out so far can be reused for a container of + * `width`: they were broken against this very width, or against a wider + * container and still fit. Re-breaking text at exactly its own width is not + * stable (Knuth-Plass), so fitting lines are preferred over a fresh layout. + * Lines broken against a narrower container are always re-broken: Taffy + * measures flex items at intermediate (even zero) sizes before their final + * one. Lines of unknown origin are trusted. + */ +const fits = (node: SafeTextNode, width: number) => { + if (!node.lines) return false; + + // A page-split fragment owns a slice of the lines but still all of the + // text children; laying it out again would resurrect the whole text. + if (node.wasSplit) return true; + + // An intermediate measure at a tiny size can truncate every line away + if (!node.lines.length) return false; + + const laidOutAt = getLinesLayoutWidth(node.lines); + + if (laidOutAt === undefined) return true; + if (Math.abs(laidOutAt - width) <= EPSILON) return true; + + return laidOutAt >= width && linesWidth(node) <= width + OVERFLOW_TOLERANCE; +}; /** - * Yoga text measure function + * Lines laid out against an unconstrained width can only be kept for + * left-aligned text: any other alignment positions glyphs relative to the + * (huge) container width. + */ +const isLeftAligned = (node: SafeTextNode) => { + const align = + node.style?.textAlign || + (node.style?.direction === 'rtl' ? 'right' : 'left'); + return align === 'left'; +}; + +/** + * Text measure function. Lays out lines against the given constraint and + * caches them on the node, so text is broken as few times as possible. + * Whatever lines the node ends up with are what gets rendered; the box may + * be narrower than the container the lines were broken against, which + * `resolveDimensions` compensates through `alignOffset`. * * @param page * @param node * @param fontStore - * @returns {MeasureText} measure text function + * @returns measure text function */ const measureText = ( page: SafePageNode, node: SafeTextNode, fontStore: FontStore, - ): Yoga.MeasureFunction => + ): MeasureFunction => (width, widthMode, height) => { - if (widthMode === Yoga.MeasureMode.Exactly) { - if (!node.lines) node.lines = layoutText(node, width, height, fontStore); - - return { height: linesHeight(node), width }; - } + if (widthMode === MeasureMode.Undefined) { + // max-content (width Infinity) or min-content (width 0) + if (node.lines) { + return { width: linesWidth(node), height: linesHeight(node) }; + } - if (widthMode === Yoga.MeasureMode.AtMost) { - const alignFactor = ALIGNMENT_FACTORS[node.style?.textAlign] || 0; + const containerWidth = width === Infinity ? MAX_CONTENT_WIDTH : 0; + const lines = layoutText(node, containerWidth, height, fontStore); + const measured = { lines } as SafeTextNode; - if (!node.lines) { - node.lines = layoutText(node, width, height, fontStore); - node.alignOffset = (width - linesWidth(node)) * alignFactor; // Compensate align in variable width containers + if (containerWidth === MAX_CONTENT_WIDTH && isLeftAligned(node)) { + node.lines = lines; } - return { - height: linesHeight(node), - width: Math.min(width, linesWidth(node)), - }; + return { width: linesWidth(measured), height: linesHeight(measured) }; + } + + // Taffy sizes flex-basis: 0 items at a zero main size before their final + // one; breaking the text word by word there is expensive and never shown. + if (width <= 0) return { width: 0, height: 0 }; + + if (!fits(node, width)) { + // Sizes arrive f32-rounded from Taffy; the tolerance keeps a line that + // measured as fitting from being broken again, or truncated away, for + // a rounding error. + node.lines = layoutText( + node, + width + EPSILON, + height + EPSILON, + fontStore, + ); } - return {}; + return { + height: linesHeight(node), + width: + widthMode === MeasureMode.Exactly + ? width + : Math.min(width, linesWidth(node)), + }; }; export default measureText; diff --git a/packages/layout/src/types/base.ts b/packages/layout/src/types/base.ts index 3fd49cb2c..bcfb28b25 100644 --- a/packages/layout/src/types/base.ts +++ b/packages/layout/src/types/base.ts @@ -1,15 +1,10 @@ import { Transform } from '@react-pdf/stylesheet'; -import { YogaNode } from 'yoga-layout/load'; import type { SafeNode } from './node'; import { SafeClipPathNode } from './clip-path'; import { SafeLinearGradientNode } from './linear-gradient'; import { SafeMarkerNode } from './marker'; import { SafeRadialGradientNode } from './radial-gradient'; -export type YogaInstance = { - node: { create: () => YogaNode }; -}; - export type Box = { width: number; height: number; diff --git a/packages/layout/src/types/canvas.ts b/packages/layout/src/types/canvas.ts index aaeeddcf0..5950289e4 100644 --- a/packages/layout/src/types/canvas.ts +++ b/packages/layout/src/types/canvas.ts @@ -1,8 +1,6 @@ import * as P from '@react-pdf/primitives'; import { SafeStyle, StyleProp } from '@react-pdf/stylesheet'; -import { YogaNode } from 'yoga-layout/load'; - import { Box, NodeProps, Origin } from './base'; interface CanvasProps extends NodeProps { @@ -19,11 +17,12 @@ export type CanvasNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: never[]; }; export type SafeCanvasNode = Omit & { style: SafeStyle; wasSplit: boolean; + stylesResolved?: boolean; }; diff --git a/packages/layout/src/types/checkbox.ts b/packages/layout/src/types/checkbox.ts index 37dbf2fc0..8b3528b43 100644 --- a/packages/layout/src/types/checkbox.ts +++ b/packages/layout/src/types/checkbox.ts @@ -1,6 +1,5 @@ import * as P from '@react-pdf/primitives'; import { SafeStyle, StyleProp } from '@react-pdf/stylesheet'; -import { YogaNode } from 'yoga-layout/load'; import { Box, FormCommonProps, Origin } from './base'; @@ -19,11 +18,12 @@ export type CheckboxNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: never[]; }; export type SafeCheckboxNode = Omit & { style: SafeStyle; wasSplit: boolean; + stylesResolved?: boolean; }; diff --git a/packages/layout/src/types/circle.ts b/packages/layout/src/types/circle.ts index cc390991b..d9ac418fa 100644 --- a/packages/layout/src/types/circle.ts +++ b/packages/layout/src/types/circle.ts @@ -27,7 +27,7 @@ export type CircleNode = { style?: StyleProp; box?: never; origin?: Origin; - yogaNode?: never; + taffyNode?: never; children?: never[]; }; @@ -35,4 +35,5 @@ export type SafeCircleNode = Omit & { style: SafeStyle; props: SafeCircleProps; wasSplit: boolean; + stylesResolved?: boolean; }; diff --git a/packages/layout/src/types/clip-path.ts b/packages/layout/src/types/clip-path.ts index defe891c0..8059f5943 100644 --- a/packages/layout/src/types/clip-path.ts +++ b/packages/layout/src/types/clip-path.ts @@ -18,7 +18,7 @@ export type ClipPathNode = { style: never; box?: never; origin?: never; - yogaNode?: never; + taffyNode?: never; children?: ( | LineNode | PolylineNode @@ -32,6 +32,7 @@ export type ClipPathNode = { export type SafeClipPathNode = Omit & { wasSplit: boolean; + stylesResolved?: boolean; children?: ( | SafeLineNode | SafePolylineNode diff --git a/packages/layout/src/types/defs.ts b/packages/layout/src/types/defs.ts index f6637e0fc..b93432c76 100644 --- a/packages/layout/src/types/defs.ts +++ b/packages/layout/src/types/defs.ts @@ -10,7 +10,7 @@ export type DefsNode = { style?: never; box?: never; origin?: never; - yogaNode?: never; + taffyNode?: never; children?: ( | ClipPathNode | LinearGradientNode @@ -23,6 +23,7 @@ export type Defs = Record; export type SafeDefsNode = Omit & { wasSplit: boolean; + stylesResolved?: boolean; children?: ( | SafeClipPathNode | SafeLinearGradientNode diff --git a/packages/layout/src/types/document.ts b/packages/layout/src/types/document.ts index aef6cb8c6..e505dd284 100644 --- a/packages/layout/src/types/document.ts +++ b/packages/layout/src/types/document.ts @@ -1,7 +1,6 @@ import * as P from '@react-pdf/primitives'; import { PageNode, SafePageNode } from './page'; -import { YogaInstance } from './base'; import { SafeStyle, StyleProp } from '@react-pdf/stylesheet'; export type PDFVersion = '1.3' | '1.4' | '1.5' | '1.6' | '1.7' | '1.7ext3'; @@ -71,13 +70,13 @@ export type DocumentNode = { box?: never; origin?: never; style?: StyleProp; - yoga?: YogaInstance; - yogaNode?: never; + taffyNode?: never; children: PageNode[]; }; export type SafeDocumentNode = Omit & { style: SafeStyle; wasSplit: boolean; + stylesResolved?: boolean; children: SafePageNode[]; }; diff --git a/packages/layout/src/types/ellipse.ts b/packages/layout/src/types/ellipse.ts index 0b69e9861..14d41744d 100644 --- a/packages/layout/src/types/ellipse.ts +++ b/packages/layout/src/types/ellipse.ts @@ -28,7 +28,7 @@ export type EllipseNode = { style?: StyleProp; box?: never; origin?: never; - yogaNode?: never; + taffyNode?: never; children?: never[]; }; @@ -36,4 +36,5 @@ export type SafeEllipseNode = Omit & { style: SafeStyle; props: SafeEllipseProps; wasSplit: boolean; + stylesResolved?: boolean; }; diff --git a/packages/layout/src/types/field-set.ts b/packages/layout/src/types/field-set.ts index 302fd7616..70989027d 100644 --- a/packages/layout/src/types/field-set.ts +++ b/packages/layout/src/types/field-set.ts @@ -1,6 +1,5 @@ import * as P from '@react-pdf/primitives'; import { SafeStyle, StyleProp } from '@react-pdf/stylesheet'; -import { YogaNode } from 'yoga-layout/load'; import { Box, NodeProps, Origin } from './base'; import { SafeTextNode, TextNode } from './text'; @@ -17,12 +16,13 @@ export type FieldSetNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: (TextNode | ViewNode | TextInputNode)[]; }; export type SafeFieldSetNode = Omit & { style: SafeStyle; wasSplit: boolean; + stylesResolved?: boolean; children?: (SafeTextNode | SafeViewNode | SafeTextInputNode)[]; }; diff --git a/packages/layout/src/types/g.ts b/packages/layout/src/types/g.ts index f3c06192e..1ab586099 100644 --- a/packages/layout/src/types/g.ts +++ b/packages/layout/src/types/g.ts @@ -30,7 +30,7 @@ export type GNode = { style?: StyleProp; box?: never; origin?: never; - yogaNode?: never; + taffyNode?: never; children?: ( | LineNode | PolylineNode @@ -50,6 +50,7 @@ export type SafeGNode = Omit & { style: SafeStyle; props: SafeGProps; wasSplit: boolean; + stylesResolved?: boolean; children?: ( | SafeLineNode | SafePolylineNode diff --git a/packages/layout/src/types/image-background.ts b/packages/layout/src/types/image-background.ts index b5267858b..5318e50ec 100644 --- a/packages/layout/src/types/image-background.ts +++ b/packages/layout/src/types/image-background.ts @@ -1,7 +1,6 @@ import { SafeStyle, Style } from '@react-pdf/stylesheet'; import { SrcSet, Sizes } from '@react-pdf/types'; 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'; @@ -46,7 +45,7 @@ export type ImageBackgroundNode = { style?: Style | Style[]; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: ( | ViewNode | ImageNode @@ -68,6 +67,7 @@ export type SafeImageBackgroundNode = Omit< > & { style: SafeStyle; wasSplit: boolean; + stylesResolved?: boolean; children?: ( | SafeViewNode | SafeImageNode diff --git a/packages/layout/src/types/image.ts b/packages/layout/src/types/image.ts index 86c329988..23c5bf9a1 100644 --- a/packages/layout/src/types/image.ts +++ b/packages/layout/src/types/image.ts @@ -2,7 +2,6 @@ import { SafeStyle, StyleProp } from '@react-pdf/stylesheet'; import { SrcSet, Sizes } from '@react-pdf/types'; import * as P from '@react-pdf/primitives'; import { Image } from '@react-pdf/image'; -import { YogaNode } from 'yoga-layout/load'; import { Box, NodeProps, Origin } from './base'; @@ -71,11 +70,12 @@ export type ImageNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: never[]; }; export type SafeImageNode = Omit & { style: SafeStyle; wasSplit: boolean; + stylesResolved?: boolean; }; diff --git a/packages/layout/src/types/line.ts b/packages/layout/src/types/line.ts index 200126ce1..cf3fb0221 100644 --- a/packages/layout/src/types/line.ts +++ b/packages/layout/src/types/line.ts @@ -28,7 +28,7 @@ export type LineNode = { style?: StyleProp; box?: never; origin?: never; - yogaNode?: never; + taffyNode?: never; children?: never[]; }; @@ -36,4 +36,5 @@ export type SafeLineNode = Omit & { style: SafeStyle; props: SafeLineProps; wasSplit: boolean; + stylesResolved?: boolean; }; diff --git a/packages/layout/src/types/linear-gradient.ts b/packages/layout/src/types/linear-gradient.ts index 58a78add7..1a74bdb8a 100644 --- a/packages/layout/src/types/linear-gradient.ts +++ b/packages/layout/src/types/linear-gradient.ts @@ -30,7 +30,7 @@ export type LinearGradientNode = { style?: never; box?: never; origin?: never; - yogaNode?: never; + taffyNode?: never; children?: StopNode[]; }; @@ -40,5 +40,6 @@ export type SafeLinearGradientNode = Omit< > & { props: SafeLinearGradientProps; wasSplit: boolean; + stylesResolved?: boolean; children?: SafeStopNode[]; }; diff --git a/packages/layout/src/types/link.ts b/packages/layout/src/types/link.ts index a6e7d3666..b8f5279ea 100644 --- a/packages/layout/src/types/link.ts +++ b/packages/layout/src/types/link.ts @@ -1,7 +1,6 @@ import { SafeStyle, StyleProp } from '@react-pdf/stylesheet'; import * as P from '@react-pdf/primitives'; import { HitSlop } from '@react-pdf/types'; -import { YogaNode } from 'yoga-layout/load'; import { Box, NodeProps, Origin, RenderProp } from './base'; import { ImageNode, SafeImageNode } from './image'; @@ -27,13 +26,14 @@ export type LinkNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: (ViewNode | ImageNode | TextNode | TextInstanceNode)[]; }; export type SafeLinkNode = Omit & { style: SafeStyle; wasSplit: boolean; + stylesResolved?: boolean; children?: ( | SafeViewNode | SafeImageNode diff --git a/packages/layout/src/types/marker.ts b/packages/layout/src/types/marker.ts index 2953db025..714ed99d2 100644 --- a/packages/layout/src/types/marker.ts +++ b/packages/layout/src/types/marker.ts @@ -40,7 +40,7 @@ export type MarkerNode = { style?: never; box?: never; origin?: never; - yogaNode?: never; + taffyNode?: never; children?: ( | LineNode | PolylineNode @@ -56,6 +56,7 @@ export type MarkerNode = { export type SafeMarkerNode = Omit & { props: SafeMarkerProps; wasSplit: boolean; + stylesResolved?: boolean; children?: ( | SafeLineNode | SafePolylineNode diff --git a/packages/layout/src/types/note.ts b/packages/layout/src/types/note.ts index ee19ff5b6..8fd3686f6 100644 --- a/packages/layout/src/types/note.ts +++ b/packages/layout/src/types/note.ts @@ -10,12 +10,13 @@ export type NoteNode = { style?: StyleProp; box?: never; origin?: never; - yogaNode?: never; + taffyNode?: never; children?: TextInstanceNode[]; }; export type SafeNoteNode = Omit & { style: SafeStyle; wasSplit: boolean; + stylesResolved?: boolean; children?: SafeTextInstanceNode[]; }; diff --git a/packages/layout/src/types/page.ts b/packages/layout/src/types/page.ts index 4860fc779..e3601183f 100644 --- a/packages/layout/src/types/page.ts +++ b/packages/layout/src/types/page.ts @@ -1,6 +1,5 @@ import { SafeStyle, StyleProp } 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'; @@ -103,7 +102,7 @@ export type PageNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: ( | ViewNode | ImageNode @@ -123,6 +122,7 @@ export type PageNode = { export type SafePageNode = Omit & { style: SafeStyle; wasSplit: boolean; + stylesResolved?: boolean; children?: ( | SafeViewNode | SafeImageNode diff --git a/packages/layout/src/types/path.ts b/packages/layout/src/types/path.ts index 03ca82324..aef976078 100644 --- a/packages/layout/src/types/path.ts +++ b/packages/layout/src/types/path.ts @@ -22,7 +22,7 @@ export type PathNode = { style?: StyleProp; box?: never; origin?: never; - yogaNode?: never; + taffyNode?: never; children?: never[]; }; @@ -30,4 +30,5 @@ export type SafePathNode = Omit & { style: SafeStyle; props: SafePathProps; wasSplit: boolean; + stylesResolved?: boolean; }; diff --git a/packages/layout/src/types/polygon.ts b/packages/layout/src/types/polygon.ts index 55940945f..87b75d84f 100644 --- a/packages/layout/src/types/polygon.ts +++ b/packages/layout/src/types/polygon.ts @@ -22,7 +22,7 @@ export type PolygonNode = { style?: StyleProp; box?: never; origin?: never; - yogaNode?: never; + taffyNode?: never; children?: never[]; }; @@ -30,4 +30,5 @@ export type SafePolygonNode = Omit & { style: SafeStyle; props: SafePolygonProps; wasSplit: boolean; + stylesResolved?: boolean; }; diff --git a/packages/layout/src/types/polyline.ts b/packages/layout/src/types/polyline.ts index e6bea22f4..9de51db37 100644 --- a/packages/layout/src/types/polyline.ts +++ b/packages/layout/src/types/polyline.ts @@ -22,7 +22,7 @@ export type PolylineNode = { style?: StyleProp; box?: never; origin?: never; - yogaNode?: never; + taffyNode?: never; children?: never[]; }; @@ -30,4 +30,5 @@ export type SafePolylineNode = Omit & { style: SafeStyle; props: SafePolylineProps; wasSplit: boolean; + stylesResolved?: boolean; }; diff --git a/packages/layout/src/types/radial-gradient.ts b/packages/layout/src/types/radial-gradient.ts index 56c18159f..fbbe628ff 100644 --- a/packages/layout/src/types/radial-gradient.ts +++ b/packages/layout/src/types/radial-gradient.ts @@ -35,7 +35,7 @@ export type RadialGradientNode = { style?: never; box?: never; origin?: never; - yogaNode?: never; + taffyNode?: never; children?: StopNode[]; }; @@ -45,5 +45,6 @@ export type SafeRadialGradientNode = Omit< > & { props: SafeRadialGradientProps; wasSplit: boolean; + stylesResolved?: boolean; children?: SafeStopNode[]; }; diff --git a/packages/layout/src/types/rect.ts b/packages/layout/src/types/rect.ts index 904f8389c..490f7f20b 100644 --- a/packages/layout/src/types/rect.ts +++ b/packages/layout/src/types/rect.ts @@ -32,7 +32,7 @@ export type RectNode = { style?: StyleProp; box?: never; origin?: never; - yogaNode?: never; + taffyNode?: never; children?: never[]; }; @@ -40,4 +40,5 @@ export type SafeRectNode = Omit & { style: SafeStyle; props: SafeRectProps; wasSplit: boolean; + stylesResolved?: boolean; }; diff --git a/packages/layout/src/types/select.ts b/packages/layout/src/types/select.ts index e2fc5399f..93bc43287 100644 --- a/packages/layout/src/types/select.ts +++ b/packages/layout/src/types/select.ts @@ -1,6 +1,5 @@ import * as P from '@react-pdf/primitives'; import { SafeStyle, StyleProp } from '@react-pdf/stylesheet'; -import { YogaNode } from 'yoga-layout/load'; import { Box, FormCommonProps, Origin } from './base'; @@ -18,13 +17,14 @@ export type SelectNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: never; + taffyNode?: never; children?: never[]; }; export type SafeSelectNode = Omit & { style: SafeStyle; wasSplit: boolean; + stylesResolved?: boolean; }; export type ListNode = { @@ -33,11 +33,12 @@ export type ListNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: never[]; }; export type SafeListNode = Omit & { style: SafeStyle; wasSplit: boolean; + stylesResolved?: boolean; }; diff --git a/packages/layout/src/types/stop.ts b/packages/layout/src/types/stop.ts index 600cc6e80..0372a6004 100644 --- a/packages/layout/src/types/stop.ts +++ b/packages/layout/src/types/stop.ts @@ -18,11 +18,12 @@ export type StopNode = { style?: never; box?: never; origin?: never; - yogaNode?: never; + taffyNode?: never; children?: never[]; }; export type SafeStopNode = Omit & { props: StopSafeProps; wasSplit: boolean; + stylesResolved?: boolean; }; diff --git a/packages/layout/src/types/svg.ts b/packages/layout/src/types/svg.ts index 6b4025545..5079d54c6 100644 --- a/packages/layout/src/types/svg.ts +++ b/packages/layout/src/types/svg.ts @@ -1,6 +1,5 @@ import { SafeStyle, StyleProp } from '@react-pdf/stylesheet'; import * as P from '@react-pdf/primitives'; -import { YogaNode } from 'yoga-layout/load'; import { Box, @@ -64,7 +63,7 @@ export type SvgNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: ( | LineNode | PolylineNode @@ -85,6 +84,7 @@ export type SafeSvgNode = Omit & { style: SafeStyle; props: SvgSafeProps; wasSplit: boolean; + stylesResolved?: boolean; children?: ( | SafeLineNode | SafePolylineNode diff --git a/packages/layout/src/types/text-input.ts b/packages/layout/src/types/text-input.ts index 934644505..d0c1668c1 100644 --- a/packages/layout/src/types/text-input.ts +++ b/packages/layout/src/types/text-input.ts @@ -1,6 +1,5 @@ import * as P from '@react-pdf/primitives'; import { SafeStyle, StyleProp } from '@react-pdf/stylesheet'; -import { YogaNode } from 'yoga-layout/load'; import { Box, FormCommonProps, Origin } from './base'; @@ -52,11 +51,12 @@ export type TextInputNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: never[]; }; export type SafeTextInputNode = Omit & { style: SafeStyle; wasSplit: boolean; + stylesResolved?: boolean; }; diff --git a/packages/layout/src/types/text-instance.ts b/packages/layout/src/types/text-instance.ts index 14c9c6b61..6f3d1c90f 100644 --- a/packages/layout/src/types/text-instance.ts +++ b/packages/layout/src/types/text-instance.ts @@ -7,8 +7,11 @@ export type TextInstanceNode = { box?: never; origin?: never; children?: never[]; - yogaNode?: never; + taffyNode?: never; value: string; }; -export type SafeTextInstanceNode = TextInstanceNode & { wasSplit: boolean }; +export type SafeTextInstanceNode = TextInstanceNode & { + wasSplit: boolean; + stylesResolved?: boolean; +}; diff --git a/packages/layout/src/types/text.ts b/packages/layout/src/types/text.ts index e521d85a8..cf3164d66 100644 --- a/packages/layout/src/types/text.ts +++ b/packages/layout/src/types/text.ts @@ -1,7 +1,6 @@ import * as P from '@react-pdf/primitives'; import { SafeStyle, StyleProp } from '@react-pdf/stylesheet'; import { HyphenationCallback } from '@react-pdf/font'; -import { YogaNode } from 'yoga-layout/load'; import { ExclusionShape, Paragraph } from '@react-pdf/textkit'; import { Box, NodeProps, Origin, RenderProp } from './base'; @@ -48,7 +47,7 @@ export type TextNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; lines?: Paragraph; alignOffset?: number; // TODO: Remove this children?: (TextNode | TextInstanceNode | ImageNode | TspanNode)[]; @@ -57,6 +56,7 @@ export type TextNode = { export type SafeTextNode = Omit & { style: SafeStyle; wasSplit: boolean; + stylesResolved?: boolean; children?: ( | SafeTextNode | SafeTextInstanceNode diff --git a/packages/layout/src/types/tspan.ts b/packages/layout/src/types/tspan.ts index c2ede7e71..fb8a39926 100644 --- a/packages/layout/src/types/tspan.ts +++ b/packages/layout/src/types/tspan.ts @@ -24,7 +24,7 @@ export type TspanNode = { style?: StyleProp; box?: never; origin?: never; - yogaNode?: never; + taffyNode?: never; lines?: Paragraph; children?: TextInstanceNode[]; }; @@ -33,5 +33,6 @@ export type SafeTspanNode = Omit & { style: SafeStyle; props: SafeTspanProps; wasSplit: boolean; + stylesResolved?: boolean; children?: SafeTextInstanceNode[]; }; diff --git a/packages/layout/src/types/view.ts b/packages/layout/src/types/view.ts index 8a64d593a..303168934 100644 --- a/packages/layout/src/types/view.ts +++ b/packages/layout/src/types/view.ts @@ -1,6 +1,5 @@ import * as P from '@react-pdf/primitives'; import { SafeStyle, StyleProp } from '@react-pdf/stylesheet'; -import { YogaNode } from 'yoga-layout/load'; import { Box, NodeProps, Origin, RenderProp } from './base'; import { ImageNode, SafeImageNode } from './image'; @@ -33,7 +32,7 @@ export type ViewNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: ( | ViewNode | ImageNode @@ -53,6 +52,7 @@ export type ViewNode = { export type SafeViewNode = Omit & { style: SafeStyle; wasSplit: boolean; + stylesResolved?: boolean; children?: ( | SafeViewNode | SafeImageNode diff --git a/packages/layout/src/yoga/index.ts b/packages/layout/src/yoga/index.ts deleted file mode 100644 index 40099b67d..000000000 --- a/packages/layout/src/yoga/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { type Yoga, loadYoga as yogaLoadYoga } from 'yoga-layout/load'; - -let instancePromise: Promise; - -export const loadYoga = async () => { - // Yoga WASM binaries must be asynchronously compiled and loaded - // to prevent Event emitter memory leak warnings, Yoga must be loaded only once - const instance = await (instancePromise ??= yogaLoadYoga()); - - const config = instance.Config.create(); - - config.setPointScaleFactor(0); - - const node = { create: () => instance.Node.createWithConfig(config) }; - - return { node }; -}; diff --git a/packages/layout/tests/image/measureImage.test.ts b/packages/layout/tests/image/measureImage.test.ts new file mode 100644 index 000000000..6267b11ab --- /dev/null +++ b/packages/layout/tests/image/measureImage.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, test } from 'vitest'; + +import measureImage from '../../src/image/measureImage'; +import { MeasureMode } from '../../src/taffy/measure'; +import { SafeImageNode, SafePageNode } from '../../src/types'; + +const page = { + type: 'PAGE', + props: {}, + style: {}, + box: { width: 500, height: 800, top: 0, left: 0, right: 0, bottom: 0 }, +} as SafePageNode; + +const image = { + type: 'IMAGE', + props: {}, + style: {}, + image: { data: Buffer.from(''), width: 200, height: 100 }, +} as unknown as SafeImageNode; + +describe('measureImage', () => { + test('should scale height from an exact width', () => { + const size = measureImage(page, image)( + 100, + MeasureMode.Exactly, + Infinity, + MeasureMode.Undefined, + ); + + expect(size).toEqual({ height: 50 }); + }); + + test('should use the intrinsic size when width is unconstrained', () => { + const size = measureImage(page, image)( + Infinity, + MeasureMode.Undefined, + Infinity, + MeasureMode.Undefined, + ); + + expect(size).toEqual({ width: 200, height: 100 }); + }); + + test('should clamp the intrinsic size to the available height', () => { + const size = measureImage(page, image)( + Infinity, + MeasureMode.Undefined, + 50, + MeasureMode.AtMost, + ); + + expect(size).toEqual({ width: 100, height: 50 }); + }); + + test('should be empty at min-content', () => { + const size = measureImage(page, image)( + 0, + MeasureMode.Undefined, + Infinity, + MeasureMode.Undefined, + ); + + expect(size).toEqual({ width: 0, height: 0 }); + }); +}); diff --git a/packages/layout/tests/node/getBorderWidth.test.ts b/packages/layout/tests/node/getBorderWidth.test.ts deleted file mode 100644 index dfcd0d92e..000000000 --- a/packages/layout/tests/node/getBorderWidth.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, test } from 'vitest'; - -import * as Yoga from 'yoga-layout/load'; - -import getBorderWidth from '../../src/node/getBorderWidth'; - -const getComputedBorder = (value) => { - if (value === Yoga.Edge.Top) return 1; - if (value === Yoga.Edge.Right) return 2; - if (value === Yoga.Edge.Bottom) return 3; - return 4; -}; - -describe('node getBorderWidth', () => { - test('Should return 0 by default if no yoga node available', () => { - const result = getBorderWidth({ - type: 'VIEW', - props: {}, - style: {}, - children: [], - }); - - expect(result).toHaveProperty('borderTopWidth', 0); - expect(result).toHaveProperty('borderRightWidth', 0); - expect(result).toHaveProperty('borderBottomWidth', 0); - expect(result).toHaveProperty('borderLeftWidth', 0); - }); - - test('Should return yoga values if node available', () => { - const yogaNode = { getComputedBorder }; - const result = getBorderWidth({ - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode, - }); - - expect(result).toHaveProperty('borderTopWidth', 1); - expect(result).toHaveProperty('borderRightWidth', 2); - expect(result).toHaveProperty('borderBottomWidth', 3); - expect(result).toHaveProperty('borderLeftWidth', 4); - }); -}); diff --git a/packages/layout/tests/node/getDimension.test.ts b/packages/layout/tests/node/getDimension.test.ts deleted file mode 100644 index f46df02ad..000000000 --- a/packages/layout/tests/node/getDimension.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { describe, expect, test } from 'vitest'; - -import getDimension from '../../src/node/getDimension'; - -const getComputedWidth = () => 10; -const getComputedHeight = () => 20; - -describe('node getDimension', () => { - test('Should return 0 by default if no yoga node available', () => { - const result = getDimension({ - type: 'VIEW', - props: {}, - style: {}, - children: [], - }); - - expect(result).toHaveProperty('width', 0); - expect(result).toHaveProperty('height', 0); - }); - - test('Should return yoga values if node available', () => { - const yogaNode = { getComputedWidth, getComputedHeight }; - 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.ts b/packages/layout/tests/node/getMargin.test.ts index 356150663..f8664edc8 100644 --- a/packages/layout/tests/node/getMargin.test.ts +++ b/packages/layout/tests/node/getMargin.test.ts @@ -1,18 +1,9 @@ import { describe, expect, test } from 'vitest'; -import * as Yoga from 'yoga-layout/load'; - import getMargin from '../../src/node/getMargin'; -const getComputedMargin = (value) => { - if (value === Yoga.Edge.Top) return 1; - if (value === Yoga.Edge.Right) return 2; - if (value === Yoga.Edge.Bottom) return 3; - return 4; -}; - describe('node getMargin', () => { - test('Should return 0 by default if no yoga node available', () => { + test('Should return 0 by default', () => { const result = getMargin({ type: 'VIEW', props: {}, @@ -26,22 +17,6 @@ describe('node getMargin', () => { expect(result).toHaveProperty('marginLeft', 0); }); - test('Should return yoga values if node available', () => { - const yogaNode = { getComputedMargin }; - const result = getMargin({ - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode, - }); - - expect(result).toHaveProperty('marginTop', 1); - expect(result).toHaveProperty('marginRight', 2); - expect(result).toHaveProperty('marginBottom', 3); - expect(result).toHaveProperty('marginLeft', 4); - }); - test('Should return box specific values if available', () => { const result = getMargin({ type: 'VIEW', diff --git a/packages/layout/tests/node/getPadding.test.ts b/packages/layout/tests/node/getPadding.test.ts index 509eb0070..3f884f419 100644 --- a/packages/layout/tests/node/getPadding.test.ts +++ b/packages/layout/tests/node/getPadding.test.ts @@ -1,18 +1,9 @@ import { describe, expect, test } from 'vitest'; -import * as Yoga from 'yoga-layout/load'; - import getPadding from '../../src/node/getPadding'; -const getComputedPadding = (value) => { - if (value === Yoga.Edge.Top) return 1; - if (value === Yoga.Edge.Right) return 2; - if (value === Yoga.Edge.Bottom) return 3; - return 4; -}; - describe('node getPadding', () => { - test('Should return 0 by default if no yoga node available', () => { + test('Should return 0 by default', () => { const result = getPadding({ type: 'VIEW', props: {}, @@ -26,22 +17,6 @@ describe('node getPadding', () => { expect(result).toHaveProperty('paddingLeft', 0); }); - test('Should return yoga values if node available', () => { - const yogaNode = { getComputedPadding }; - const result = getPadding({ - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode, - }); - - expect(result).toHaveProperty('paddingTop', 1); - expect(result).toHaveProperty('paddingRight', 2); - expect(result).toHaveProperty('paddingBottom', 3); - expect(result).toHaveProperty('paddingLeft', 4); - }); - test('Should return box specific values if available', () => { const result = getPadding({ type: 'VIEW', diff --git a/packages/layout/tests/node/getPosition.test.ts b/packages/layout/tests/node/getPosition.test.ts deleted file mode 100644 index ec990cd2f..000000000 --- a/packages/layout/tests/node/getPosition.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { describe, expect, test } from 'vitest'; - -import getPosition from '../../src/node/getPosition'; - -const getComputedTop = () => 10; -const getComputedRight = () => 20; -const getComputedBottom = () => 30; -const getComputedLeft = () => 40; - -describe('node getPosition', () => { - test('Should return 0 by default if no yoga node available', () => { - const result = getPosition({ - type: 'VIEW', - props: {}, - style: {}, - children: [], - }); - - expect(result).toHaveProperty('top', 0); - expect(result).toHaveProperty('right', 0); - expect(result).toHaveProperty('bottom', 0); - expect(result).toHaveProperty('left', 0); - }); - - test('Should return yoga values if node available', () => { - const yogaNode = { - getComputedTop, - getComputedRight, - getComputedBottom, - getComputedLeft, - }; - const result = getPosition({ - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode, - }); - - expect(result).toHaveProperty('top', 10); - expect(result).toHaveProperty('right', 20); - expect(result).toHaveProperty('bottom', 30); - expect(result).toHaveProperty('left', 40); - }); -}); diff --git a/packages/layout/tests/node/setAlignContent.test.ts b/packages/layout/tests/node/setAlignContent.test.ts deleted file mode 100644 index ec49fd1f4..000000000 --- a/packages/layout/tests/node/setAlignContent.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -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 = { - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode: { setAlignContent: mock }, - } as SafeNode; - - beforeEach(() => { - mock.mockReset(); - }); - - test('should return node if no yoga node available', () => { - 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); - }); - - test('Should set align auto by default', () => { - const result = setAlignContent(null)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.Auto); - expect(result).toBe(node); - }); - - test('Should set flex-start', () => { - const result = setAlignContent('flex-start')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.FlexStart); - expect(result).toBe(node); - }); - - test('Should set center', () => { - const result = setAlignContent('center')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.Center); - expect(result).toBe(node); - }); - - test('Should set flex-end', () => { - const result = setAlignContent('flex-end')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.FlexEnd); - expect(result).toBe(node); - }); - - test('Should set stretch', () => { - const result = setAlignContent('stretch')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.Stretch); - expect(result).toBe(node); - }); - - test('Should set baseline', () => { - const result = setAlignContent('baseline')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.Baseline); - expect(result).toBe(node); - }); - - test('Should set space-between', () => { - const result = setAlignContent('space-between')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.SpaceBetween); - expect(result).toBe(node); - }); - - test('Should set space-around', () => { - const result = setAlignContent('space-around')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.SpaceAround); - expect(result).toBe(node); - }); - - test('Should set space-evenly', () => { - const result = setAlignContent('space-evenly')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.SpaceEvenly); - expect(result).toBe(node); - }); -}); diff --git a/packages/layout/tests/node/setAlignItems.test.ts b/packages/layout/tests/node/setAlignItems.test.ts deleted file mode 100644 index 46733ad78..000000000 --- a/packages/layout/tests/node/setAlignItems.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -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 = { - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode: { setAlignItems: mock }, - } as SafeNode; - - beforeEach(() => { - mock.mockReset(); - }); - - test('should return node if no yoga node available', () => { - 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); - }); - - test('Should set align auto by stretch', () => { - const result = setAlignItems(null)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.Stretch); - expect(result).toBe(node); - }); - - test('Should set flex-start', () => { - const result = setAlignItems('flex-start')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.FlexStart); - expect(result).toBe(node); - }); - - test('Should set center', () => { - const result = setAlignItems('center')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.Center); - expect(result).toBe(node); - }); - - test('Should set flex-end', () => { - const result = setAlignItems('flex-end')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.FlexEnd); - expect(result).toBe(node); - }); - - test('Should set stretch', () => { - const result = setAlignItems('stretch')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.Stretch); - expect(result).toBe(node); - }); - - test('Should set baseline', () => { - const result = setAlignItems('baseline')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.Baseline); - expect(result).toBe(node); - }); - - test('Should set space-between', () => { - const result = setAlignItems('space-between')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.SpaceBetween); - expect(result).toBe(node); - }); - - test('Should set space-around', () => { - const result = setAlignItems('space-around')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.SpaceAround); - expect(result).toBe(node); - }); -}); diff --git a/packages/layout/tests/node/setAlignSelf.test.ts b/packages/layout/tests/node/setAlignSelf.test.ts deleted file mode 100644 index 34f15e602..000000000 --- a/packages/layout/tests/node/setAlignSelf.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -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 = { - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode: { setAlignSelf: mock }, - } as SafeNode; - - beforeEach(() => { - mock.mockReset(); - }); - - test('should return node if no yoga node available', () => { - 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); - }); - - test('Should set align auto by default', () => { - const result = setAlignSelf(null)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.Auto); - expect(result).toBe(node); - }); - - test('Should set flex-start', () => { - const result = setAlignSelf('flex-start')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.FlexStart); - expect(result).toBe(node); - }); - - test('Should set center', () => { - const result = setAlignSelf('center')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.Center); - expect(result).toBe(node); - }); - - test('Should set flex-end', () => { - const result = setAlignSelf('flex-end')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.FlexEnd); - expect(result).toBe(node); - }); - - test('Should set stretch', () => { - const result = setAlignSelf('stretch')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.Stretch); - expect(result).toBe(node); - }); - - test('Should set baseline', () => { - const result = setAlignSelf('baseline')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.Baseline); - expect(result).toBe(node); - }); - - test('Should set space-between', () => { - const result = setAlignSelf('space-between')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.SpaceBetween); - expect(result).toBe(node); - }); - - test('Should set space-around', () => { - const result = setAlignSelf('space-around')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Align.SpaceAround); - expect(result).toBe(node); - }); -}); diff --git a/packages/layout/tests/node/setAspectRatio.test.ts b/packages/layout/tests/node/setAspectRatio.test.ts deleted file mode 100644 index 1fc97aeed..000000000 --- a/packages/layout/tests/node/setAspectRatio.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -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 = { - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode: { setAspectRatio: mock }, - } as SafeNode; - - beforeEach(() => { - mock.mockReset(); - }); - - test('should return node if no yoga node available', () => { - 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); - }); - - test('Should call yoga node setter if provided', () => { - const result = setAspectRatio(3)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(3); - expect(result).toBe(node); - }); -}); diff --git a/packages/layout/tests/node/setBorderWidth.test.ts b/packages/layout/tests/node/setBorderWidth.test.ts deleted file mode 100644 index 01fb18c21..000000000 --- a/packages/layout/tests/node/setBorderWidth.test.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; - -import * as Yoga from 'yoga-layout/load'; - -import setBorder, { - setBorderTop, - setBorderRight, - setBorderBottom, - setBorderLeft, -} from '../../src/node/setBorderWidth'; -import { SafeNode } from '../../src/types'; - -describe('node setBorderWidth', () => { - const mock = vi.fn(); - - 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(); - }); - - describe('setBorderTop', () => { - test('should return node if no yoga node available', () => { - const result = setBorderTop(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setBorderTop(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Edge.Top); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('Should throw error for percent values', () => { - expect(() => setBorderTop('50%')(node)).toThrow(); - }); - }); - - describe('setBorderRight', () => { - test('should return node if no yoga node available', () => { - const result = setBorderRight(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setBorderRight(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Edge.Right); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('Should throw error for percent values', () => { - expect(() => setBorderRight('50%')(node)).toThrow(); - }); - }); - - describe('setBorderBottom', () => { - test('should return node if no yoga node available', () => { - const result = setBorderBottom(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setBorderBottom(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Edge.Bottom); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('Should throw error for percent values', () => { - expect(() => setBorderBottom('50%')(node)).toThrow(); - }); - }); - - describe('setBorderLeft', () => { - test('should return node if no yoga node available', () => { - const result = setBorderLeft(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setBorderLeft(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Edge.Left); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('Should throw error for percent values', () => { - expect(() => setBorderLeft('50%')(node)).toThrow(); - }); - }); - - describe('setBorder', () => { - test('should return node if no yoga node available', () => { - const result = setBorder(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setBorder(50)(node); - - expect(mock.mock.calls).toHaveLength(4); - expect(mock.mock.calls[0]).toEqual([Yoga.Edge.Top, 50]); - expect(mock.mock.calls[1]).toEqual([Yoga.Edge.Right, 50]); - expect(mock.mock.calls[2]).toEqual([Yoga.Edge.Bottom, 50]); - expect(mock.mock.calls[3]).toEqual([Yoga.Edge.Left, 50]); - expect(result).toBe(node); - }); - - test('Should throw error for percent values', () => { - expect(() => setBorder('50%' as any)(node)).toThrow(); - }); - }); -}); diff --git a/packages/layout/tests/node/setDimension.test.ts b/packages/layout/tests/node/setDimension.test.ts deleted file mode 100644 index 4c6c1cbfb..000000000 --- a/packages/layout/tests/node/setDimension.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; -import { - setWidth, - setMinWidth, - setMaxWidth, - setHeight, - setMinHeight, - setMaxHeight, -} from '../../src/node/setDimension'; -import { SafeNode } from '../../src/types'; - -describe('node setDimension', () => { - const mockSetWidth = vi.fn(); - const mockSetWidthPercent = vi.fn(); - const mockSetMinWidth = vi.fn(); - const mockSetMaxWidth = vi.fn(); - const mockSetHeight = vi.fn(); - const mockSetHeightPercent = vi.fn(); - const mockSetMinHeight = vi.fn(); - const mockSetMaxHeight = vi.fn(); - - const node = { - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode: { - setWidth: mockSetWidth, - setWidthPercent: mockSetWidthPercent, - setMinWidth: mockSetMinWidth, - setMaxWidth: mockSetMaxWidth, - setHeight: mockSetHeight, - setHeightPercent: mockSetHeightPercent, - 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(); - mockSetWidthPercent.mockReset(); - mockSetMinWidth.mockReset(); - mockSetMaxWidth.mockReset(); - mockSetHeight.mockReset(); - mockSetHeightPercent.mockReset(); - mockSetMinHeight.mockReset(); - mockSetMaxHeight.mockReset(); - }); - - describe('setWidth', () => { - test('should return node if no yoga node available', () => { - const result = setWidth(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setWidth(50)(node); - - expect(mockSetWidth.mock.calls).toHaveLength(1); - expect(mockSetWidthPercent.mock.calls).toHaveLength(0); - expect(mockSetWidth.mock.calls[0][0]).toBe(50); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for percent values', () => { - const result = setWidth('50%')(node); - - expect(mockSetWidth.mock.calls).toHaveLength(0); - expect(mockSetWidthPercent.mock.calls).toHaveLength(1); - expect(mockSetWidthPercent.mock.calls[0][0]).toBe(50); - expect(result).toBe(node); - }); - }); - - describe('setMinWidth', () => { - test('should return node if no yoga node available', () => { - const result = setMinWidth(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setMinWidth(50)(node); - - expect(mockSetMaxWidth.mock.calls).toHaveLength(0); - expect(mockSetMinWidth.mock.calls).toHaveLength(1); - expect(mockSetMinWidth.mock.calls[0][0]).toBe(50); - expect(result).toBe(node); - }); - - test('Should throw error for percent values', () => { - expect(() => setMinWidth('50%')(node)).toThrow(); - }); - }); - - describe('setMaxWidth', () => { - test('should return node if no yoga node available', () => { - const result = setMaxWidth(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setMaxWidth(50)(node); - - expect(mockSetMinWidth.mock.calls).toHaveLength(0); - expect(mockSetMaxWidth.mock.calls).toHaveLength(1); - expect(mockSetMaxWidth.mock.calls[0][0]).toBe(50); - expect(result).toBe(node); - }); - - test('Should throw error for percent values', () => { - expect(() => setMaxWidth('50%')(node)).toThrow(); - }); - }); - - describe('setHeight', () => { - test('should return node if no yoga node available', () => { - const result = setHeight(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setHeight(50)(node); - - expect(mockSetHeight.mock.calls).toHaveLength(1); - expect(mockSetHeightPercent.mock.calls).toHaveLength(0); - expect(mockSetHeight.mock.calls[0][0]).toBe(50); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for percent values', () => { - const result = setHeight('50%')(node); - - expect(mockSetHeight.mock.calls).toHaveLength(0); - expect(mockSetHeightPercent.mock.calls).toHaveLength(1); - expect(mockSetHeightPercent.mock.calls[0][0]).toBe(50); - expect(result).toBe(node); - }); - }); - - describe('setMinHeight', () => { - test('should return node if no yoga node available', () => { - const result = setMinWidth(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setMinHeight(50)(node); - - expect(mockSetMaxHeight.mock.calls).toHaveLength(0); - expect(mockSetMinHeight.mock.calls).toHaveLength(1); - expect(mockSetMinHeight.mock.calls[0][0]).toBe(50); - expect(result).toBe(node); - }); - - test('Should throw error for percent values', () => { - expect(() => setMinHeight('50%')(node)).toThrow(); - }); - }); - - describe('setMaxHeight', () => { - test('should return node if no yoga node available', () => { - const result = setMaxHeight(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setMaxHeight(50)(node); - - expect(mockSetMinHeight.mock.calls).toHaveLength(0); - expect(mockSetMaxHeight.mock.calls).toHaveLength(1); - expect(mockSetMaxHeight.mock.calls[0][0]).toBe(50); - expect(result).toBe(node); - }); - - test('Should throw error for percent values', () => { - expect(() => setMaxHeight('50%')(node)).toThrow(); - }); - }); -}); diff --git a/packages/layout/tests/node/setDisplay.test.ts b/packages/layout/tests/node/setDisplay.test.ts deleted file mode 100644 index 0aad067fc..000000000 --- a/packages/layout/tests/node/setDisplay.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -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 = { - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode: { setDisplay: mock }, - } as SafeNode; - - beforeEach(() => { - mock.mockReset(); - }); - - test('should return node if no yoga node available', () => { - 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); - }); - - test('Should set flex by default', () => { - const result = setDisplay(null)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Display.Flex); - expect(result).toBe(node); - }); - - test('Should set flex', () => { - const result = setDisplay('flex')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Display.Flex); - expect(result).toBe(node); - }); - - test('Should set none', () => { - const result = setDisplay('none')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Display.None); - expect(result).toBe(node); - }); -}); diff --git a/packages/layout/tests/node/setFlexBasis.test.ts b/packages/layout/tests/node/setFlexBasis.test.ts deleted file mode 100644 index f9f3e89a6..000000000 --- a/packages/layout/tests/node/setFlexBasis.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -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 = { - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode: { setFlexBasis: mock }, - } as SafeNode; - - beforeEach(() => { - mock.mockReset(); - }); - - test('should return node if no yoga node available', () => { - 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); - }); - - test('Should set provided value', () => { - const result = setFlexBasis(2)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(2); - expect(result).toBe(node); - }); -}); diff --git a/packages/layout/tests/node/setFlexDirection.test.ts b/packages/layout/tests/node/setFlexDirection.test.ts deleted file mode 100644 index 5dc9ff1f3..000000000 --- a/packages/layout/tests/node/setFlexDirection.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -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 = { - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode: { setFlexDirection: mock }, - } as SafeNode; - - beforeEach(() => { - mock.mockReset(); - }); - - test('should return node if no yoga node available', () => { - 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); - }); - - test('Should set column by default', () => { - const result = setFlexDirection(null)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.FlexDirection.Column); - expect(result).toBe(node); - }); - - test('Should set column', () => { - const result = setFlexDirection('column')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.FlexDirection.Column); - expect(result).toBe(node); - }); - - test('Should set row', () => { - const result = setFlexDirection('row')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.FlexDirection.Row); - expect(result).toBe(node); - }); - - test('Should set row-reverse', () => { - const result = setFlexDirection('row-reverse')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.FlexDirection.RowReverse); - expect(result).toBe(node); - }); - - test('Should set column-reverse', () => { - const result = setFlexDirection('column-reverse')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.FlexDirection.ColumnReverse); - expect(result).toBe(node); - }); -}); diff --git a/packages/layout/tests/node/setFlexGrow.test.ts b/packages/layout/tests/node/setFlexGrow.test.ts deleted file mode 100644 index d9c50d57e..000000000 --- a/packages/layout/tests/node/setFlexGrow.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -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 = { - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode: { setFlexGrow: mock }, - } as SafeNode; - - beforeEach(() => { - mock.mockReset(); - }); - - test('should return node if no yoga node available', () => { - 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); - }); - - test('Should set zero by default', () => { - const result = setFlexGrow(null)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(0); - expect(result).toBe(node); - }); - - test('Should set provided value', () => { - const result = setFlexGrow(2)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(2); - expect(result).toBe(node); - }); -}); diff --git a/packages/layout/tests/node/setFlexShrink.test.ts b/packages/layout/tests/node/setFlexShrink.test.ts deleted file mode 100644 index e0a6db1a5..000000000 --- a/packages/layout/tests/node/setFlexShrink.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -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 = { - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode: { setFlexShrink: mock }, - } as SafeNode; - - beforeEach(() => { - mock.mockReset(); - }); - - test('should return node if no yoga node available', () => { - 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); - }); - - test('Should set one by default', () => { - const result = setFlexShrink(null)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(1); - expect(result).toBe(node); - }); - - test('Should set provided value', () => { - const result = setFlexShrink(2)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(2); - expect(result).toBe(node); - }); -}); diff --git a/packages/layout/tests/node/setFlexWrap.test.ts b/packages/layout/tests/node/setFlexWrap.test.ts deleted file mode 100644 index 37d0435fd..000000000 --- a/packages/layout/tests/node/setFlexWrap.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -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 = { - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode: { setFlexWrap: mock }, - } as SafeNode; - - beforeEach(() => { - mock.mockReset(); - }); - - test('should return node if no yoga node available', () => { - 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); - }); - - test('Should set no-wrap by default', () => { - const result = setFlexWrap(null)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Wrap.NoWrap); - expect(result).toBe(node); - }); - - test('Should set no-wrap', () => { - const result = setFlexWrap('no-wrap')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Wrap.NoWrap); - expect(result).toBe(node); - }); - - test('Should set wrap', () => { - const result = setFlexWrap('wrap')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Wrap.Wrap); - expect(result).toBe(node); - }); - - test('Should set wrap-reverse', () => { - const result = setFlexWrap('wrap-reverse')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Wrap.WrapReverse); - expect(result).toBe(node); - }); -}); diff --git a/packages/layout/tests/node/setGap.test.ts b/packages/layout/tests/node/setGap.test.ts deleted file mode 100644 index 3a6a629e6..000000000 --- a/packages/layout/tests/node/setGap.test.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; - -import * as Yoga from 'yoga-layout/load'; - -import { setRowGap, setColumnGap } from '../../src/node/setGap'; -import { SafeNode } from '../../src/types'; - -describe('node setGap', () => { - const mock = vi.fn(); - const mockPercent = vi.fn(); - - const node = { - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode: { - setGap: mock, - setGapPercent: 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(); - mockPercent.mockReset(); - }); - - describe('setRowGap', () => { - test('should return node if no yoga node available', () => { - const result = setRowGap(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('should call appropiate yoga node method for numeric values', () => { - const result = setRowGap(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Gutter.Row); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('should call appropiate yoga node method for percent values', () => { - const result = setRowGap('50%')(node); - - expect(mockPercent.mock.calls).toHaveLength(1); - expect(mockPercent.mock.calls[0][0]).toBe(Yoga.Gutter.Row); - expect(mockPercent.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - }); - - describe('setColumnGap', () => { - test('should return node if no yoga node available', () => { - const result = setColumnGap(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('should call appropiate yoga node method for numeric values', () => { - const result = setColumnGap(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Gutter.Column); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('should call appropiate yoga node method for percent values', () => { - const result = setColumnGap('50%')(node); - - expect(mockPercent.mock.calls).toHaveLength(1); - expect(mockPercent.mock.calls[0][0]).toBe(Yoga.Gutter.Column); - expect(mockPercent.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - }); -}); diff --git a/packages/layout/tests/node/setJustifyContent.test.ts b/packages/layout/tests/node/setJustifyContent.test.ts deleted file mode 100644 index 420c5155f..000000000 --- a/packages/layout/tests/node/setJustifyContent.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -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 = { - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode: { setJustifyContent: mock }, - } as SafeNode; - - beforeEach(() => { - mock.mockReset(); - }); - - test('should return node if no yoga node available', () => { - 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); - }); - - test('Should set center', () => { - const result = setJustifyContent('center')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Justify.Center); - expect(result).toBe(node); - }); - - test('Should set flex-end', () => { - const result = setJustifyContent('flex-end')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Justify.FlexEnd); - expect(result).toBe(node); - }); - - test('Should set flex-start', () => { - const result = setJustifyContent('flex-start')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Justify.FlexStart); - expect(result).toBe(node); - }); - - test('Should set space-between', () => { - const result = setJustifyContent('space-between')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Justify.SpaceBetween); - expect(result).toBe(node); - }); - - test('Should set space-around', () => { - const result = setJustifyContent('space-around')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Justify.SpaceAround); - expect(result).toBe(node); - }); - - test('Should set space-evenly', () => { - const result = setJustifyContent('space-evenly')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Justify.SpaceEvenly); - expect(result).toBe(node); - }); -}); diff --git a/packages/layout/tests/node/setMargin.test.ts b/packages/layout/tests/node/setMargin.test.ts deleted file mode 100644 index cf3b0e14e..000000000 --- a/packages/layout/tests/node/setMargin.test.ts +++ /dev/null @@ -1,220 +0,0 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; - -import * as Yoga from 'yoga-layout/load'; - -import setMargin, { - setMarginTop, - setMarginRight, - setMarginBottom, - setMarginLeft, -} from '../../src/node/setMargin'; -import { SafeNode } from '../../src/types'; - -describe('node setMargin', () => { - const mock = vi.fn(); - const mockAuto = vi.fn(); - 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(); - mockAuto.mockReset(); - mockPercent.mockReset(); - }); - - describe('setMarginTop', () => { - test('should return node if no yoga node available', () => { - const result = setMarginTop(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for auto value', () => { - const result = setMarginTop('auto')(node); - - expect(mockAuto.mock.calls).toHaveLength(1); - expect(mockAuto.mock.calls[0][0]).toBe(Yoga.Edge.Top); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setMarginTop(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Edge.Top); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for percent values', () => { - const result = setMarginTop('50%')(node); - - expect(mockPercent.mock.calls).toHaveLength(1); - expect(mockPercent.mock.calls[0][0]).toBe(Yoga.Edge.Top); - expect(mockPercent.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - }); - - describe('setMarginRight', () => { - test('should return node if no yoga node available', () => { - const result = setMarginRight(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for auto value', () => { - const result = setMarginRight('auto')(node); - - expect(mockAuto.mock.calls).toHaveLength(1); - expect(mockAuto.mock.calls[0][0]).toBe(Yoga.Edge.Right); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setMarginRight(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Edge.Right); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for percent values', () => { - const result = setMarginRight('50%')(node); - - expect(mockPercent.mock.calls).toHaveLength(1); - expect(mockPercent.mock.calls[0][0]).toBe(Yoga.Edge.Right); - expect(mockPercent.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - }); - - describe('setMarginBottom', () => { - test('should return node if no yoga node available', () => { - const result = setMarginBottom(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for auto value', () => { - const result = setMarginBottom('auto')(node); - - expect(mockAuto.mock.calls).toHaveLength(1); - expect(mockAuto.mock.calls[0][0]).toBe(Yoga.Edge.Bottom); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setMarginBottom(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Edge.Bottom); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for percent values', () => { - const result = setMarginBottom('50%')(node); - - expect(mockPercent.mock.calls).toHaveLength(1); - expect(mockPercent.mock.calls[0][0]).toBe(Yoga.Edge.Bottom); - expect(mockPercent.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - }); - - describe('setMarginLeft', () => { - test('should return node if no yoga node available', () => { - const result = setMarginLeft(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for auto value', () => { - const result = setMarginLeft('auto')(node); - - expect(mockAuto.mock.calls).toHaveLength(1); - expect(mockAuto.mock.calls[0][0]).toBe(Yoga.Edge.Left); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setMarginLeft(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Edge.Left); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for percent values', () => { - const result = setMarginLeft('50%')(node); - - expect(mockPercent.mock.calls).toHaveLength(1); - expect(mockPercent.mock.calls[0][0]).toBe(Yoga.Edge.Left); - expect(mockPercent.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - }); - - describe('setMargin', () => { - test('should return node if no yoga node available', () => { - const result = setMargin(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for auto value', () => { - const result = setMargin('auto')(node); - - expect(mockAuto.mock.calls).toHaveLength(4); - expect(mockAuto.mock.calls[0][0]).toBe(Yoga.Edge.Top); - expect(mockAuto.mock.calls[1][0]).toBe(Yoga.Edge.Right); - expect(mockAuto.mock.calls[2][0]).toBe(Yoga.Edge.Bottom); - expect(mockAuto.mock.calls[3][0]).toBe(Yoga.Edge.Left); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setMargin(50)(node); - - expect(mock.mock.calls).toHaveLength(4); - expect(mock.mock.calls[0]).toEqual([Yoga.Edge.Top, 50]); - expect(mock.mock.calls[1]).toEqual([Yoga.Edge.Right, 50]); - expect(mock.mock.calls[2]).toEqual([Yoga.Edge.Bottom, 50]); - expect(mock.mock.calls[3]).toEqual([Yoga.Edge.Left, 50]); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for percent values', () => { - const result = setMargin('50%')(node); - - expect(mockPercent.mock.calls).toHaveLength(4); - expect(mockPercent.mock.calls[0]).toEqual([Yoga.Edge.Top, 50]); - expect(mockPercent.mock.calls[1]).toEqual([Yoga.Edge.Right, 50]); - expect(mockPercent.mock.calls[2]).toEqual([Yoga.Edge.Bottom, 50]); - expect(mockPercent.mock.calls[3]).toEqual([Yoga.Edge.Left, 50]); - expect(result).toBe(node); - }); - }); -}); diff --git a/packages/layout/tests/node/setOverflow.test.ts b/packages/layout/tests/node/setOverflow.test.ts deleted file mode 100644 index 288564cac..000000000 --- a/packages/layout/tests/node/setOverflow.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -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 = { - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode: { setOverflow: mock }, - } as SafeNode; - - beforeEach(() => { - mock.mockReset(); - }); - - test('should return node if no yoga node available', () => { - 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); - }); - - test('Should set visible', () => { - const result = setOverflow('visible')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Overflow.Visible); - expect(result).toBe(node); - }); - - test('Should set scroll', () => { - const result = setOverflow('scroll')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Overflow.Scroll); - expect(result).toBe(node); - }); - - test('Should set hidden', () => { - const result = setOverflow('hidden')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Overflow.Hidden); - expect(result).toBe(node); - }); -}); diff --git a/packages/layout/tests/node/setPadding.test.ts b/packages/layout/tests/node/setPadding.test.ts deleted file mode 100644 index 19b7d6259..000000000 --- a/packages/layout/tests/node/setPadding.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; - -import * as Yoga from 'yoga-layout/load'; - -import setPadding, { - setPaddingTop, - setPaddingRight, - 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(); - mockPercent.mockReset(); - }); - - describe('setPaddingTop', () => { - test('should return node if no yoga node available', () => { - const result = setPaddingTop(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setPaddingTop(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Edge.Top); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for percent values', () => { - const result = setPaddingTop('50%')(node); - - expect(mockPercent.mock.calls).toHaveLength(1); - expect(mockPercent.mock.calls[0][0]).toBe(Yoga.Edge.Top); - expect(mockPercent.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - }); - - describe('setPaddingRight', () => { - test('should return node if no yoga node available', () => { - const result = setPaddingRight(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setPaddingRight(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Edge.Right); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for percent values', () => { - const result = setPaddingRight('50%')(node); - - expect(mockPercent.mock.calls).toHaveLength(1); - expect(mockPercent.mock.calls[0][0]).toBe(Yoga.Edge.Right); - expect(mockPercent.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - }); - - describe('setPaddingBottom', () => { - test('should return node if no yoga node available', () => { - const result = setPaddingBottom(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setPaddingBottom(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Edge.Bottom); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for percent values', () => { - const result = setPaddingBottom('50%')(node); - - expect(mockPercent.mock.calls).toHaveLength(1); - expect(mockPercent.mock.calls[0][0]).toBe(Yoga.Edge.Bottom); - expect(mockPercent.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - }); - - describe('setPaddingLeft', () => { - test('should return node if no yoga node available', () => { - const result = setPaddingLeft(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setPaddingLeft(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Edge.Left); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for percent values', () => { - const result = setPaddingLeft('50%')(node); - - expect(mockPercent.mock.calls).toHaveLength(1); - expect(mockPercent.mock.calls[0][0]).toBe(Yoga.Edge.Left); - expect(mockPercent.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - }); - - describe('setPadding', () => { - test('should return node if no yoga node available', () => { - const result = setPadding(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setPadding(50)(node); - - expect(mock.mock.calls).toHaveLength(4); - expect(mock.mock.calls[0]).toEqual([Yoga.Edge.Top, 50]); - expect(mock.mock.calls[1]).toEqual([Yoga.Edge.Right, 50]); - expect(mock.mock.calls[2]).toEqual([Yoga.Edge.Bottom, 50]); - expect(mock.mock.calls[3]).toEqual([Yoga.Edge.Left, 50]); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for percent values', () => { - const result = setPadding('50%')(node); - - expect(mockPercent.mock.calls).toHaveLength(4); - expect(mockPercent.mock.calls[0]).toEqual([Yoga.Edge.Top, 50]); - expect(mockPercent.mock.calls[1]).toEqual([Yoga.Edge.Right, 50]); - expect(mockPercent.mock.calls[2]).toEqual([Yoga.Edge.Bottom, 50]); - expect(mockPercent.mock.calls[3]).toEqual([Yoga.Edge.Left, 50]); - expect(result).toBe(node); - }); - }); -}); diff --git a/packages/layout/tests/node/setPosition.test.ts b/packages/layout/tests/node/setPosition.test.ts deleted file mode 100644 index 8781cd7e1..000000000 --- a/packages/layout/tests/node/setPosition.test.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { beforeEach, describe, expect, test, vi } from 'vitest'; - -import * as Yoga from 'yoga-layout/load'; - -import setPosition, { - setPositionTop, - setPositionRight, - 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(); - mockPercent.mockReset(); - }); - - describe('setPositionTop', () => { - test('should return node if no yoga node available', () => { - const result = setPositionTop(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setPositionTop(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Edge.Top); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for percent values', () => { - const result = setPositionTop('50%')(node); - - expect(mockPercent.mock.calls).toHaveLength(1); - expect(mockPercent.mock.calls[0][0]).toBe(Yoga.Edge.Top); - expect(mockPercent.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - }); - - describe('setPositionRight', () => { - test('should return node if no yoga node available', () => { - const result = setPositionRight(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setPositionRight(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Edge.Right); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for percent values', () => { - const result = setPositionRight('50%')(node); - - expect(mockPercent.mock.calls).toHaveLength(1); - expect(mockPercent.mock.calls[0][0]).toBe(Yoga.Edge.Right); - expect(mockPercent.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - }); - - describe('setPositionBottom', () => { - test('should return node if no yoga node available', () => { - const result = setPositionBottom(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setPositionBottom(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Edge.Bottom); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for percent values', () => { - const result = setPositionBottom('50%')(node); - - expect(mockPercent.mock.calls).toHaveLength(1); - expect(mockPercent.mock.calls[0][0]).toBe(Yoga.Edge.Bottom); - expect(mockPercent.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - }); - - describe('setPositionLeft', () => { - test('should return node if no yoga node available', () => { - const result = setPositionLeft(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setPositionLeft(50)(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.Edge.Left); - expect(mock.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for percent values', () => { - const result = setPositionLeft('50%')(node); - - expect(mockPercent.mock.calls).toHaveLength(1); - expect(mockPercent.mock.calls[0][0]).toBe(Yoga.Edge.Left); - expect(mockPercent.mock.calls[0][1]).toBe(50); - expect(result).toBe(node); - }); - }); - - describe('setPosition', () => { - test('should return node if no yoga node available', () => { - const result = setPosition(null)(emptyNode); - - expect(result).toBe(emptyNode); - }); - - test('Should call appropiate yoga node method for numeric values', () => { - const result = setPosition(50)(node); - - expect(mock.mock.calls).toHaveLength(4); - expect(mock.mock.calls[0]).toEqual([Yoga.Edge.Top, 50]); - expect(mock.mock.calls[1]).toEqual([Yoga.Edge.Right, 50]); - expect(mock.mock.calls[2]).toEqual([Yoga.Edge.Bottom, 50]); - expect(mock.mock.calls[3]).toEqual([Yoga.Edge.Left, 50]); - expect(result).toBe(node); - }); - - test('Should call appropiate yoga node method for percent values', () => { - const result = setPosition('50%')(node); - - expect(mockPercent.mock.calls).toHaveLength(4); - expect(mockPercent.mock.calls[0]).toEqual([Yoga.Edge.Top, 50]); - expect(mockPercent.mock.calls[1]).toEqual([Yoga.Edge.Right, 50]); - expect(mockPercent.mock.calls[2]).toEqual([Yoga.Edge.Bottom, 50]); - expect(mockPercent.mock.calls[3]).toEqual([Yoga.Edge.Left, 50]); - expect(result).toBe(node); - }); - }); -}); diff --git a/packages/layout/tests/node/setPositionType.test.ts b/packages/layout/tests/node/setPositionType.test.ts deleted file mode 100644 index 6f442ed1e..000000000 --- a/packages/layout/tests/node/setPositionType.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -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 = { - type: 'VIEW', - props: {}, - style: {}, - children: [], - yogaNode: { setPositionType: mock }, - } as SafeNode; - - beforeEach(() => { - mock.mockReset(); - }); - - test('should return node if no yoga node available', () => { - 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); - }); - - test('Should set relative', () => { - const result = setPositionType('relative')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.PositionType.Relative); - expect(result).toBe(node); - }); - - test('Should set absolute', () => { - const result = setPositionType('absolute')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.PositionType.Absolute); - expect(result).toBe(node); - }); - - test('Should set static', () => { - const result = setPositionType('static')(node); - - expect(mock.mock.calls).toHaveLength(1); - expect(mock.mock.calls[0][0]).toBe(Yoga.PositionType.Static); - expect(result).toBe(node); - }); -}); diff --git a/packages/layout/tests/node/toTaffyStyle.test.ts b/packages/layout/tests/node/toTaffyStyle.test.ts new file mode 100644 index 000000000..854b1fd3d --- /dev/null +++ b/packages/layout/tests/node/toTaffyStyle.test.ts @@ -0,0 +1,140 @@ +import { describe, expect, test } from 'vitest'; +import { + AlignContent, + AlignItems, + AlignSelf, + Display, + FlexDirection, + FlexWrap, + JustifyContent, + Overflow, + Position, +} from 'taffy-layout'; + +import toTaffyStyle from '../../src/node/toTaffyStyle'; +import { SafeNode } from '../../src/types'; + +const view = (style = {}, extra = {}): SafeNode => + ({ type: 'VIEW', props: {}, children: [], style, ...extra }) as SafeNode; + +describe('node toTaffyStyle', () => { + test('should apply yoga compatible defaults', () => { + const style = toTaffyStyle(view()); + + expect(style.display).toBe(Display.Flex); + expect(style.position).toBe(Position.Relative); + expect(style.flexDirection).toBe(FlexDirection.Column); + expect(style.flexWrap).toBe(FlexWrap.NoWrap); + expect(style.alignItems).toBe(AlignItems.Stretch); + expect(style.alignSelf).toBeUndefined(); + expect(style.alignContent).toBe(AlignContent.FlexStart); + expect(style.justifyContent).toBe(JustifyContent.FlexStart); + expect(style.overflowX).toBe(Overflow.Visible); + expect(style.flexGrow).toBe(0); + expect(style.flexShrink).toBe(1); + expect(style.flexBasis).toBe('auto'); + expect(style.width).toBe('auto'); + expect(style.height).toBe('auto'); + expect(style.minWidth).toBe(0); + expect(style.minHeight).toBe(0); + expect(style.maxWidth).toBe('auto'); + expect(style.marginTop).toBe(0); + expect(style.paddingLeft).toBe(0); + expect(style.borderTop).toBe(0); + expect(style.rowGap).toBe(0); + expect(style.top).toBe('auto'); + expect(style.aspectRatio).toBeUndefined(); + }); + + test('should map enum-like values', () => { + const style = toTaffyStyle( + view({ + display: 'none', + position: 'absolute', + flexDirection: 'row-reverse', + flexWrap: 'wrap', + alignItems: 'center', + alignSelf: 'flex-end', + alignContent: 'space-between', + justifyContent: 'space-evenly', + overflow: 'hidden', + }), + ); + + expect(style.display).toBe(Display.None); + expect(style.position).toBe(Position.Absolute); + expect(style.flexDirection).toBe(FlexDirection.RowReverse); + expect(style.flexWrap).toBe(FlexWrap.Wrap); + expect(style.alignItems).toBe(AlignItems.Center); + expect(style.alignSelf).toBe(AlignSelf.FlexEnd); + expect(style.alignContent).toBe(AlignContent.SpaceBetween); + expect(style.justifyContent).toBe(JustifyContent.SpaceEvenly); + expect(style.overflowX).toBe(Overflow.Hidden); + expect(style.overflowY).toBe(Overflow.Hidden); + }); + + test('should pass numbers, percentages and auto through', () => { + const style = toTaffyStyle( + view({ + width: 100, + height: '50%', + marginLeft: 'auto', + flexBasis: '25%', + minWidth: 10, + maxHeight: '80%', + paddingTop: '5%', + top: 5, + left: '10%', + rowGap: 4, + columnGap: '2%', + aspectRatio: 1.5, + flexGrow: 2, + flexShrink: 0, + }), + ); + + expect(style.width).toBe(100); + expect(style.height).toBe('50%'); + expect(style.marginLeft).toBe('auto'); + expect(style.flexBasis).toBe('25%'); + expect(style.minWidth).toBe(10); + expect(style.maxHeight).toBe('80%'); + expect(style.paddingTop).toBe('5%'); + expect(style.top).toBe(5); + expect(style.left).toBe('10%'); + expect(style.rowGap).toBe(4); + expect(style.columnGap).toBe('2%'); + expect(style.aspectRatio).toBe(1.5); + expect(style.flexGrow).toBe(2); + expect(style.flexShrink).toBe(0); + }); + + test('should ignore auto where taffy has no auto', () => { + const style = toTaffyStyle( + view({ paddingTop: 'auto', minWidth: 'auto', borderTopWidth: 'auto' }), + ); + + expect(style.paddingTop).toBe(0); + expect(style.minWidth).toBe(0); + expect(style.borderTop).toBe(0); + }); + + test('should ignore insets on static nodes', () => { + const style = toTaffyStyle(view({ position: 'static', top: 10 })); + + expect(style.position).toBe(Position.Relative); + expect(style.top).toBeUndefined(); + }); + + test('should take page height from its box', () => { + const page = { + type: 'PAGE', + props: {}, + children: [], + style: { height: 500 }, + box: { width: 100, height: 200, top: 0, left: 0, right: 0, bottom: 0 }, + } as unknown as SafeNode; + + expect(toTaffyStyle(page).height).toBe(200); + }); +}); diff --git a/packages/layout/tests/paginate/fuzz.test.ts b/packages/layout/tests/paginate/fuzz.test.ts index b52f1daea..8cd041833 100644 --- a/packages/layout/tests/paginate/fuzz.test.ts +++ b/packages/layout/tests/paginate/fuzz.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from 'vitest'; import FontStore from '@react-pdf/font'; -import { loadYoga } from '../../src/yoga'; +import { loadTaffy } from '../../src/taffy'; import resolveDimensions from '../../src/steps/resolveDimensions'; import resolveStyles from '../../src/steps/resolveStyles'; import resolveInheritance from '../../src/steps/resolveInheritance'; @@ -9,6 +9,8 @@ import resolvePageTemplates from '../../src/steps/resolvePageTemplates'; import nextPagination from '../../src/paginate'; import { SafeDocumentNode, SafeNode } from '../../src/types'; +await loadTaffy(); + const fontStore = new FontStore(); // Deterministic, so a failure names the seed that reproduces it. @@ -81,7 +83,6 @@ const node = (rand: () => number, depth: number): any => { const document = async (rand: () => number): Promise => ({ type: 'DOCUMENT', - yoga: await loadYoga(), props: {}, children: [ { @@ -135,7 +136,7 @@ describe('randomly generated documents', () => { documents.forEach((laid, index) => { const seed = SEEDS[index]; - const root = nextPagination(laid); + const root = nextPagination(laid, fontStore); root.children.forEach((page, pageIndex) => { boxes(page as unknown as SafeNode).forEach(([child, top]) => { @@ -172,8 +173,8 @@ describe('randomly generated documents', () => { ); pairs.forEach(([seed, a, b]) => { - const first = nextPagination(a); - const second = nextPagination(b); + const first = nextPagination(a, fontStore); + const second = nextPagination(b, fontStore); const shape = (root: SafeDocumentNode) => root.children.map((page) => diff --git a/packages/layout/tests/paginate/layout.test.ts b/packages/layout/tests/paginate/layout.test.ts index 2b8d3cc80..b3079349f 100644 --- a/packages/layout/tests/paginate/layout.test.ts +++ b/packages/layout/tests/paginate/layout.test.ts @@ -1,13 +1,15 @@ import { describe, expect, test } from 'vitest'; import FontStore from '@react-pdf/font'; -import { loadYoga } from '../../src/yoga'; +import { loadTaffy } from '../../src/taffy'; import resolveDimensions from '../../src/steps/resolveDimensions'; import resolvePageTemplates from '../../src/steps/resolvePageTemplates'; import resolvePagination from '../../src/paginate'; import { instantiateTemplate, isContent } from '../../src/page/template'; import { SafeNode, SafePageNode } from '../../src/types'; +await loadTaffy(); + const fontStore = new FontStore(); // Instance-shaped nodes throughout: the reconciler host config wraps user @@ -35,7 +37,6 @@ const region = (children: any[]): any => const doc = async (pageStyle: any, children: any[], pageProps = {}) => ({ type: 'DOCUMENT', - yoga: await loadYoga(), props: {}, children: [{ type: 'PAGE', props: pageProps, style: pageStyle, children }], }); diff --git a/packages/layout/tests/paginate/paginate.test.ts b/packages/layout/tests/paginate/paginate.test.ts index 2b41c1fd1..d8db3c498 100644 --- a/packages/layout/tests/paginate/paginate.test.ts +++ b/packages/layout/tests/paginate/paginate.test.ts @@ -1,12 +1,14 @@ import { describe, expect, test, vi } from 'vitest'; import FontStore from '@react-pdf/font'; -import { loadYoga } from '../../src/yoga'; +import { loadTaffy } from '../../src/taffy'; import resolveDimensions from '../../src/steps/resolveDimensions'; import resolvePageTemplates from '../../src/steps/resolvePageTemplates'; import resolvePagination from '../../src/paginate'; import { SafeDocumentNode, SafeNode, SafePageNode } from '../../src/types'; +await loadTaffy(); + const fontStore = new FontStore(); // Instances, as the pre-wrapped render props return them @@ -51,7 +53,6 @@ const doc = async ( pageProps = {}, ): Promise => ({ type: 'DOCUMENT', - yoga: await loadYoga(), props: {}, children: [{ type: 'PAGE', props: pageProps, style: pageStyle, children }], }); diff --git a/packages/layout/tests/paginate/parity.test.ts b/packages/layout/tests/paginate/parity.test.ts index 5ca9cba3c..d73c63f64 100644 --- a/packages/layout/tests/paginate/parity.test.ts +++ b/packages/layout/tests/paginate/parity.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from 'vitest'; import FontStore from '@react-pdf/font'; -import { loadYoga } from '../../src/yoga'; +import { loadTaffy } from '../../src/taffy'; import resolveDimensions from '../../src/steps/resolveDimensions'; import resolveStyles from '../../src/steps/resolveStyles'; import resolveInheritance from '../../src/steps/resolveInheritance'; @@ -10,6 +10,8 @@ import resolvePageTemplates from '../../src/steps/resolvePageTemplates'; import nextPagination from '../../src/paginate'; import { SafeDocumentNode, SafeNode } from '../../src/types'; +await loadTaffy(); + const fontStore = new FontStore(); const view = (style, children = []): any => ({ @@ -35,7 +37,6 @@ const text = (value: string, style = {}): any => ({ const doc = async (pageStyle, children): Promise => ({ type: 'DOCUMENT', - yoga: await loadYoga(), props: {}, children: [{ type: 'PAGE', props: {}, style: pageStyle, children }], }); @@ -185,7 +186,7 @@ describe('paginate adapter parity with resolvePagination', () => { }); // Free space is whatever the page has left once its content is placed. The -// first pass runs pages unconstrained, so yoga never distributes it; these +// first pass runs pages unconstrained, so the layout engine never distributes it; these // pin the two ways it comes back — a relayout for growth, arithmetic for // justifyContent. describe('free space left at the bottom of a page', () => { diff --git a/packages/layout/tests/steps/__snapshots__/resolveStyles.test.ts.snap b/packages/layout/tests/steps/__snapshots__/resolveStyles.test.ts.snap index a2ed375a2..059fe7dd5 100644 --- a/packages/layout/tests/steps/__snapshots__/resolveStyles.test.ts.snap +++ b/packages/layout/tests/steps/__snapshots__/resolveStyles.test.ts.snap @@ -19,12 +19,14 @@ exports[`layout resolveStyles > Should overide default link styles 1`] = ` "color": "wheat", "textDecoration": "none", }, + "stylesResolved": true, "type": "LINK", "wasSplit": false, }, ], "props": {}, "style": {}, + "stylesResolved": true, "type": "PAGE", "wasSplit": false, }, @@ -54,12 +56,14 @@ exports[`layout resolveStyles > Should overide default link styles with array 1` "color": "wheat", "textDecoration": "none", }, + "stylesResolved": true, "type": "LINK", "wasSplit": false, }, ], "props": {}, "style": {}, + "stylesResolved": true, "type": "PAGE", "wasSplit": false, }, @@ -89,12 +93,14 @@ exports[`layout resolveStyles > Should resolve default link styles 1`] = ` "color": "blue", "textDecoration": "underline", }, + "stylesResolved": true, "type": "LINK", "wasSplit": false, }, ], "props": {}, "style": {}, + "stylesResolved": true, "type": "PAGE", "wasSplit": false, }, @@ -139,12 +145,14 @@ exports[`layout resolveStyles > Should resolve nested node styles 1`] = ` "paddingRight": 720, "paddingTop": 28.34645669291339, }, + "stylesResolved": true, "type": "VIEW", "wasSplit": false, }, ], "props": {}, "style": {}, + "stylesResolved": true, "type": "PAGE", "wasSplit": false, }, @@ -189,12 +197,14 @@ exports[`layout resolveStyles > Should resolve nested node styles array 1`] = ` "paddingRight": 720, "paddingTop": 28.34645669291339, }, + "stylesResolved": true, "type": "VIEW", "wasSplit": false, }, ], "props": {}, "style": {}, + "stylesResolved": true, "type": "PAGE", "wasSplit": false, }, @@ -223,6 +233,7 @@ exports[`layout resolveStyles > Should resolve nested node styles media queries "style": { "backgroundColor": "green", }, + "stylesResolved": true, "type": "VIEW", "wasSplit": false, }, @@ -231,12 +242,14 @@ exports[`layout resolveStyles > Should resolve nested node styles media queries "style": { "backgroundColor": "red", }, + "stylesResolved": true, "type": "VIEW", "wasSplit": false, }, ], "props": {}, "style": {}, + "stylesResolved": true, "type": "PAGE", "wasSplit": false, }, @@ -257,6 +270,7 @@ exports[`layout resolveStyles > Should resolve nested node styles media queries "style": { "backgroundColor": "green", }, + "stylesResolved": true, "type": "VIEW", "wasSplit": false, }, @@ -265,6 +279,7 @@ exports[`layout resolveStyles > Should resolve nested node styles media queries "style": { "backgroundColor": "red", }, + "stylesResolved": true, "type": "VIEW", "wasSplit": false, }, @@ -274,6 +289,7 @@ exports[`layout resolveStyles > Should resolve nested node styles media queries "height": 200, "width": 100, }, + "stylesResolved": true, "type": "PAGE", "wasSplit": false, }, @@ -308,6 +324,7 @@ exports[`layout resolveStyles > Should resolve page styles 1`] = ` "paddingRight": 720, "paddingTop": 28.34645669291339, }, + "stylesResolved": true, "type": "PAGE", "wasSplit": false, }, @@ -342,6 +359,7 @@ exports[`layout resolveStyles > Should resolve page styles array 1`] = ` "paddingRight": 720, "paddingTop": 28.34645669291339, }, + "stylesResolved": true, "type": "PAGE", "wasSplit": false, }, @@ -366,6 +384,7 @@ exports[`layout resolveStyles > Should resolve several pages styles 1`] = ` "paddingRight": 720, "paddingTop": 28.34645669291339, }, + "stylesResolved": true, "type": "PAGE", "wasSplit": false, }, @@ -387,6 +406,7 @@ exports[`layout resolveStyles > Should resolve several pages styles 1`] = ` "borderTopWidth": 28.346456692913385, "fontWeight": 700, }, + "stylesResolved": true, "type": "PAGE", "wasSplit": false, }, diff --git a/packages/layout/tests/steps/resolvePagination.test.ts b/packages/layout/tests/steps/resolvePagination.test.ts index 2466aaf4b..7ed232ee5 100644 --- a/packages/layout/tests/steps/resolvePagination.test.ts +++ b/packages/layout/tests/steps/resolvePagination.test.ts @@ -1,11 +1,13 @@ import { describe, expect, test } from 'vitest'; import FontStore from '@react-pdf/font'; -import { loadYoga } from '../../src/yoga'; +import { loadTaffy } from '../../src/taffy'; import resolvePagination from '../../src/steps/resolvePagination'; import resolveDimensions from '../../src/steps/resolveDimensions'; import { SafeDocumentNode } from '../../src/types'; +await loadTaffy(); + const fontStore = new FontStore(); // dimensions is required by pagination step and them are calculated here @@ -14,11 +16,8 @@ const calcLayout = (node: SafeDocumentNode) => describe('pagination step', () => { test('should stretch absolute block to full page size', async () => { - const yoga = await loadYoga(); - const layout = calcLayout({ type: 'DOCUMENT', - yoga, props: {}, children: [ { @@ -64,11 +63,8 @@ describe('pagination step', () => { }); test('should force new height for split nodes', async () => { - const yoga = await loadYoga(); - const layout = calcLayout({ type: 'DOCUMENT', - yoga, props: {}, children: [ { @@ -110,11 +106,8 @@ describe('pagination step', () => { }); test('should force new height for split nodes with fixed height', async () => { - const yoga = await loadYoga(); - const layout = calcLayout({ type: 'DOCUMENT', - yoga, props: {}, children: [ { @@ -147,11 +140,8 @@ describe('pagination step', () => { }); test('should not wrap page with false wrap prop', async () => { - const yoga = await loadYoga(); - const layout = calcLayout({ type: 'DOCUMENT', - yoga, props: {}, children: [ { @@ -179,11 +169,8 @@ describe('pagination step', () => { }); test('should break on a container whose children can not fit on a page', async () => { - const yoga = await loadYoga(); - const layout = calcLayout({ type: 'DOCUMENT', - yoga, props: {}, children: [ { @@ -239,11 +226,8 @@ describe('pagination step', () => { }); test('should not infinitely loop when splitting pages', async () => { - const yoga = await loadYoga(); - calcLayout({ type: 'DOCUMENT', - yoga, props: {}, children: [ { @@ -277,11 +261,8 @@ describe('pagination step', () => { }); test('should take padding into account when splitting pages', async () => { - const yoga = await loadYoga(); - const root = { type: 'DOCUMENT' as const, - yoga, props: {}, style: {}, children: [ @@ -340,8 +321,6 @@ describe('pagination step', () => { }); test('should not duplicate bookmarks', async () => { - const yoga = await loadYoga(); - const bookmarkChapter1 = { ref: 0, title: 'chapter 1', @@ -378,7 +357,6 @@ describe('pagination step', () => { const result = calcLayout({ type: 'DOCUMENT', - yoga, props: {}, style: {}, children: [ diff --git a/packages/layout/tests/steps/resolveStyles.test.ts b/packages/layout/tests/steps/resolveStyles.test.ts index 6b00d834a..e46461546 100644 --- a/packages/layout/tests/steps/resolveStyles.test.ts +++ b/packages/layout/tests/steps/resolveStyles.test.ts @@ -1,6 +1,8 @@ import { describe, expect, test } from 'vitest'; -import resolveStyles from '../../src/steps/resolveStyles'; +import resolveStyles, { + resolvePageStyles, +} from '../../src/steps/resolveStyles'; describe('layout resolveStyles', () => { test('Should resolve page styles', () => { @@ -235,6 +237,39 @@ describe('layout resolveStyles', () => { expect(result).toMatchSnapshot(); }); + test('Should not resolve already resolved styles again', () => { + const doc = { + type: 'DOCUMENT', + props: {}, + children: [ + { + type: 'PAGE', + props: {}, + style: { fontSize: 10 }, + children: [ + { + type: 'TEXT', + props: {}, + style: { fontSize: 10, lineHeight: 1.5 }, + children: [{ type: 'TEXT_INSTANCE', value: 'hello' }], + }, + ], + }, + ], + }; + + const once = resolveStyles(doc as any); + const text = () => once.children[0].children[0]; + + expect(text().style.lineHeight).toBe(15); + + // Page relayout re-runs style resolution over the resolved page + const twice = resolvePageStyles(once.children[0] as any); + + expect(twice.children[0].style.lineHeight).toBe(15); + expect(twice.children[0].stylesResolved).toBe(true); + }); + test('Should resolve default link styles', () => { const result = resolveStyles({ type: 'DOCUMENT', diff --git a/packages/layout/tests/steps/resolveTextLayout.test.ts b/packages/layout/tests/steps/resolveTextLayout.test.ts index 7c2aa161b..16f648f85 100644 --- a/packages/layout/tests/steps/resolveTextLayout.test.ts +++ b/packages/layout/tests/steps/resolveTextLayout.test.ts @@ -1,12 +1,14 @@ import { describe, expect, test } from 'vitest'; import FontStore from '@react-pdf/font'; -import { loadYoga } from '../../src/yoga'; +import { loadTaffy } from '../../src/taffy'; import resolveTextLayout from '../../src/steps/resolveTextLayout'; import resolveDimensions from '../../src/steps/resolveDimensions'; import { SafeDocumentNode } from '../../src/types'; +await loadTaffy(); + const fontStore = new FontStore(); const getRoot = async ( @@ -15,7 +17,6 @@ const getRoot = async ( ): Promise => ({ type: 'DOCUMENT', props: {}, - yoga: await loadYoga(), children: [ { type: 'PAGE', @@ -54,9 +55,6 @@ describe('text layout step', () => { test('should calculate lines for text width defined height', async () => { const root = await getRoot('text text text', { height: 50 }); const dimensions = resolveDimensions(root, fontStore); - - expect(getText(dimensions).lines).not.toBeDefined(); - const textLayout = resolveTextLayout(dimensions, fontStore); expect(getText(textLayout).lines).toBeDefined(); diff --git a/packages/layout/tests/svg/measureSvg.test.ts b/packages/layout/tests/svg/measureSvg.test.ts new file mode 100644 index 000000000..36c5fbf07 --- /dev/null +++ b/packages/layout/tests/svg/measureSvg.test.ts @@ -0,0 +1,50 @@ +import { describe, expect, test } from 'vitest'; + +import measureSvg from '../../src/svg/measureSvg'; +import { MeasureMode } from '../../src/taffy/measure'; +import { SafePageNode, SafeSvgNode } from '../../src/types'; + +const page = { type: 'PAGE', props: {}, style: {} } as SafePageNode; + +const svg = { + type: 'SVG', + props: { viewBox: { minX: 0, minY: 0, maxX: 300, maxY: 150 } }, + style: {}, + children: [], +} as unknown as SafeSvgNode; + +describe('measureSvg', () => { + test('should scale height from the given width', () => { + const size = measureSvg(page, svg)( + 100, + MeasureMode.AtMost, + Infinity, + MeasureMode.Undefined, + ); + + expect(size).toEqual({ width: 100, height: 50 }); + }); + + test('should use the viewBox size when width is unconstrained', () => { + const size = measureSvg(page, svg)( + Infinity, + MeasureMode.Undefined, + Infinity, + MeasureMode.Undefined, + ); + + expect(size).toEqual({ width: 300, height: 150 }); + }); + + test('should be empty without a viewBox', () => { + const node = { ...svg, props: {} } as SafeSvgNode; + const size = measureSvg(page, node)( + Infinity, + MeasureMode.Undefined, + Infinity, + MeasureMode.Undefined, + ); + + expect(size).toEqual({}); + }); +}); diff --git a/packages/layout/tests/text/measureText.test.ts b/packages/layout/tests/text/measureText.test.ts index 3e757d940..d17b6475b 100644 --- a/packages/layout/tests/text/measureText.test.ts +++ b/packages/layout/tests/text/measureText.test.ts @@ -4,12 +4,19 @@ import * as P from '@react-pdf/primitives'; import FontStore from '@react-pdf/font'; import { SafeTextNode } from '../../src'; import measureText from '../../src/text/measureText'; +import { MeasureMode } from '../../src/taffy/measure'; 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 fontStore = new FontStore(); +const page = { + type: 'PAGE' as const, + props: {}, + style: {}, +}; + const createTextNode = ( value: string, style = {}, @@ -24,15 +31,10 @@ const createTextNode = ( describe('measureText', () => { describe('widthMode Exactly', () => { test('should return width and height', async () => { - const page = { - type: 'PAGE' as const, - props: {}, - style: {}, - }; const node = createTextNode(TEXT); const measureFunc = measureText(page, node, fontStore); - const dimensions = measureFunc(100, 1 /*Yoga.MeasureMode.Exactly*/, 50); + const dimensions = measureFunc(100, MeasureMode.Exactly, 50); expect(dimensions.width).toStrictEqual(100); expect(dimensions.height).toBe(39.599999999999994); @@ -41,18 +43,130 @@ describe('measureText', () => { describe('widthMode AtMost', () => { test('should return width and height', async () => { - const page = { - type: 'PAGE' as const, - props: {}, - style: {}, - }; const node = createTextNode(TEXT); const measureFunc = measureText(page, node, fontStore); - const dimensions = measureFunc(100, 2 /*Yoga.MeasureMode.AtMost*/, 50); + const dimensions = measureFunc(100, MeasureMode.AtMost, 50); expect(dimensions.width).toStrictEqual(100); expect(dimensions.height).toBe(39.599999999999994); }); + + test('should shrink-wrap short text', async () => { + const node = createTextNode('hi'); + const measureFunc = measureText(page, node, fontStore); + + const dimensions = measureFunc(100, MeasureMode.AtMost, 50); + + expect(dimensions.width).toBeLessThan(100); + expect(dimensions.width).toBeGreaterThan(0); + }); + }); + + describe('widthMode Undefined (max-content)', () => { + test('should return the natural single-line width', async () => { + const node = createTextNode(TEXT); + const measureFunc = measureText(page, node, fontStore); + + const dimensions = measureFunc(Infinity, MeasureMode.Undefined, Infinity); + const exactly = measureText(page, createTextNode(TEXT), fontStore)( + 100, + MeasureMode.Exactly, + 50, + ); + + expect(dimensions.width).toBeGreaterThan(100); + expect(dimensions.height).toBeLessThan(exactly.height!); + }); + + test('should keep the lines for left aligned text', async () => { + const node = createTextNode(TEXT); + measureText(page, node, fontStore)(Infinity, MeasureMode.Undefined, 50); + + expect(node.lines).toHaveLength(1); + }); + + test('should not keep the lines for centered text', async () => { + const node = createTextNode(TEXT, { textAlign: 'center' }); + measureText(page, node, fontStore)(Infinity, MeasureMode.Undefined, 50); + + expect(node.lines).toBeUndefined(); + }); + }); + + describe('line reuse', () => { + test('should reuse lines that fit a later, narrower constraint', async () => { + const node = createTextNode('hi'); + const measureFunc = measureText(page, node, fontStore); + + measureFunc(100, MeasureMode.AtMost, 50); + const lines = node.lines; + measureFunc(lines![0].xAdvance, MeasureMode.Exactly, 50); + + expect(node.lines).toBe(lines); + }); + + test('should re-layout when the box is wider than the lines were broken for', async () => { + const node = createTextNode(TEXT); + const measureFunc = measureText(page, node, fontStore); + + measureFunc(50, MeasureMode.Exactly, Infinity); + const narrow = node.lines!.length; + measureFunc(300, MeasureMode.Exactly, Infinity); + + expect(node.lines!.length).toBeLessThan(narrow); + }); + + test('should not lay out text at a zero width', async () => { + const node = createTextNode(TEXT); + const measureFunc = measureText(page, node, fontStore); + + const size = measureFunc(0, MeasureMode.Exactly, Infinity); + + expect(size).toEqual({ width: 0, height: 0 }); + expect(node.lines).toBeUndefined(); + }); + + test('should recover from an intermediate measure that truncated every line', async () => { + const node = createTextNode('Titel'); + const measureFunc = measureText(page, node, fontStore); + + const first = measureFunc(300, MeasureMode.Exactly, Infinity); + // padding-only width with the previous line height: nothing fits + measureFunc(5, MeasureMode.Exactly, first.height!); + const size = measureFunc(300, MeasureMode.Exactly, first.height!); + + expect(node.lines).toHaveLength(1); + expect(size.height).toBeGreaterThan(0); + }); + + test('should never re-layout a page-split fragment', async () => { + const node = createTextNode(TEXT); + const measureFunc = measureText(page, node, fontStore); + + measureFunc(100, MeasureMode.Exactly, 50); + const fragment = Object.assign({}, node, { + lines: node.lines!.slice(0, 1), + wasSplit: true, + }); + const fragmentMeasure = measureText(page, fragment, fontStore); + + fragmentMeasure(0, MeasureMode.Exactly, 50); + fragmentMeasure(300, MeasureMode.Exactly, 50); + + expect(fragment.lines).toHaveLength(1); + }); + + test('should re-layout when lines do not fit', async () => { + const node = createTextNode(TEXT); + const measureFunc = measureText(page, node, fontStore); + + measureFunc(Infinity, MeasureMode.Undefined, 50); + const lines = node.lines; + measureFunc(100, MeasureMode.Exactly, 50); + + expect(node.lines).not.toBe(lines); + expect(node.lines!.length).toBeGreaterThan(1); + }); }); }); diff --git a/packages/layout/tsconfig.json b/packages/layout/tsconfig.json index 490d6d29a..4dcaa2d62 100644 --- a/packages/layout/tsconfig.json +++ b/packages/layout/tsconfig.json @@ -15,5 +15,5 @@ "forceConsistentCasingInFileNames": true, "types": ["vitest/globals"], }, - "include": ["src", "globals.d.ts"], + "include": ["src"], } diff --git a/packages/paginate/README.md b/packages/paginate/README.md index 7e3684b90..cb4c66691 100644 --- a/packages/paginate/README.md +++ b/packages/paginate/README.md @@ -14,7 +14,7 @@ its page number is. The engine is independent of React and PDF rendering — it takes a vertical box model in and returns placed fragments out. React-PDF uses it to paginate -Yoga layout trees, but any consumer that can describe its content as boxes +Taffy layout trees, but any consumer that can describe its content as boxes with tops and heights can use it directly. Internally the tree is compiled to a box/glue/penalty item stream; that representation is an implementation detail. diff --git a/packages/tailwind/README.md b/packages/tailwind/README.md index 92cf94904..795a15e71 100644 --- a/packages/tailwind/README.md +++ b/packages/tailwind/README.md @@ -117,7 +117,7 @@ The returned `tw` function takes a space-separated class string and returns a re - Supports the CSS properties that make sense in a PDF context and are supported by react-pdf — see [valid CSS properties](https://react-pdf.org/styling#valid-css-properties). - Uses `pt` as the internal unit ([valid units](https://react-pdf.org/styling#valid-units)), with `1rem = 12pt` by default. Change it with `ptPerRem`. -- react-pdf uses [Yoga](https://yogalayout.dev/) for layout, so some defaults differ from the web — `flex-direction` defaults to `column`, for example. Add `flex-row` where you need it. +- react-pdf keeps Yoga-style layout defaults (now implemented with [Taffy](https://github.com/DioxusLabs/taffy)), so some differ from the web — `flex-direction` defaults to `column`, for example. Add `flex-row` where you need it. - Line heights are emitted unitless, since react-pdf only supports unitless `lineHeight`. - `aspect-auto` and `line-clamp-none` warn as unsupported. react-pdf has no style value meaning "no aspect ratio" or "no clamp" — leaving the utility off is the reset. - Intrinsic sizing (`w-fit`, `h-min`, `max-w-max`, …), `max-w-none` / `max-h-none`, and lengths in units react-pdf can't parse (`max-w-prose` is `65ch`) warn as unsupported. Yoga has no equivalent, and passing the value through would throw while laying out the document. diff --git a/yarn.lock b/yarn.lock index 58ee4e1fe..e6c9a02dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -766,8 +766,14 @@ dependencies: "@babel/helper-plugin-utils" "^7.18.6" -"@babel/plugin-transform-react-jsx-development@^7.18.6", "@babel/plugin-transform-react-jsx-self@^7.23.3": - name "@babel/plugin-transform-react-jsx-development" +"@babel/plugin-transform-react-jsx-development@^7.18.6": + version "7.23.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz#ed3e7dadde046cce761a8e3cf003a13d1a7972d9" + integrity sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ== + dependencies: + "@babel/helper-plugin-utils" "^7.22.5" + +"@babel/plugin-transform-react-jsx-self@^7.23.3": version "7.23.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.23.3.tgz#ed3e7dadde046cce761a8e3cf003a13d1a7972d9" integrity sha512-qXRvbeKDSfwnlJnanVRp0SfuWE5DQhwQr5xtLBzp56Wabyo+4CMosF6Kfp+eOD/4FYpql64XVJ2W0pVLlJZxOQ== @@ -9482,7 +9488,16 @@ string-argv@0.3.1: resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.3.1.tgz#95e2fbec0427ae19184935f816d74aaa4c5c19da" integrity sha512-a1uQGz7IyVy9YwhqjZIZu1c8JO8dNIe20xBmSS6qu9kv++k3JGzCVmprbNN5Kn+BgzD5E7YYwg1CcjuJMRNsvg== -"string-width-cjs@npm:string-width@^4.2.0", "string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -9582,7 +9597,14 @@ stringify-object@^3.3.0: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -9714,6 +9736,11 @@ synckit@^0.9.1: "@pkgr/core" "^0.1.0" tslib "^2.6.2" +taffy-layout@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/taffy-layout/-/taffy-layout-3.0.0.tgz#51930e2f7107d585540a66be016f6874abc1d3d3" + integrity sha512-d5IK13tpgAbGz8w4BBTpVdagN0GNMEV563FwuU8iHbGhBm9kWHZoZvS716sG2vUPZmol6DSuZZmRwLrG95s78Q== + tailwindcss@^3.4.17: version "3.4.17" resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.17.tgz#ae8406c0f96696a631c790768ff319d46d5e5a63" @@ -10553,7 +10580,7 @@ wordwrap@^1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -10571,6 +10598,15 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" @@ -10726,8 +10762,3 @@ yocto-queue@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251" integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g== - -yoga-layout@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/yoga-layout/-/yoga-layout-3.2.1.tgz#d2d1ba06f0e81c2eb650c3e5ad8b0b4adde1e843" - integrity sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==