From c81bccbe275ecf0df969065cb7317ba129921090 Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Sun, 20 Sep 2026 23:31:51 +0200 Subject: [PATCH 01/11] feat(layout): add taffy-layout dependency and loader taffy-layout wraps DioxusLabs' Taffy (flexbox/grid in Rust) as WASM. The loader compiles the module once per process, mirroring what the yoga loader did. Co-Authored-By: Claude Fable 5.1 --- packages/layout/package.json | 1 + packages/layout/src/taffy/index.ts | 11 +++++++ yarn.lock | 46 ++++++++++++++++++++++++++---- 3 files changed, 53 insertions(+), 5 deletions(-) create mode 100644 packages/layout/src/taffy/index.ts diff --git a/packages/layout/package.json b/packages/layout/package.json index 4e6c84aa3..edbe0679e 100644 --- a/packages/layout/package.json +++ b/packages/layout/package.json @@ -29,6 +29,7 @@ "@react-pdf/types": "^2.14.0", "emoji-regex-xs": "^1.0.0", "queue": "^6.0.1", + "taffy-layout": "^3.0.0", "yoga-layout": "^3.2.1" }, "files": [ 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/yarn.lock b/yarn.lock index 58ee4e1fe..79ccca5bc 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" From e0784968bae1952338c78490a2e964fe7e50d206 Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Sun, 20 Sep 2026 23:31:51 +0200 Subject: [PATCH 02/11] feat(layout): map node styles to Taffy style properties One pure function replaces the 30 yoga setter files. Defaults are pinned to what Yoga (without web defaults) applied so existing documents keep their layout: column direction, align-content flex-start, and a 0 automatic minimum size instead of Taffy's content-based one. Co-Authored-By: Claude Fable 5.1 --- packages/layout/src/node/toTaffyStyle.ts | 161 ++++++++++++++++++ .../layout/tests/node/toTaffyStyle.test.ts | 140 +++++++++++++++ 2 files changed, 301 insertions(+) create mode 100644 packages/layout/src/node/toTaffyStyle.ts create mode 100644 packages/layout/tests/node/toTaffyStyle.test.ts diff --git a/packages/layout/src/node/toTaffyStyle.ts b/packages/layout/src/node/toTaffyStyle.ts new file mode 100644 index 000000000..a828a7a83 --- /dev/null +++ b/packages/layout/src/node/toTaffyStyle.ts @@ -0,0 +1,161 @@ +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, + scroll: Overflow.Scroll, +}; + +/** + * 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/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); + }); +}); From 2c3e703ba770632eaf434da87c2bab53b4e91b62 Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Sun, 20 Sep 2026 23:31:51 +0200 Subject: [PATCH 03/11] refactor(layout): compute layout with Taffy instead of Yoga - resolveDimensions builds one TaffyTree per page, stores the node id on `taffyNode`, computes with rounding disabled and reads the Layout back into `box`. - Measure functions keep their (width, widthMode, height, heightMode) signature through a small adapter from Taffy's known/available sizes. `max-content` maps to an unconstrained width, which Yoga never asked for but Taffy does when sizing flex items in rows. - measureText caches lines on the node like before, but since Taffy calls measure several times per node it re-breaks only unconstrained lines that overflow, and lays out with a 0.01pt tolerance because Taffy hands widths back as f32. `alignOffset` is derived after layout from the width the lines were broken against (tracked in layoutText), instead of during measurement. - The engine instance is no longer threaded through pagination; the `resolveYoga` step becomes `resolveTaffy` and only loads the module. - getMargin/getPadding lose their engine lookups; they are only used inside measure functions, where the tree is mid-compute. - Yoga-specific unit tests for the setters/getters are replaced by toTaffyStyle and measureText tests. Co-Authored-By: Claude Fable 5.1 --- packages/layout/src/canvas/measureCanvas.ts | 4 +- packages/layout/src/image/measureImage.ts | 24 +- packages/layout/src/index.ts | 4 +- packages/layout/src/node/getBorderWidth.ts | 27 -- packages/layout/src/node/getDimension.ts | 25 -- packages/layout/src/node/getMargin.ts | 37 +- packages/layout/src/node/getPadding.ts | 37 +- packages/layout/src/node/getPosition.ts | 20 - packages/layout/src/node/removePaddings.ts | 7 +- packages/layout/src/node/setAlign.ts | 37 -- packages/layout/src/node/setAlignContent.ts | 12 - packages/layout/src/node/setAlignItems.ts | 12 - packages/layout/src/node/setAlignSelf.ts | 12 - packages/layout/src/node/setAspectRatio.ts | 20 - packages/layout/src/node/setBorderWidth.ts | 57 --- packages/layout/src/node/setDimension.ts | 55 --- packages/layout/src/node/setDisplay.ts | 22 -- packages/layout/src/node/setFlexBasis.ts | 12 - packages/layout/src/node/setFlexDirection.ts | 28 -- packages/layout/src/node/setFlexGrow.ts | 14 - packages/layout/src/node/setFlexShrink.ts | 14 - packages/layout/src/node/setFlexWrap.ts | 26 -- packages/layout/src/node/setGap.ts | 19 - packages/layout/src/node/setJustifyContent.ts | 30 -- packages/layout/src/node/setMargin.ts | 58 --- packages/layout/src/node/setOverflow.ts | 27 -- packages/layout/src/node/setPadding.ts | 58 --- packages/layout/src/node/setPosition.ts | 58 --- packages/layout/src/node/setPositionType.ts | 27 -- packages/layout/src/node/setYogaValue.ts | 52 --- packages/layout/src/paginate/fromPage.ts | 2 +- packages/layout/src/paginate/index.ts | 12 +- packages/layout/src/paginate/toFlow.ts | 10 +- packages/layout/src/paginate/types.ts | 3 +- packages/layout/src/steps/relayoutPage.ts | 2 +- .../layout/src/steps/resolveDimensions.ts | 353 +++++++----------- packages/layout/src/steps/resolveFloats.ts | 4 +- .../layout/src/steps/resolvePagePaddings.ts | 2 +- .../layout/src/steps/resolvePageTemplates.ts | 4 +- .../layout/src/steps/resolvePagination.ts | 27 +- packages/layout/src/steps/resolveStyles.ts | 2 +- packages/layout/src/steps/resolveSvg.ts | 1 - packages/layout/src/steps/resolveTaffy.ts | 14 + .../layout/src/steps/resolveTextLayout.ts | 2 +- packages/layout/src/steps/resolveYoga.ts | 10 - packages/layout/src/svg/measureSvg.ts | 13 +- packages/layout/src/taffy/measure.ts | 93 +++++ packages/layout/src/text/layoutText.ts | 19 +- packages/layout/src/text/measureText.ts | 92 +++-- packages/layout/src/types/base.ts | 5 - packages/layout/src/types/canvas.ts | 4 +- packages/layout/src/types/checkbox.ts | 3 +- packages/layout/src/types/circle.ts | 2 +- packages/layout/src/types/clip-path.ts | 2 +- packages/layout/src/types/defs.ts | 2 +- packages/layout/src/types/document.ts | 4 +- packages/layout/src/types/ellipse.ts | 2 +- packages/layout/src/types/field-set.ts | 3 +- packages/layout/src/types/g.ts | 2 +- packages/layout/src/types/image-background.ts | 3 +- packages/layout/src/types/image.ts | 3 +- packages/layout/src/types/line.ts | 2 +- packages/layout/src/types/linear-gradient.ts | 2 +- packages/layout/src/types/link.ts | 3 +- packages/layout/src/types/marker.ts | 2 +- packages/layout/src/types/note.ts | 2 +- packages/layout/src/types/page.ts | 3 +- packages/layout/src/types/path.ts | 2 +- packages/layout/src/types/polygon.ts | 2 +- packages/layout/src/types/polyline.ts | 2 +- packages/layout/src/types/radial-gradient.ts | 2 +- packages/layout/src/types/rect.ts | 2 +- packages/layout/src/types/select.ts | 5 +- packages/layout/src/types/stop.ts | 2 +- packages/layout/src/types/svg.ts | 3 +- packages/layout/src/types/text-input.ts | 3 +- packages/layout/src/types/text-instance.ts | 2 +- packages/layout/src/types/text.ts | 3 +- packages/layout/src/types/tspan.ts | 2 +- packages/layout/src/types/view.ts | 3 +- packages/layout/src/yoga/index.ts | 17 - .../layout/tests/node/getBorderWidth.test.ts | 44 --- .../layout/tests/node/getDimension.test.ts | 34 -- packages/layout/tests/node/getMargin.test.ts | 27 +- packages/layout/tests/node/getPadding.test.ts | 27 +- .../layout/tests/node/getPosition.test.ts | 45 --- .../layout/tests/node/setAlignContent.test.ts | 106 ------ .../layout/tests/node/setAlignItems.test.ts | 98 ----- .../layout/tests/node/setAlignSelf.test.ts | 98 ----- .../layout/tests/node/setAspectRatio.test.ts | 39 -- .../layout/tests/node/setBorderWidth.test.ts | 142 ------- .../layout/tests/node/setDimension.test.ts | 193 ---------- packages/layout/tests/node/setDisplay.test.ts | 59 --- .../layout/tests/node/setFlexBasis.test.ts | 40 -- .../tests/node/setFlexDirection.test.ts | 75 ---- .../layout/tests/node/setFlexGrow.test.ts | 48 --- .../layout/tests/node/setFlexShrink.test.ts | 48 --- .../layout/tests/node/setFlexWrap.test.ts | 67 ---- packages/layout/tests/node/setGap.test.ts | 87 ----- .../tests/node/setJustifyContent.test.ts | 83 ---- packages/layout/tests/node/setMargin.test.ts | 220 ----------- .../layout/tests/node/setOverflow.test.ts | 59 --- packages/layout/tests/node/setPadding.test.ts | 171 --------- .../layout/tests/node/setPosition.test.ts | 171 --------- .../layout/tests/node/setPositionType.test.ts | 59 --- packages/layout/tests/paginate/fuzz.test.ts | 5 +- packages/layout/tests/paginate/layout.test.ts | 5 +- .../layout/tests/paginate/paginate.test.ts | 5 +- packages/layout/tests/paginate/parity.test.ts | 7 +- .../tests/steps/resolvePagination.test.ts | 28 +- .../tests/steps/resolveTextLayout.test.ts | 8 +- .../layout/tests/text/measureText.test.ts | 87 ++++- 112 files changed, 522 insertions(+), 3262 deletions(-) delete mode 100644 packages/layout/src/node/getBorderWidth.ts delete mode 100644 packages/layout/src/node/getDimension.ts delete mode 100644 packages/layout/src/node/getPosition.ts delete mode 100644 packages/layout/src/node/setAlign.ts delete mode 100644 packages/layout/src/node/setAlignContent.ts delete mode 100644 packages/layout/src/node/setAlignItems.ts delete mode 100644 packages/layout/src/node/setAlignSelf.ts delete mode 100644 packages/layout/src/node/setAspectRatio.ts delete mode 100644 packages/layout/src/node/setBorderWidth.ts delete mode 100644 packages/layout/src/node/setDimension.ts delete mode 100644 packages/layout/src/node/setDisplay.ts delete mode 100644 packages/layout/src/node/setFlexBasis.ts delete mode 100644 packages/layout/src/node/setFlexDirection.ts delete mode 100644 packages/layout/src/node/setFlexGrow.ts delete mode 100644 packages/layout/src/node/setFlexShrink.ts delete mode 100644 packages/layout/src/node/setFlexWrap.ts delete mode 100644 packages/layout/src/node/setGap.ts delete mode 100644 packages/layout/src/node/setJustifyContent.ts delete mode 100644 packages/layout/src/node/setMargin.ts delete mode 100644 packages/layout/src/node/setOverflow.ts delete mode 100644 packages/layout/src/node/setPadding.ts delete mode 100644 packages/layout/src/node/setPosition.ts delete mode 100644 packages/layout/src/node/setPositionType.ts delete mode 100644 packages/layout/src/node/setYogaValue.ts create mode 100644 packages/layout/src/steps/resolveTaffy.ts delete mode 100644 packages/layout/src/steps/resolveYoga.ts create mode 100644 packages/layout/src/taffy/measure.ts delete mode 100644 packages/layout/src/yoga/index.ts delete mode 100644 packages/layout/tests/node/getBorderWidth.test.ts delete mode 100644 packages/layout/tests/node/getDimension.test.ts delete mode 100644 packages/layout/tests/node/getPosition.test.ts delete mode 100644 packages/layout/tests/node/setAlignContent.test.ts delete mode 100644 packages/layout/tests/node/setAlignItems.test.ts delete mode 100644 packages/layout/tests/node/setAlignSelf.test.ts delete mode 100644 packages/layout/tests/node/setAspectRatio.test.ts delete mode 100644 packages/layout/tests/node/setBorderWidth.test.ts delete mode 100644 packages/layout/tests/node/setDimension.test.ts delete mode 100644 packages/layout/tests/node/setDisplay.test.ts delete mode 100644 packages/layout/tests/node/setFlexBasis.test.ts delete mode 100644 packages/layout/tests/node/setFlexDirection.test.ts delete mode 100644 packages/layout/tests/node/setFlexGrow.test.ts delete mode 100644 packages/layout/tests/node/setFlexShrink.test.ts delete mode 100644 packages/layout/tests/node/setFlexWrap.test.ts delete mode 100644 packages/layout/tests/node/setGap.test.ts delete mode 100644 packages/layout/tests/node/setJustifyContent.test.ts delete mode 100644 packages/layout/tests/node/setMargin.test.ts delete mode 100644 packages/layout/tests/node/setOverflow.test.ts delete mode 100644 packages/layout/tests/node/setPadding.test.ts delete mode 100644 packages/layout/tests/node/setPosition.test.ts delete mode 100644 packages/layout/tests/node/setPositionType.test.ts 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..95ce989f8 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, 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/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..4eb92c5eb 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'; } 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..18801ee54 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,25 +10,22 @@ 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 }; } 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..311a88147 100644 --- a/packages/layout/src/text/measureText.ts +++ b/packages/layout/src/text/measureText.ts @@ -1,49 +1,99 @@ -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; + +/** + * Whether the lines laid out so far can be reused for a container of + * `width`. Lines broken against a definite width are always kept, even if + * the box has since become narrower (flex shrink) or wider (a split stripped + * padding): re-breaking text is not stable (Knuth-Plass) and pagination + * relies on lines surviving relayouts, which is also how the Yoga-based + * layout behaved. Only lines broken against an unconstrained width are + * re-broken, when they overflow. Lines of unknown origin are trusted. + */ +const fits = (node: SafeTextNode, width: number) => { + if (!node.lines) return false; + + const laidOutAt = getLinesLayoutWidth(node.lines); + + if (laidOutAt !== MAX_CONTENT_WIDTH) return true; + + return linesWidth(node) <= width + EPSILON; +}; /** - * 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) }; + } + + if (!fits(node, width)) { + // Widths arrive f32-rounded from Taffy; the tolerance keeps a line that + // measured as fitting from being broken again for a rounding error. + node.lines = layoutText(node, width + EPSILON, height, 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..d5ad0093d 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,7 +17,7 @@ export type CanvasNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: never[]; }; diff --git a/packages/layout/src/types/checkbox.ts b/packages/layout/src/types/checkbox.ts index 37dbf2fc0..543bb559e 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,7 +18,7 @@ export type CheckboxNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: never[]; }; diff --git a/packages/layout/src/types/circle.ts b/packages/layout/src/types/circle.ts index cc390991b..4a8ebc926 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[]; }; diff --git a/packages/layout/src/types/clip-path.ts b/packages/layout/src/types/clip-path.ts index defe891c0..128917f43 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 diff --git a/packages/layout/src/types/defs.ts b/packages/layout/src/types/defs.ts index f6637e0fc..ad6bd6e36 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 diff --git a/packages/layout/src/types/document.ts b/packages/layout/src/types/document.ts index aef6cb8c6..8f2062788 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,8 +70,7 @@ export type DocumentNode = { box?: never; origin?: never; style?: StyleProp; - yoga?: YogaInstance; - yogaNode?: never; + taffyNode?: never; children: PageNode[]; }; diff --git a/packages/layout/src/types/ellipse.ts b/packages/layout/src/types/ellipse.ts index 0b69e9861..5b99224a7 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[]; }; diff --git a/packages/layout/src/types/field-set.ts b/packages/layout/src/types/field-set.ts index 302fd7616..be5c9879d 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,7 +16,7 @@ export type FieldSetNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: (TextNode | ViewNode | TextInputNode)[]; }; diff --git a/packages/layout/src/types/g.ts b/packages/layout/src/types/g.ts index f3c06192e..af7b31a87 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 diff --git a/packages/layout/src/types/image-background.ts b/packages/layout/src/types/image-background.ts index b5267858b..b7d11564b 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 diff --git a/packages/layout/src/types/image.ts b/packages/layout/src/types/image.ts index 86c329988..de510b6b9 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,7 +70,7 @@ export type ImageNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: never[]; }; diff --git a/packages/layout/src/types/line.ts b/packages/layout/src/types/line.ts index 200126ce1..717f0ded7 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[]; }; diff --git a/packages/layout/src/types/linear-gradient.ts b/packages/layout/src/types/linear-gradient.ts index 58a78add7..2dd9511c5 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[]; }; diff --git a/packages/layout/src/types/link.ts b/packages/layout/src/types/link.ts index a6e7d3666..bc2ed5cbb 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,7 +26,7 @@ export type LinkNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: (ViewNode | ImageNode | TextNode | TextInstanceNode)[]; }; diff --git a/packages/layout/src/types/marker.ts b/packages/layout/src/types/marker.ts index 2953db025..cfdbba287 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 diff --git a/packages/layout/src/types/note.ts b/packages/layout/src/types/note.ts index ee19ff5b6..e7d9a0365 100644 --- a/packages/layout/src/types/note.ts +++ b/packages/layout/src/types/note.ts @@ -10,7 +10,7 @@ export type NoteNode = { style?: StyleProp; box?: never; origin?: never; - yogaNode?: never; + taffyNode?: never; children?: TextInstanceNode[]; }; diff --git a/packages/layout/src/types/page.ts b/packages/layout/src/types/page.ts index 4860fc779..c2cbc6aca 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 diff --git a/packages/layout/src/types/path.ts b/packages/layout/src/types/path.ts index 03ca82324..ba16c15fc 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[]; }; diff --git a/packages/layout/src/types/polygon.ts b/packages/layout/src/types/polygon.ts index 55940945f..0c017cf00 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[]; }; diff --git a/packages/layout/src/types/polyline.ts b/packages/layout/src/types/polyline.ts index e6bea22f4..ded0cf718 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[]; }; diff --git a/packages/layout/src/types/radial-gradient.ts b/packages/layout/src/types/radial-gradient.ts index 56c18159f..9fe7c0ac5 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[]; }; diff --git a/packages/layout/src/types/rect.ts b/packages/layout/src/types/rect.ts index 904f8389c..2f575615b 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[]; }; diff --git a/packages/layout/src/types/select.ts b/packages/layout/src/types/select.ts index e2fc5399f..cb971527b 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,7 +17,7 @@ export type SelectNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: never; + taffyNode?: never; children?: never[]; }; @@ -33,7 +32,7 @@ export type ListNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: never[]; }; diff --git a/packages/layout/src/types/stop.ts b/packages/layout/src/types/stop.ts index 600cc6e80..641a86454 100644 --- a/packages/layout/src/types/stop.ts +++ b/packages/layout/src/types/stop.ts @@ -18,7 +18,7 @@ export type StopNode = { style?: never; box?: never; origin?: never; - yogaNode?: never; + taffyNode?: never; children?: never[]; }; diff --git a/packages/layout/src/types/svg.ts b/packages/layout/src/types/svg.ts index 6b4025545..ff0db24b0 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 diff --git a/packages/layout/src/types/text-input.ts b/packages/layout/src/types/text-input.ts index 934644505..f9b23e943 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,7 +51,7 @@ export type TextInputNode = { style?: StyleProp; box?: Box; origin?: Origin; - yogaNode?: YogaNode; + taffyNode?: bigint; children?: never[]; }; diff --git a/packages/layout/src/types/text-instance.ts b/packages/layout/src/types/text-instance.ts index 14c9c6b61..898368698 100644 --- a/packages/layout/src/types/text-instance.ts +++ b/packages/layout/src/types/text-instance.ts @@ -7,7 +7,7 @@ export type TextInstanceNode = { box?: never; origin?: never; children?: never[]; - yogaNode?: never; + taffyNode?: never; value: string; }; diff --git a/packages/layout/src/types/text.ts b/packages/layout/src/types/text.ts index e521d85a8..1f314f19f 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)[]; diff --git a/packages/layout/src/types/tspan.ts b/packages/layout/src/types/tspan.ts index c2ede7e71..1df7cac6d 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[]; }; diff --git a/packages/layout/src/types/view.ts b/packages/layout/src/types/view.ts index 8a64d593a..7c421344c 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 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/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/paginate/fuzz.test.ts b/packages/layout/tests/paginate/fuzz.test.ts index b52f1daea..28d3f05dc 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: [ { 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/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/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/text/measureText.test.ts b/packages/layout/tests/text/measureText.test.ts index 3e757d940..bb8dcdc88 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,79 @@ 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 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); + }); }); }); From 21da2b5eeb23f06f7f0e4ed0318d57e3abdfd42e Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Sun, 20 Sep 2026 23:32:57 +0200 Subject: [PATCH 04/11] chore(layout): remove yoga-layout Drops the dependency, the hand-written yoga typings that shadowed it, and the ignore entries for a vendored yoga package that no longer exists. Co-Authored-By: Claude Fable 5.1 --- .prettierignore | 1 - CLAUDE.md | 2 +- eslint.config.ts | 1 - packages/layout/globals.d.ts | 246 ---------------------------------- packages/layout/package.json | 3 +- packages/layout/tsconfig.json | 2 +- packages/tailwind/README.md | 2 +- yarn.lock | 5 - 8 files changed, 4 insertions(+), 258 deletions(-) delete mode 100644 packages/layout/globals.d.ts 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/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/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 edbe0679e..34aec9987 100644 --- a/packages/layout/package.json +++ b/packages/layout/package.json @@ -29,8 +29,7 @@ "@react-pdf/types": "^2.14.0", "emoji-regex-xs": "^1.0.0", "queue": "^6.0.1", - "taffy-layout": "^3.0.0", - "yoga-layout": "^3.2.1" + "taffy-layout": "^3.0.0" }, "files": [ "lib" 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/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 79ccca5bc..e6c9a02dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10762,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== From b486dd3d8b5ff90c886c63cf470b5ab6e1f03714 Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Sun, 20 Sep 2026 23:38:15 +0200 Subject: [PATCH 05/11] fix(layout): give images and svgs an intrinsic max-content size Taffy measures flex items in rows with an unconstrained width. Yoga never did (it offered the container width), so measureImage and measureSvg had no answer and images without an explicit size collapsed inside row containers. Images now report their pixel size, svgs their viewBox size; flex-shrink still fits them into the row. Co-Authored-By: Claude Fable 5.1 --- packages/layout/src/image/measureImage.ts | 14 ++++ packages/layout/src/svg/measureSvg.ts | 12 ++++ .../layout/tests/image/measureImage.test.ts | 65 +++++++++++++++++++ packages/layout/tests/svg/measureSvg.test.ts | 50 ++++++++++++++ 4 files changed, 141 insertions(+) create mode 100644 packages/layout/tests/image/measureImage.test.ts create mode 100644 packages/layout/tests/svg/measureSvg.test.ts diff --git a/packages/layout/src/image/measureImage.ts b/packages/layout/src/image/measureImage.ts index 95ce989f8..9dbed335f 100644 --- a/packages/layout/src/image/measureImage.ts +++ b/packages/layout/src/image/measureImage.ts @@ -72,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/svg/measureSvg.ts b/packages/layout/src/svg/measureSvg.ts index 18801ee54..e0c3c5fc2 100644 --- a/packages/layout/src/svg/measureSvg.ts +++ b/packages/layout/src/svg/measureSvg.ts @@ -29,6 +29,18 @@ const measureCanvas = 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/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/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({}); + }); +}); From 640dbc4702f0e2f267d27dfb32611e3ac434d696 Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Sun, 20 Sep 2026 23:40:15 +0200 Subject: [PATCH 06/11] docs: record taffy migration results Fills in the differences, breakage and missing-features section of the migration notes, adds the changeset, and updates the current docs that named Yoga as the layout engine. Co-Authored-By: Claude Fable 5.1 --- .changeset/taffy-layout-engine.md | 7 +++++++ apps/site/app/(home)/page.tsx | 2 +- apps/site/content/docs/v4/tailwind.mdx | 4 ++-- packages/layout/README.md | 4 ++-- packages/paginate/README.md | 2 +- 5 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 .changeset/taffy-layout-engine.md 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/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/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/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. From 9ec5e3e71fb9ff0c17981f1bd346cd5bc33d97f9 Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Tue, 22 Sep 2026 23:43:57 +0200 Subject: [PATCH 07/11] fix(layout): re-break cached text lines when the box grows Taffy sizes flex items at intermediate main sizes before their final one (the hypothetical cross size of a flex-basis: 0 item is computed at a zero main size). Lines broken at such a width must not be kept once the box is wider, otherwise titles in row headers come out truncated to their first word. Lines are now reused only when broken at the same width, or at a wider one while still fitting (with 1pt of tolerance for f32 shrink). The paginate fuzz test now passes its font store to the paginator, since relayouts can legitimately re-break text. Co-Authored-By: Claude Fable 5.1 --- packages/layout/src/text/measureText.ts | 22 +++++++++++++-------- packages/layout/tests/paginate/fuzz.test.ts | 6 +++--- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/packages/layout/src/text/measureText.ts b/packages/layout/src/text/measureText.ts index 311a88147..45b3855ce 100644 --- a/packages/layout/src/text/measureText.ts +++ b/packages/layout/src/text/measureText.ts @@ -14,23 +14,29 @@ const MAX_CONTENT_WIDTH = 1e6; // 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`. Lines broken against a definite width are always kept, even if - * the box has since become narrower (flex shrink) or wider (a split stripped - * padding): re-breaking text is not stable (Knuth-Plass) and pagination - * relies on lines surviving relayouts, which is also how the Yoga-based - * layout behaved. Only lines broken against an unconstrained width are - * re-broken, when they overflow. Lines of unknown origin are trusted. + * `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; const laidOutAt = getLinesLayoutWidth(node.lines); - if (laidOutAt !== MAX_CONTENT_WIDTH) return true; + if (laidOutAt === undefined) return true; + if (Math.abs(laidOutAt - width) <= EPSILON) return true; - return linesWidth(node) <= width + EPSILON; + return laidOutAt >= width && linesWidth(node) <= width + OVERFLOW_TOLERANCE; }; /** diff --git a/packages/layout/tests/paginate/fuzz.test.ts b/packages/layout/tests/paginate/fuzz.test.ts index 28d3f05dc..8cd041833 100644 --- a/packages/layout/tests/paginate/fuzz.test.ts +++ b/packages/layout/tests/paginate/fuzz.test.ts @@ -136,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]) => { @@ -173,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) => From 08dcf98c2878d32d0f4015d604433757397a9c94 Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Tue, 22 Sep 2026 23:51:25 +0200 Subject: [PATCH 08/11] fix(layout): never re-break page-split text, skip zero-width measures splitText slices a node's lines but keeps all of its text children, so re-laying out a fragment resurrects the whole text and pagination never converges. Fragments (wasSplit) now always keep their lines. Taffy measures flex-basis: 0 items at a zero main size before their final one; breaking text word by word there is pure cost, so zero-width measures return an empty size without touching the cached lines. Co-Authored-By: Claude Fable 5.1 --- packages/layout/src/text/measureText.ts | 8 ++++ .../layout/tests/text/measureText.test.ts | 38 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/packages/layout/src/text/measureText.ts b/packages/layout/src/text/measureText.ts index 45b3855ce..4d6aafd71 100644 --- a/packages/layout/src/text/measureText.ts +++ b/packages/layout/src/text/measureText.ts @@ -31,6 +31,10 @@ const OVERFLOW_TOLERANCE = 1; 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; + const laidOutAt = getLinesLayoutWidth(node.lines); if (laidOutAt === undefined) return true; @@ -87,6 +91,10 @@ const measureText = 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)) { // Widths arrive f32-rounded from Taffy; the tolerance keeps a line that // measured as fitting from being broken again for a rounding error. diff --git a/packages/layout/tests/text/measureText.test.ts b/packages/layout/tests/text/measureText.test.ts index bb8dcdc88..ec4f614ea 100644 --- a/packages/layout/tests/text/measureText.test.ts +++ b/packages/layout/tests/text/measureText.test.ts @@ -106,6 +106,44 @@ describe('measureText', () => { 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 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); From 1a1afbcd187882ec4aff9957530b5aad497a1916 Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Wed, 23 Sep 2026 00:18:13 +0200 Subject: [PATCH 09/11] fix(layout): do not resolve node styles twice on page relayout relayoutPage runs resolvePageStyles over pages whose styles are already resolved (only freshly rendered dynamic nodes are new). Resolution is not idempotent: a unitless lineHeight is multiplied by the font size on every pass, so after a few page splits a line is thousands of points tall. Under Yoga this stayed invisible because text lines were never recomputed after the first layout. With Taffy, text is re-broken when its box grows, which picked up the inflated lineHeight and made pagination run forever. Nodes now carry a stylesResolved flag and are skipped on later passes. Co-Authored-By: Claude Fable 5.1 --- packages/layout/src/steps/resolveStyles.ts | 21 +++++++++-- packages/layout/src/types/canvas.ts | 1 + packages/layout/src/types/checkbox.ts | 1 + packages/layout/src/types/circle.ts | 1 + packages/layout/src/types/clip-path.ts | 1 + packages/layout/src/types/defs.ts | 1 + packages/layout/src/types/document.ts | 1 + packages/layout/src/types/ellipse.ts | 1 + packages/layout/src/types/field-set.ts | 1 + packages/layout/src/types/g.ts | 1 + packages/layout/src/types/image-background.ts | 1 + packages/layout/src/types/image.ts | 1 + packages/layout/src/types/line.ts | 1 + packages/layout/src/types/linear-gradient.ts | 1 + packages/layout/src/types/link.ts | 1 + packages/layout/src/types/marker.ts | 1 + packages/layout/src/types/note.ts | 1 + packages/layout/src/types/page.ts | 1 + packages/layout/src/types/path.ts | 1 + packages/layout/src/types/polygon.ts | 1 + packages/layout/src/types/polyline.ts | 1 + packages/layout/src/types/radial-gradient.ts | 1 + packages/layout/src/types/rect.ts | 1 + packages/layout/src/types/select.ts | 2 + packages/layout/src/types/stop.ts | 1 + packages/layout/src/types/svg.ts | 1 + packages/layout/src/types/text-input.ts | 1 + packages/layout/src/types/text-instance.ts | 5 ++- packages/layout/src/types/text.ts | 1 + packages/layout/src/types/tspan.ts | 1 + packages/layout/src/types/view.ts | 1 + .../__snapshots__/resolveStyles.test.ts.snap | 20 ++++++++++ .../layout/tests/steps/resolveStyles.test.ts | 37 ++++++++++++++++++- 33 files changed, 108 insertions(+), 5 deletions(-) diff --git a/packages/layout/src/steps/resolveStyles.ts b/packages/layout/src/steps/resolveStyles.ts index 4eb92c5eb..7fbf5beef 100644 --- a/packages/layout/src/steps/resolveStyles.ts +++ b/packages/layout/src/steps/resolveStyles.ts @@ -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/types/canvas.ts b/packages/layout/src/types/canvas.ts index d5ad0093d..5950289e4 100644 --- a/packages/layout/src/types/canvas.ts +++ b/packages/layout/src/types/canvas.ts @@ -24,4 +24,5 @@ export type CanvasNode = { 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 543bb559e..8b3528b43 100644 --- a/packages/layout/src/types/checkbox.ts +++ b/packages/layout/src/types/checkbox.ts @@ -25,4 +25,5 @@ export type CheckboxNode = { 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 4a8ebc926..d9ac418fa 100644 --- a/packages/layout/src/types/circle.ts +++ b/packages/layout/src/types/circle.ts @@ -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 128917f43..8059f5943 100644 --- a/packages/layout/src/types/clip-path.ts +++ b/packages/layout/src/types/clip-path.ts @@ -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 ad6bd6e36..b93432c76 100644 --- a/packages/layout/src/types/defs.ts +++ b/packages/layout/src/types/defs.ts @@ -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 8f2062788..e505dd284 100644 --- a/packages/layout/src/types/document.ts +++ b/packages/layout/src/types/document.ts @@ -77,5 +77,6 @@ export type DocumentNode = { 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 5b99224a7..14d41744d 100644 --- a/packages/layout/src/types/ellipse.ts +++ b/packages/layout/src/types/ellipse.ts @@ -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 be5c9879d..70989027d 100644 --- a/packages/layout/src/types/field-set.ts +++ b/packages/layout/src/types/field-set.ts @@ -23,5 +23,6 @@ export type FieldSetNode = { 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 af7b31a87..1ab586099 100644 --- a/packages/layout/src/types/g.ts +++ b/packages/layout/src/types/g.ts @@ -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 b7d11564b..5318e50ec 100644 --- a/packages/layout/src/types/image-background.ts +++ b/packages/layout/src/types/image-background.ts @@ -67,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 de510b6b9..23c5bf9a1 100644 --- a/packages/layout/src/types/image.ts +++ b/packages/layout/src/types/image.ts @@ -77,4 +77,5 @@ export type ImageNode = { 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 717f0ded7..cf3fb0221 100644 --- a/packages/layout/src/types/line.ts +++ b/packages/layout/src/types/line.ts @@ -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 2dd9511c5..1a74bdb8a 100644 --- a/packages/layout/src/types/linear-gradient.ts +++ b/packages/layout/src/types/linear-gradient.ts @@ -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 bc2ed5cbb..b8f5279ea 100644 --- a/packages/layout/src/types/link.ts +++ b/packages/layout/src/types/link.ts @@ -33,6 +33,7 @@ export type LinkNode = { 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 cfdbba287..714ed99d2 100644 --- a/packages/layout/src/types/marker.ts +++ b/packages/layout/src/types/marker.ts @@ -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 e7d9a0365..8fd3686f6 100644 --- a/packages/layout/src/types/note.ts +++ b/packages/layout/src/types/note.ts @@ -17,5 +17,6 @@ export type NoteNode = { 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 c2cbc6aca..e3601183f 100644 --- a/packages/layout/src/types/page.ts +++ b/packages/layout/src/types/page.ts @@ -122,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 ba16c15fc..aef976078 100644 --- a/packages/layout/src/types/path.ts +++ b/packages/layout/src/types/path.ts @@ -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 0c017cf00..87b75d84f 100644 --- a/packages/layout/src/types/polygon.ts +++ b/packages/layout/src/types/polygon.ts @@ -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 ded0cf718..9de51db37 100644 --- a/packages/layout/src/types/polyline.ts +++ b/packages/layout/src/types/polyline.ts @@ -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 9fe7c0ac5..fbbe628ff 100644 --- a/packages/layout/src/types/radial-gradient.ts +++ b/packages/layout/src/types/radial-gradient.ts @@ -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 2f575615b..490f7f20b 100644 --- a/packages/layout/src/types/rect.ts +++ b/packages/layout/src/types/rect.ts @@ -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 cb971527b..93bc43287 100644 --- a/packages/layout/src/types/select.ts +++ b/packages/layout/src/types/select.ts @@ -24,6 +24,7 @@ export type SelectNode = { export type SafeSelectNode = Omit & { style: SafeStyle; wasSplit: boolean; + stylesResolved?: boolean; }; export type ListNode = { @@ -39,4 +40,5 @@ export type ListNode = { 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 641a86454..0372a6004 100644 --- a/packages/layout/src/types/stop.ts +++ b/packages/layout/src/types/stop.ts @@ -25,4 +25,5 @@ export type StopNode = { 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 ff0db24b0..5079d54c6 100644 --- a/packages/layout/src/types/svg.ts +++ b/packages/layout/src/types/svg.ts @@ -84,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 f9b23e943..d0c1668c1 100644 --- a/packages/layout/src/types/text-input.ts +++ b/packages/layout/src/types/text-input.ts @@ -58,4 +58,5 @@ export type TextInputNode = { 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 898368698..6f3d1c90f 100644 --- a/packages/layout/src/types/text-instance.ts +++ b/packages/layout/src/types/text-instance.ts @@ -11,4 +11,7 @@ export type TextInstanceNode = { 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 1f314f19f..cf3164d66 100644 --- a/packages/layout/src/types/text.ts +++ b/packages/layout/src/types/text.ts @@ -56,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 1df7cac6d..fb8a39926 100644 --- a/packages/layout/src/types/tspan.ts +++ b/packages/layout/src/types/tspan.ts @@ -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 7c421344c..303168934 100644 --- a/packages/layout/src/types/view.ts +++ b/packages/layout/src/types/view.ts @@ -52,6 +52,7 @@ export type ViewNode = { export type SafeViewNode = Omit & { style: SafeStyle; wasSplit: boolean; + stylesResolved?: boolean; children?: ( | SafeViewNode | SafeImageNode 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/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', From 52b3f2c9cddff4c61fb472489c400322df70d66e Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Wed, 23 Sep 2026 00:26:02 +0200 Subject: [PATCH 10/11] fix(layout): re-layout text whose lines were all truncated at an intermediate size Taffy measures a flex-basis: 0 text at its padding-only width with the previously measured height as known height. Re-breaking there truncates every line away, and an empty lines array was then trusted as final, so the cell rendered empty. Empty lines now trigger a fresh layout (split fragments excepted), and measurement heights get the same f32 tolerance as widths so a rounding error cannot drop the last line. Co-Authored-By: Claude Fable 5.1 --- packages/layout/src/text/measureText.ts | 15 ++++++++++++--- packages/layout/tests/text/measureText.test.ts | 13 +++++++++++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/layout/src/text/measureText.ts b/packages/layout/src/text/measureText.ts index 4d6aafd71..48c858756 100644 --- a/packages/layout/src/text/measureText.ts +++ b/packages/layout/src/text/measureText.ts @@ -35,6 +35,9 @@ const fits = (node: SafeTextNode, width: number) => { // 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; @@ -96,9 +99,15 @@ const measureText = if (width <= 0) return { width: 0, height: 0 }; if (!fits(node, width)) { - // Widths arrive f32-rounded from Taffy; the tolerance keeps a line that - // measured as fitting from being broken again for a rounding error. - node.lines = layoutText(node, width + EPSILON, height, fontStore); + // 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 { diff --git a/packages/layout/tests/text/measureText.test.ts b/packages/layout/tests/text/measureText.test.ts index ec4f614ea..d17b6475b 100644 --- a/packages/layout/tests/text/measureText.test.ts +++ b/packages/layout/tests/text/measureText.test.ts @@ -127,6 +127,19 @@ describe('measureText', () => { 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); From 990fa26a5b0915da2742924dcb14964ded86e502 Mon Sep 17 00:00:00 2001 From: carlobeltrame Date: Wed, 23 Sep 2026 11:38:09 +0200 Subject: [PATCH 11/11] refactor(layout): drop the overflow: scroll mapping PDFs cannot scroll, the stylesheet types only allow overflow: hidden, and the renderer only clips on hidden. Mapping scroll to Taffy was dead code. Co-Authored-By: Claude Opus 5.5 --- packages/layout/src/node/toTaffyStyle.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/layout/src/node/toTaffyStyle.ts b/packages/layout/src/node/toTaffyStyle.ts index a828a7a83..e35edb9a9 100644 --- a/packages/layout/src/node/toTaffyStyle.ts +++ b/packages/layout/src/node/toTaffyStyle.ts @@ -88,7 +88,6 @@ const FLEX_WRAP: Record = { const OVERFLOW: Record = { hidden: Overflow.Hidden, - scroll: Overflow.Scroll, }; /**