├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── VISION.md ├── dangerfile.ts ├── package.json ├── simple.svg ├── src ├── _tests │ ├── Arimo │ │ ├── Arimo-Bold.ttf │ │ ├── Arimo-BoldItalic.ttf │ │ ├── Arimo-Italic.ttf │ │ ├── Arimo-Regular.ttf │ │ └── LICENSE.txt │ ├── __fixtures__ │ │ ├── artwork-grid.json │ │ └── welcome.json │ ├── __snapshots__ │ │ ├── _component-to-node.test.ts.snap │ │ ├── _node-instance-count.test.tsx-counting-nodes-it-is-good-with-memory.svg │ │ ├── _node-to-svg.test.ts.snap │ │ ├── _tree-to-svg-sub-funcs.test.ts.snap │ │ ├── _tree-to-svg.test.ts.snap │ │ ├── render.test.tsx-handles-some-simple-jsx.svg │ │ └── render.test.tsx.snap │ ├── _component-to-node.test.ts │ ├── _node-instance-count.test.tsx │ ├── _node-to-svg.test.ts │ ├── _tree-to-svg-sub-funcs.test.ts │ ├── _tree-to-svg.test.ts │ ├── example_layouts │ │ ├── __snapshots__ │ │ │ ├── _align-items.test.tsx-renders-three-vertically-horizontally-centeredblocks.svg │ │ │ ├── _borders.test.tsx-border-radius-larger-than-height.svg │ │ │ ├── _borders.test.tsx-border-radius-larger-than-width.svg │ │ │ ├── _borders.test.tsx-no-border-radius.svg │ │ │ ├── _borders.test.tsx-small-border-radius.svg │ │ │ ├── _borders.test.tsx-varying-border-colors,-radii-and-widths.svg │ │ │ ├── _borders.test.tsx-varying-border-colors-and-widths.svg │ │ │ ├── _borders.test.tsx-varying-border-colors-dashed.svg │ │ │ ├── _borders.test.tsx-varying-border-colors-dotted.svg │ │ │ ├── _borders.test.tsx-varying-border-colors.svg │ │ │ ├── _borders.test.tsx-varying-border-radii-dashed.svg │ │ │ ├── _borders.test.tsx-varying-border-radii-dotted.svg │ │ │ ├── _borders.test.tsx-varying-border-radii.svg │ │ │ ├── _borders.test.tsx-varying-border-widths.svg │ │ │ ├── _flex-direction.test.tsx-renders-three-blocks-in-a-row.svg │ │ │ ├── _justify-contents.test.tsx-splits-the-layout-vertically-across.svg │ │ │ ├── _position.test.tsx-renders-views-on-bottom-position.svg │ │ │ ├── _position.test.tsx-renders-views-on-left-position.svg │ │ │ ├── _position.test.tsx-renders-views-on-top-position.svg │ │ │ ├── _text.test.tsx-does-not-break-if-a-text-tag-is-empty.svg │ │ │ ├── _text.test.tsx-renders-a-line-of-text.svg │ │ │ ├── _text.test.tsx-renders-multiple-lines-of-text-with-multiple-styles.svg │ │ │ ├── _text.test.tsx-renders-multiple-lines-of-text-with-text-align.svg │ │ │ ├── _text.test.tsx-renders-multiple-lines-of-text.svg │ │ │ ├── _text.test.tsx-renders-text-in-the-correct-place-when-positioned-with-flex.svg │ │ │ ├── _text.test.tsx-renders-text-using-the-specified-default-font.svg │ │ │ ├── _text.test.tsx-renders-text-with-different-colors.svg │ │ │ ├── _text.test.tsx-renders-text-with-different-sizes.svg │ │ │ ├── _text.test.tsx-renders-text-with-forced-break.svg │ │ │ ├── _wireframe.test.tsx-nested-wireframe.svg │ │ │ ├── _wireframe.test.tsx-simple-wireframe-with-background-color.svg │ │ │ ├── _wireframe.test.tsx-simple-wireframe-with-border-radius.svg │ │ │ └── _wireframe.test.tsx-simple-wireframe.svg │ │ ├── _align-items.test.tsx │ │ ├── _borders.test.tsx │ │ ├── _flex-direction.test.tsx │ │ ├── _justify-contents.test.tsx │ │ ├── _position.test.tsx │ │ ├── _text.test.tsx │ │ └── _wireframe.test.tsx │ └── render.test.tsx ├── ambient.d.ts ├── component-to-node.ts ├── component-tree-to-nodes.ts ├── extract-text.ts ├── flatten-styles.ts ├── font-loader.ts ├── font │ └── proza-libre │ │ ├── ProzaLibre-Bold.ttf │ │ ├── ProzaLibre-BoldItalic.ttf │ │ ├── ProzaLibre-ExtraBold.ttf │ │ ├── ProzaLibre-ExtraBoldItalic.ttf │ │ ├── ProzaLibre-Italic.ttf │ │ ├── ProzaLibre-Light.ttf │ │ ├── ProzaLibre-LightItalic.ttf │ │ ├── ProzaLibre-Medium.ttf │ │ ├── ProzaLibre-MediumItalic.ttf │ │ ├── ProzaLibre-Regular.ttf │ │ ├── ProzaLibre-SemiBold.ttf │ │ ├── ProzaLibre-SemiBoldItalic.ttf │ │ └── SIL Open Font License.txt ├── index.ts ├── node-to-svg.ts ├── reapply-layouts-to-components.ts ├── svg │ ├── Text.ts │ ├── View.ts │ ├── ViewWireframe.ts │ ├── borders.ts │ └── util.ts ├── text-layout.ts ├── tree-to-svg.ts └── whitespace.ts ├── tsconfig.json ├── tslint.json ├── web └── screenshot.png └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | 7 | # Coverage directory used by tools like istanbul 8 | coverage 9 | 10 | # nyc test coverage 11 | .nyc_output 12 | 13 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 14 | .grunt 15 | 16 | # node-waf configuration 17 | .lock-wscript 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directories 23 | node_modules 24 | jspm_packages 25 | 26 | # Optional npm cache directory 27 | .npm 28 | 29 | # Optional REPL history 30 | .node_repl_history 31 | 32 | # Jest cache 33 | .jest/ 34 | 35 | # TS build files 36 | build 37 | 38 | error_log.txt 39 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src 2 | build/_tests 3 | build/src 4 | .vscode 5 | .babelrc 6 | .travis.yml 7 | dangerfile.ts 8 | tsconfig.json 9 | tslint.json 10 | yarn.lock 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: cpp 2 | compiler: gcc 3 | dist: trusty 4 | 5 | before_install: 6 | # C++14 7 | - sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test 8 | - sudo apt-get update -qq 9 | # Node JS 10 | - curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - 11 | # Yarn 12 | - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - 13 | - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list 14 | 15 | 16 | install: 17 | # C++14 18 | - sudo apt-get install -qq g++-6 19 | - sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-6 90 20 | # Node 21 | - sudo apt-get install -y nodejs 22 | # Yarn 23 | - sudo apt-get update && sudo apt-get install yarn 24 | # Deps 25 | - yarn install 26 | 27 | 28 | cache: 29 | yarn: true 30 | directories: 31 | - node_modules 32 | - .jest 33 | 34 | script: 35 | - yarn lint 36 | - yarn jest 37 | - yarn danger 38 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/.git": true, 4 | "**/.DS_Store": true, 5 | "**/.jest": true, 6 | "**/node_modules": true, 7 | "**/build": true 8 | }, 9 | "prettier.semi": false, 10 | "prettier.printWidth": 120, 11 | "prettier.singleQuote": false 12 | } 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Master 2 | 3 | ### 0.1.0 4 | 5 | * Adds the ability to set a default font [dawnmist] 6 | * Improved text position rendering [jacobp100] 7 | 8 | ### 0.0.24 9 | 10 | * Fix style property number `0` not set - [jhen0409] 11 | 12 | ### 0.0.22 - 0.0.23 13 | 14 | * Add support for `EDGE_ALL` of margin / padding / borderWidth - [jhen0409] 15 | 16 | ### 0.0.21 17 | 18 | * fixes for jest 21.x - [orta] 19 | 20 | ### 0.0.20 21 | 22 | Initial working version 23 | 24 | - Handles a lot of flexbox 25 | - Handles text layouts inside the SVGs 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 and Artsy Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jest-snapshots-svg 2 | 3 | Take a React Native component tree, and render it into an SVG. 4 | 5 | ```ts 6 | // _tests/render.test.tsx 7 | 8 | import * as React from "react" 9 | import { Text } from "react-native" 10 | import * as renderer from "react-test-renderer" 11 | import "jest-snapshots-svg" 12 | 13 | describe("Fixtures", () => { 14 | it("does some simple JSX", () => { 15 | const component = renderer.create().toJSON() 16 | expect(component).toMatchSnapshot() 17 | expect(component).toMatchSVGSnapshot(480, 640) 18 | }) 19 | }) 20 | ``` 21 | 22 | Would make: 23 | 24 | ```sh 25 | src/_tests/ 26 | ├── __snapshots__ 27 | │   ├── render.test.tsx.snap 28 | │ └── render.test.tsx-does-some-simple-jsx.svg 29 | └── render.test.tsx 30 | ``` 31 | 32 | It does this by emulating the rendering process of React Native by calling yoga-layout directly in your tests, then converting the output of the layout-pass into SVG items that can easy be previewed in GitHub. 33 | 34 | 👍 35 | 36 | ## What does this look like in principal? 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 72 | 73 | 74 | 75 | 76 | 77 | 90 | 91 |
Your codeThe final SVG

Write your normal Jest snapshot tests, but use toMatchSVGSnapshot

45 | 46 | import * as React from "react" 47 | import { View } from "react-native" 48 | import * as renderer from "react-test-renderer" 49 | import "jest-snapshots-svg" 50 | 51 | const squareStyle = (color) => 52 | ({ width: 50, height: 50, backgroundColor: color }) 53 | 54 | it("Renders three centered blocks", () => { 55 | const jsx = 56 | 62 | 63 | 64 | 65 | 66 | 67 | const component = renderer.create(jsx).toJSON() 68 | expect(component).toMatchSVGSnapshot(320, 480) 69 | }) 70 | 71 |
Then you run your tests. yarn jest.

Then you get SVG output in the __snapshots__ folder. Example

78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
92 | 93 | ### Fonts 94 | 95 | If you use `` elements, you must have access to the font files so we can lay the text out. 96 | Usually, this just means having the font installed. However, if this goes wrong, this can be done 97 | manually via the `loadFont` function, where you pass in the font file as a buffer. If you need a 98 | fallback, you can use `addFontFallback`. If you just want to be able to specify the default 99 | fontFamily to use, you can use `setDefaultFont`. 100 | 101 | ```js 102 | import { addFontFallback, loadFont, setDefaultFont } from "jest-snapshots-svg" 103 | 104 | setDefaultFont("DejaVu Sans") 105 | loadFont(fs.readFileSync("your-font-file.ttf")) 106 | addFontFallback("Your Font", "'Helvetica', 'Arial', sans-serif") 107 | ``` 108 | 109 | This should be able to determine the `fontFamily`, `fontWeight`, and `fontStyle`. However, if it's 110 | wrong, or it failed, you can pass these in as a second argument. 111 | 112 | ```js 113 | loadFont(fs.readFileSync("your-font-file.ttf"), { 114 | fontFamily: "Helvetica", 115 | fontWeight: "normal", 116 | fontStyle: "normal" 117 | }) 118 | ``` 119 | 120 | If you have a `.ttc` file (a collection of multiple files), and it fails to correctly guess the font 121 | style parameters, you can provide a `postscriptName` in the style object to target a specific font. 122 | Do this in combination with passing in the font style arguments. See more about this over at 123 | [fontkit](https://github.com/devongovett/fontkit#api). 124 | 125 | ## Adding relative font path definitions to the output svg 126 | 127 | An alternative way to load a font is to use the `addFontToSvg` function instead of the normal `loadFont`. This takes as input the path (can be a node_modules import reference) to load the font from, plus optionally the normal font style information that loadFont takes. Loading the font this way means that we can keep a reference to the file path that the font was loaded from, and then embed the path to the font into the svg in a css style definition. 128 | 129 | ```js 130 | addFontToSvg("@expo/vector-icons/fonts/FontAwesome.ttf") 131 | ``` 132 | 133 | Any fonts loaded using `addFontToSvg` will be checked against the list of actual fonts used in Text styles in the component. Only those that have actually been used in the rendered component will be output into the svg. 134 | 135 | ### Using relative font paths with react-native-vector-icons and @expo/vector-icons 136 | 137 | In order for the above to work with the vector icons, you will need to create a test mock file for these packages. The default output from react-test-renderer does not include the actual character codes to display, which means that it cannot be captured from the rendered component to add to the svg. The file should be added to the `__mocks__` directory _next to_ the node_modules directory, and should have the same name/path as the module it is replacing. 138 | 139 | For a typical project with the node_modules directory in the project root directory you should create a file `/__mocks__/@expo/vector-icons.js` (or ts if you are using typescript) for mocking @expo/vector-icons. For react-native-vector-icons the mock file should be `/__mocks__/react-native-vector-icons.js`. 140 | 141 | In the mock file, you need to set the render function to return a Text component, with the font style details set to match the icon font and the text itself set to the unicode reference for the particular character that should be displayed for that icon. 142 | 143 | An example mock for @expo/vector-icons could be (written in typescript): 144 | 145 | ```js 146 | import * as fs from "fs" 147 | import { addFontToSvg } from "jest-snapshots-svg" 148 | import * as React from "react" 149 | import { StyleProp, StyleSheet, Text, TextStyle } from "react-native" 150 | import { IconProps } from "react-native-vector-icons/Icon" 151 | 152 | const glyphmapDir = "@expo/vector-icons/vendor/react-native-vector-icons/glyphmaps" 153 | const fontDir = "@expo/vector-icons/fonts" 154 | 155 | export const createIconSet = ( 156 | glyphMap: { [name: string]: string | number }, 157 | fontFamily: string, 158 | fontFile?: string) => 159 | { 160 | const filename = require.resolve(`${fontDir}/${fontFamily}.ttf`) 161 | addFontToSvg(filename, { fontFamily, fontStyle: "normal", fontWeight: "normal" }) 162 | 163 | class Icon extends React.Component { 164 | static loadFont() { 165 | console.log("called Icon:loadFont()") 166 | } 167 | static getImageSource() { 168 | console.log("called Icon.getImageSource()") 169 | } 170 | 171 | render() { 172 | const { name, size, color, style, ...props } = this.props 173 | 174 | let glyph = name ? glyphMap[name] : undefined 175 | if (typeof glyph === "number") { 176 | glyph = String.fromCodePoint(glyph); 177 | } 178 | 179 | const styleDefaults: TextStyle = { 180 | fontFamily, 181 | fontSize: size, 182 | fontWeight: "normal", 183 | fontStyle: "normal", 184 | color 185 | } 186 | 187 | return ( 188 | 189 | {`&#x${glyph 190 | .codePointAt(0) 191 | .toString(16) 192 | .toUpperCase()};`} 193 | 194 | ); 195 | } 196 | } 197 | 198 | return Icon 199 | } 200 | 201 | export const createIconSetFromFontello = jest.fn() 202 | export const createIconSetFromIcoMoon = jest.fn() 203 | 204 | // If you know you're only going to use a few/one of these fonts, you can safely 205 | // remove any you won't use. 206 | export const Entypo = createIconSet( 207 | require(`${glyphmapDir}/Entypo.json`), 208 | "Entypo" 209 | ) 210 | export const EvilIcons = createIconSet( 211 | require(`${glyphmapDir}/EvilIcons.json`), 212 | "EvilIcons" 213 | ) 214 | export const Feather = createIconSet( 215 | require(`${glyphmapDir}/Feather.json`), 216 | "Feather" 217 | ) 218 | export const FontAwesome = createIconSet( 219 | require(`${glyphmapDir}/FontAwesome.json`), 220 | "FontAwesome" 221 | ) 222 | export const Foundation = createIconSet( 223 | require(`${glyphmapDir}/Foundation.json`), 224 | "Foundation" 225 | ) 226 | export const Ionicons = createIconSet( 227 | require(`${glyphmapDir}/Ionicons.json`), 228 | "Ionicons" 229 | ) 230 | export const MaterialCommunityIcons = createIconSet( 231 | require(`${glyphmapDir}/MaterialCommunityIcons.json`), 232 | "MaterialCommunityIcons" 233 | ) 234 | export const MaterialIcons = createIconSet( 235 | require(`${glyphmapDir}/MaterialIcons.json`), 236 | "MaterialIcons" 237 | ) 238 | export const Octicons = createIconSet( 239 | require(`${glyphmapDir}/Octicons.json`), 240 | "Octicons" 241 | ) 242 | export const SimpleLineIcons = createIconSet( 243 | require(`${glyphmapDir}/SimpleLineIcons.json`), 244 | "SimpleLineIcons" 245 | ) 246 | export const Zocial = createIconSet( 247 | require(`${glyphmapDir}/Zocial.json`), 248 | "Zocial" 249 | ) 250 | ``` 251 | 252 | For react-native-vector-icons, change the `glyphmapDir` and `fontDir` paths: 253 | 254 | ```js 255 | const glyphmapDir = "react-native-vector-icons/glyphmaps" 256 | const fontDir = "react-native-vector-icons/Fonts" 257 | ``` 258 | 259 | ### Flaws 260 | 261 | This is definitely pre-1.0, we only have it working on a few tests in [artsy/emission](https://github.com/artsy/emission/). Expect alpha quality style snapshots for a while, but more people working on it will mean we all get a better chance at it working out well. 262 | 263 | * Doesn't render image - see [#18](https://github.com/jest-community/jest-snapshots-svg/issues/18) 264 | * Not all flexbox attributes are supported - see [#19](https://github.com/jest-community/jest-snapshots-svg/issues/19) 265 | 266 | ### I want to work on this 267 | 268 | OK, you need to clone this repo: 269 | 270 | ```sh 271 | git clone https://github.com/jest-community/jest-snapshots-svg.git 272 | ``` 273 | 274 | There's the usual stuff, `yarn test` and `yarn lint`. 275 | 276 | If you want to work against your own projects, then you need to set it up for linking and turn on watch mode. 277 | 278 | ```sh 279 | yarn watch # starts a server, so make a new tab for the next bits 280 | yarn link 281 | 282 | cd [my_project] 283 | yarn link jest-snapshot-svg 284 | ``` 285 | 286 | Now your project is using the dev version of this. 287 | 288 | ## TODO: 289 | 290 | - **v0-0.5:** make it work 291 | - **v0.5-1:** make it good 292 | - **v1:** figure out how/if it should end up in jest 293 | - **v2:** use iTerm to show the images inline 294 | - **v3:** get vscode-jest to preview them inline 295 | -------------------------------------------------------------------------------- /VISION.md: -------------------------------------------------------------------------------- 1 | I want to be able to generate _reasonable_ visual layout snapshots. Because when we get bugs at Artsy, they tend to be layout driven regressions, on iOS this [was easy to handle](https://github.com/artsy/eigen/tree/master/Artsy_Tests/ReferenceImages) and we made a [few](https://cocoapods.org/pods/Expecta+Snapshots) [libraries](https://cocoapods.org/pods/Nimble-Snapshots) to make it feel good. 2 | 3 | Moving to React Native gave us the chance to have fast tests that are run out of process, this is really cool. However, high level, pixel-accurate representations of the layouts can only be done on a simulator, meaning they can't be fast. So, this project aims to provide a *good enough* representation of a React Native tree of components that you can feel some security that making UI changes in one platform does not affect layouts in others. 4 | 5 | 6 | 7 | 8 | --- 9 | 10 | This project came from this [original issue](https://github.com/artsy/emission/issues/442). 11 | -------------------------------------------------------------------------------- /dangerfile.ts: -------------------------------------------------------------------------------- 1 | import { danger, fail, warn } from "danger" 2 | import * as fs from "fs" 3 | 4 | // Setup 5 | const pr = danger.github.pr 6 | const modified = danger.git.modified_files 7 | const newFiles = danger.git.created_files 8 | const bodyAndTitle = (pr.body + pr.title).toLowerCase() 9 | 10 | // Custom modifiers for people submitting PRs to be able to say "skip this" 11 | const trivialPR = bodyAndTitle.includes("trivial") 12 | const acceptedNoTests = bodyAndTitle.includes("skip new tests") 13 | 14 | const filesOnly = (file: string) => fs.existsSync(file) && fs.lstatSync(file).isFile() 15 | 16 | // Custom subsets of known files 17 | const modifiedAppFiles = modified.filter(p => p.includes("lib/")).filter(p => filesOnly(p)) 18 | const modifiedTestFiles = modified.filter(p => p.includes("_tests")).filter(p => filesOnly(p)) 19 | 20 | // When there are app-changes and it's not a PR marked as trivial, expect 21 | // there to be CHANGELOG changes. 22 | const changelogChanges = modified.includes("CHANGELOG.md") 23 | if (modifiedAppFiles.length > 0 && !trivialPR && !changelogChanges) { 24 | fail("No CHANGELOG added.") 25 | } 26 | 27 | // No PR is too small to warrant a paragraph or two of summary 28 | if (pr.body.length === 0) { 29 | fail("Please add a description to your PR.") 30 | } 31 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-snapshots-svg", 3 | "version": "0.1.0", 4 | "description": "Generate SVG Snapshots of React Native Component Trees", 5 | "main": "build/index.js", 6 | "files": [ 7 | "build/*" 8 | ], 9 | "typings": "build/index.d.ts", 10 | "author": "Orta Therox & Art.sy Inc", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "@types/jest": "^20.0.8", 14 | "@types/node": "^7.0.31", 15 | "babel-cli": "^6.24.1", 16 | "babel-jest": "^20.0.3", 17 | "danger": "^0.18.0", 18 | "husky": "^0.13.3", 19 | "jest": "^21.0.1", 20 | "lint-staged": "^3.2.5", 21 | "prop-types": "^15.5.10", 22 | "react": "16.0.0-alpha.6", 23 | "react-native": "^0.44.0", 24 | "react-test-renderer": "^15.5.4", 25 | "ts-jest": "^21.0.0", 26 | "ts-node": "^3.0.0", 27 | "tslint": "^5.2.0", 28 | "typescript": "^2.3.2" 29 | }, 30 | "scripts": { 31 | "test": "jest", 32 | "type-check": "tsc --noEmit", 33 | "build": "tsc && cp -r src/font build", 34 | "watch": "tsc --watch", 35 | "lint": "tslint 'src/**/*.{ts,tsx}'", 36 | "precommit": "lint-staged", 37 | "prepush": "yarn build && yarn jest" 38 | }, 39 | "lint-staged": { 40 | "*.@(ts|tsx)": [ 41 | "tslint --fix", 42 | "git add" 43 | ] 44 | }, 45 | "jest": { 46 | "moduleFileExtensions": [ 47 | "ts", 48 | "tsx", 49 | "js", 50 | "node" 51 | ], 52 | "transform": { 53 | "^.+\\.js$": "/node_modules/babel-jest", 54 | ".(ts|tsx)": "/node_modules/ts-jest/preprocessor.js" 55 | }, 56 | "testRegex": "(.test)\\.(ts|tsx)$", 57 | "testPathIgnorePatterns": [ 58 | "\\.snap$", 59 | "/node_modules/" 60 | ], 61 | "cacheDirectory": ".jest/cache", 62 | "preset": "react-native" 63 | }, 64 | "dependencies": { 65 | "font-manager": "^0.2.2", 66 | "fontkit": "^1.7.7", 67 | "linebreak": "^0.3.0", 68 | "nbind": "^0.3.14", 69 | "yoga-layout": "^1.9.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /simple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/_tests/Arimo/Arimo-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/jest-snapshots-svg/b647082cc5b67bb21a187749f0840755a840ca54/src/_tests/Arimo/Arimo-Bold.ttf -------------------------------------------------------------------------------- /src/_tests/Arimo/Arimo-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/jest-snapshots-svg/b647082cc5b67bb21a187749f0840755a840ca54/src/_tests/Arimo/Arimo-BoldItalic.ttf -------------------------------------------------------------------------------- /src/_tests/Arimo/Arimo-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/jest-snapshots-svg/b647082cc5b67bb21a187749f0840755a840ca54/src/_tests/Arimo/Arimo-Italic.ttf -------------------------------------------------------------------------------- /src/_tests/Arimo/Arimo-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/jest-snapshots-svg/b647082cc5b67bb21a187749f0840755a840ca54/src/_tests/Arimo/Arimo-Regular.ttf -------------------------------------------------------------------------------- /src/_tests/Arimo/LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /src/_tests/__fixtures__/artwork-grid.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "View", 3 | "props": {}, 4 | "children": [ 5 | { 6 | "type": "View", 7 | "props": { 8 | "style": { 9 | "flexDirection": "row" 10 | }, 11 | "accessibilityLabel": "Artworks Content View" 12 | }, 13 | "children": [ 14 | { 15 | "type": "View", 16 | "props": { 17 | "style": [ 18 | { 19 | "flexDirection": "column" 20 | }, 21 | { 22 | "width": 242.66666666666666, 23 | "marginRight": 20 24 | } 25 | ], 26 | "accessibilityLabel": "Section 0" 27 | }, 28 | "children": [ 29 | { 30 | "type": "View", 31 | "props": { 32 | "accessible": true 33 | }, 34 | "children": [ 35 | { 36 | "type": "AROpaqueImageView", 37 | "props": { 38 | "style": { 39 | "marginBottom": 10 40 | }, 41 | "aspectRatio": 2.18, 42 | "imageURL": "artsy.net/image-url" 43 | }, 44 | "children": null 45 | }, 46 | { 47 | "type": "Text", 48 | "props": { 49 | "style": [ 50 | { 51 | "fontSize": 17 52 | }, 53 | [ 54 | { 55 | "fontSize": 12, 56 | "color": "#666666" 57 | }, 58 | { 59 | "fontWeight": "bold" 60 | } 61 | ], 62 | { 63 | "fontFamily": "AGaramondPro-Regular" 64 | } 65 | ], 66 | "numberOfLines": 1, 67 | "accessible": true, 68 | "allowFontScaling": true, 69 | "ellipsizeMode": "tail" 70 | }, 71 | "children": [ 72 | "Guerrilla Girls" 73 | ] 74 | }, 75 | { 76 | "type": "Text", 77 | "props": { 78 | "style": [ 79 | { 80 | "fontSize": 17 81 | }, 82 | { 83 | "fontSize": 12, 84 | "color": "#666666" 85 | }, 86 | { 87 | "fontFamily": "AGaramondPro-Regular" 88 | } 89 | ], 90 | "numberOfLines": 1, 91 | "accessible": true, 92 | "allowFontScaling": true, 93 | "ellipsizeMode": "tail" 94 | }, 95 | "children": [ 96 | { 97 | "type": "Text", 98 | "props": { 99 | "style": [ 100 | { 101 | "fontSize": 17 102 | }, 103 | [ 104 | { 105 | "fontSize": 12, 106 | "color": "#666666" 107 | }, 108 | { 109 | "fontStyle": "italic" 110 | } 111 | ], 112 | { 113 | "fontFamily": "AGaramondPro-Regular" 114 | } 115 | ], 116 | "numberOfLines": 1, 117 | "accessible": true, 118 | "allowFontScaling": true, 119 | "ellipsizeMode": "tail" 120 | }, 121 | "children": [ 122 | "DO WOMEN STILL HAVE TO BE NAKED TO GET INTO THE MET. MUSEUM" 123 | ] 124 | }, 125 | ", 2012" 126 | ] 127 | }, 128 | { 129 | "type": "Text", 130 | "props": { 131 | "style": [ 132 | { 133 | "fontSize": 17 134 | }, 135 | { 136 | "fontSize": 12, 137 | "color": "#666666" 138 | }, 139 | { 140 | "fontFamily": "AGaramondPro-Regular" 141 | } 142 | ], 143 | "numberOfLines": 1, 144 | "accessible": true, 145 | "allowFontScaling": true, 146 | "ellipsizeMode": "tail" 147 | }, 148 | "children": [ 149 | "Whitechapel Gallery" 150 | ] 151 | } 152 | ] 153 | } 154 | ] 155 | }, 156 | { 157 | "type": "View", 158 | "props": { 159 | "style": [ 160 | { 161 | "flexDirection": "column" 162 | }, 163 | { 164 | "width": 242.66666666666666, 165 | "marginRight": 20 166 | } 167 | ], 168 | "accessibilityLabel": "Section 1" 169 | }, 170 | "children": [ 171 | { 172 | "type": "View", 173 | "props": { 174 | "accessible": true 175 | }, 176 | "children": [ 177 | { 178 | "type": "AROpaqueImageView", 179 | "props": { 180 | "style": { 181 | "marginBottom": 10 182 | }, 183 | "aspectRatio": 2.18, 184 | "imageURL": "artsy.net/image-url" 185 | }, 186 | "children": null 187 | }, 188 | { 189 | "type": "Text", 190 | "props": { 191 | "style": [ 192 | { 193 | "fontSize": 17 194 | }, 195 | [ 196 | { 197 | "fontSize": 12, 198 | "color": "#666666" 199 | }, 200 | { 201 | "fontWeight": "bold" 202 | } 203 | ], 204 | { 205 | "fontFamily": "AGaramondPro-Regular" 206 | } 207 | ], 208 | "numberOfLines": 1, 209 | "accessible": true, 210 | "allowFontScaling": true, 211 | "ellipsizeMode": "tail" 212 | }, 213 | "children": [ 214 | "Guerrilla Girls" 215 | ] 216 | }, 217 | { 218 | "type": "Text", 219 | "props": { 220 | "style": [ 221 | { 222 | "fontSize": 17 223 | }, 224 | { 225 | "fontSize": 12, 226 | "color": "#666666" 227 | }, 228 | { 229 | "fontFamily": "AGaramondPro-Regular" 230 | } 231 | ], 232 | "numberOfLines": 1, 233 | "accessible": true, 234 | "allowFontScaling": true, 235 | "ellipsizeMode": "tail" 236 | }, 237 | "children": [ 238 | { 239 | "type": "Text", 240 | "props": { 241 | "style": [ 242 | { 243 | "fontSize": 17 244 | }, 245 | [ 246 | { 247 | "fontSize": 12, 248 | "color": "#666666" 249 | }, 250 | { 251 | "fontStyle": "italic" 252 | } 253 | ], 254 | { 255 | "fontFamily": "AGaramondPro-Regular" 256 | } 257 | ], 258 | "numberOfLines": 1, 259 | "accessible": true, 260 | "allowFontScaling": true, 261 | "ellipsizeMode": "tail" 262 | }, 263 | "children": [ 264 | "DO WOMEN STILL HAVE TO BE NAKED TO GET INTO THE MET. MUSEUM" 265 | ] 266 | }, 267 | ", 2012" 268 | ] 269 | }, 270 | { 271 | "type": "Text", 272 | "props": { 273 | "style": [ 274 | { 275 | "fontSize": 17 276 | }, 277 | { 278 | "fontSize": 12, 279 | "color": "#666666" 280 | }, 281 | { 282 | "fontFamily": "AGaramondPro-Regular" 283 | } 284 | ], 285 | "numberOfLines": 1, 286 | "accessible": true, 287 | "allowFontScaling": true, 288 | "ellipsizeMode": "tail" 289 | }, 290 | "children": [ 291 | "Whitechapel Gallery" 292 | ] 293 | } 294 | ] 295 | } 296 | ] 297 | }, 298 | { 299 | "type": "View", 300 | "props": { 301 | "style": [ 302 | { 303 | "flexDirection": "column" 304 | }, 305 | { 306 | "width": 242.66666666666666, 307 | "marginRight": 0 308 | } 309 | ], 310 | "accessibilityLabel": "Section 2" 311 | }, 312 | "children": [ 313 | { 314 | "type": "View", 315 | "props": { 316 | "accessible": true 317 | }, 318 | "children": [ 319 | { 320 | "type": "AROpaqueImageView", 321 | "props": { 322 | "style": { 323 | "marginBottom": 10 324 | }, 325 | "aspectRatio": 2.18, 326 | "imageURL": "artsy.net/image-url" 327 | }, 328 | "children": null 329 | }, 330 | { 331 | "type": "Text", 332 | "props": { 333 | "style": [ 334 | { 335 | "fontSize": 17 336 | }, 337 | [ 338 | { 339 | "fontSize": 12, 340 | "color": "#666666" 341 | }, 342 | { 343 | "fontWeight": "bold" 344 | } 345 | ], 346 | { 347 | "fontFamily": "AGaramondPro-Regular" 348 | } 349 | ], 350 | "numberOfLines": 1, 351 | "accessible": true, 352 | "allowFontScaling": true, 353 | "ellipsizeMode": "tail" 354 | }, 355 | "children": [ 356 | "Guerrilla Girls" 357 | ] 358 | }, 359 | { 360 | "type": "Text", 361 | "props": { 362 | "style": [ 363 | { 364 | "fontSize": 17 365 | }, 366 | { 367 | "fontSize": 12, 368 | "color": "#666666" 369 | }, 370 | { 371 | "fontFamily": "AGaramondPro-Regular" 372 | } 373 | ], 374 | "numberOfLines": 1, 375 | "accessible": true, 376 | "allowFontScaling": true, 377 | "ellipsizeMode": "tail" 378 | }, 379 | "children": [ 380 | { 381 | "type": "Text", 382 | "props": { 383 | "style": [ 384 | { 385 | "fontSize": 17 386 | }, 387 | [ 388 | { 389 | "fontSize": 12, 390 | "color": "#666666" 391 | }, 392 | { 393 | "fontStyle": "italic" 394 | } 395 | ], 396 | { 397 | "fontFamily": "AGaramondPro-Regular" 398 | } 399 | ], 400 | "numberOfLines": 1, 401 | "accessible": true, 402 | "allowFontScaling": true, 403 | "ellipsizeMode": "tail" 404 | }, 405 | "children": [ 406 | "DO WOMEN STILL HAVE TO BE NAKED TO GET INTO THE MET. MUSEUM" 407 | ] 408 | }, 409 | ", 2012" 410 | ] 411 | }, 412 | { 413 | "type": "Text", 414 | "props": { 415 | "style": [ 416 | { 417 | "fontSize": 17 418 | }, 419 | { 420 | "fontSize": 12, 421 | "color": "#666666" 422 | }, 423 | { 424 | "fontFamily": "AGaramondPro-Regular" 425 | } 426 | ], 427 | "numberOfLines": 1, 428 | "accessible": true, 429 | "allowFontScaling": true, 430 | "ellipsizeMode": "tail" 431 | }, 432 | "children": [ 433 | "Whitechapel Gallery" 434 | ] 435 | } 436 | ] 437 | } 438 | ] 439 | } 440 | ] 441 | } 442 | ] 443 | } 444 | -------------------------------------------------------------------------------- /src/_tests/__fixtures__/welcome.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "RCTScrollView", 3 | "props": { 4 | "style": { 5 | "backgroundColor": "black" 6 | } 7 | }, 8 | "children": [ 9 | { 10 | "type": "View", 11 | "props": {}, 12 | "children": [ 13 | { 14 | "type": "View", 15 | "props": { 16 | "style": { 17 | "flex": 1, 18 | "paddingTop": 40, 19 | "alignItems": "center" 20 | } 21 | }, 22 | "children": [ 23 | { 24 | "type": "Text", 25 | "props": { 26 | "style": [ 27 | { 28 | "fontSize": 30, 29 | "color": "white", 30 | "textAlign": "center", 31 | "paddingLeft": 20, 32 | "paddingRight": 20 33 | }, 34 | {}, 35 | { 36 | "fontFamily": "AGaramondPro-Regular" 37 | } 38 | ], 39 | "accessible": true, 40 | "allowFontScaling": true, 41 | "ellipsizeMode": "tail" 42 | }, 43 | "children": [ 44 | "Sell works from your collection through our partner network" 45 | ] 46 | }, 47 | { 48 | "type": "View", 49 | "props": { 50 | "style": { 51 | "width": 300, 52 | "alignItems": "center", 53 | "marginTop": 20 54 | } 55 | }, 56 | "children": [ 57 | { 58 | "type": "View", 59 | "props": { 60 | "style": { 61 | "borderColor": "white", 62 | "borderRadius": 40, 63 | "width": 80, 64 | "height": 80, 65 | "borderWidth": 2, 66 | "justifyContent": "center", 67 | "alignItems": "center" 68 | } 69 | }, 70 | "children": [ 71 | { 72 | "type": "Image", 73 | "props": { 74 | "source": 1 75 | }, 76 | "children": null 77 | } 78 | ] 79 | }, 80 | { 81 | "type": "Text", 82 | "props": { 83 | "style": [ 84 | { 85 | "fontSize": 20, 86 | "color": "#cccccc", 87 | "paddingLeft": 25, 88 | "paddingRight": 25, 89 | "marginTop": 18, 90 | "marginBottom": 18, 91 | "textAlign": "center" 92 | }, 93 | {}, 94 | { 95 | "fontFamily": "AGaramondPro-Regular" 96 | } 97 | ], 98 | "accessible": true, 99 | "allowFontScaling": true, 100 | "ellipsizeMode": "tail" 101 | }, 102 | "children": [ 103 | "Sell work from your collection through our partner network." 104 | ] 105 | }, 106 | { 107 | "type": "View", 108 | "props": { 109 | "style": { 110 | "borderColor": "white", 111 | "borderRadius": 40, 112 | "width": 80, 113 | "height": 80, 114 | "borderWidth": 2, 115 | "justifyContent": "center", 116 | "alignItems": "center" 117 | } 118 | }, 119 | "children": [ 120 | { 121 | "type": "Image", 122 | "props": { 123 | "source": 1 124 | }, 125 | "children": null 126 | } 127 | ] 128 | }, 129 | { 130 | "type": "Text", 131 | "props": { 132 | "style": [ 133 | { 134 | "fontSize": 20, 135 | "color": "#cccccc", 136 | "paddingLeft": 25, 137 | "paddingRight": 25, 138 | "marginTop": 18, 139 | "marginBottom": 18, 140 | "textAlign": "center" 141 | }, 142 | {}, 143 | { 144 | "fontFamily": "AGaramondPro-Regular" 145 | } 146 | ], 147 | "accessible": true, 148 | "allowFontScaling": true, 149 | "ellipsizeMode": "tail" 150 | }, 151 | "children": [ 152 | "Get your work placed in an upcoming sale." 153 | ] 154 | } 155 | ] 156 | }, 157 | { 158 | "type": "View", 159 | "props": { 160 | "style": { 161 | "height": 43, 162 | "width": 320, 163 | "marginTop": 20 164 | } 165 | }, 166 | "children": [ 167 | { 168 | "type": "View", 169 | "props": { 170 | "accessible": true, 171 | "style": [ 172 | { 173 | "backgroundColor": "transparent" 174 | }, 175 | { 176 | "justifyContent": "center", 177 | "alignItems": "center", 178 | "borderColor": "#e5e5e5", 179 | "borderWidth": 1, 180 | "flex": 1, 181 | "backgroundColor": "rgba(255, 255, 255, 1)" 182 | } 183 | ], 184 | "isTVSelectable": true 185 | }, 186 | "children": [ 187 | { 188 | "type": "View", 189 | "props": {}, 190 | "children": [ 191 | { 192 | "type": "Text", 193 | "props": { 194 | "style": [ 195 | { 196 | "fontSize": 12 197 | }, 198 | { 199 | "fontFamily": "AGaramondPro-Regular", 200 | "color": "black", 201 | "opacity": 1 202 | }, 203 | { 204 | "fontFamily": "Avant Garde Gothic ITCW01Dm" 205 | } 206 | ], 207 | "accessible": true, 208 | "allowFontScaling": true, 209 | "ellipsizeMode": "tail" 210 | }, 211 | "children": [ 212 | "GET STARTED" 213 | ] 214 | } 215 | ] 216 | } 217 | ] 218 | } 219 | ] 220 | } 221 | ] 222 | } 223 | ] 224 | } 225 | ] 226 | } 227 | -------------------------------------------------------------------------------- /src/_tests/__snapshots__/_component-to-node.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`styleFromComponent handles deeply nested arrays of styles 1`] = `[Function]`; 4 | -------------------------------------------------------------------------------- /src/_tests/__snapshots__/_node-instance-count.test.tsx-counting-nodes-it-is-good-with-memory.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/_tests/__snapshots__/_node-to-svg.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`nodeToSVG handles a simple square 1`] = ` 4 | " 5 | " 6 | `; 7 | -------------------------------------------------------------------------------- /src/_tests/__snapshots__/_tree-to-svg-sub-funcs.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`svgWrapper wraps whatever text you pass into it with an SVG schema 1`] = ` 4 | " 5 | 6 | [My Body] 7 | 8 | " 9 | `; 10 | -------------------------------------------------------------------------------- /src/_tests/__snapshots__/_tree-to-svg.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`treeToSVG wraps whatever text you pass into it with an SVG schema 1`] = ` 4 | " 5 | 6 | 7 | 8 | 9 | " 10 | `; 11 | -------------------------------------------------------------------------------- /src/_tests/__snapshots__/render.test.tsx-handles-some-simple-jsx.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/_tests/__snapshots__/render.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`handles some simple JSX 1`] = ` 4 | " 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | " 39 | `; 40 | -------------------------------------------------------------------------------- /src/_tests/_component-to-node.test.ts: -------------------------------------------------------------------------------- 1 | import * as yoga from "yoga-layout" 2 | 3 | import componentToNode, { styleFromComponent } from "../component-to-node" 4 | 5 | describe("componentToNode", () => { 6 | it("generates the width for a simple component", () => { 7 | const component = { 8 | type: "View", 9 | props: { 10 | style: { 11 | width: 300, 12 | height: 40 13 | } 14 | }, 15 | children: null 16 | } 17 | 18 | const settings = { 19 | width: 1024, 20 | height: 768, 21 | wireframe: false 22 | } 23 | 24 | const node = componentToNode(component, settings) 25 | node.calculateLayout(yoga.UNDEFINED, yoga.UNDEFINED, yoga.DIRECTION_INHERIT) 26 | 27 | expect(node.getComputedWidth()).toEqual(300) 28 | expect(node.getComputedHeight()).toEqual(40) 29 | 30 | node.free() 31 | }) 32 | }) 33 | 34 | const componentWithStyle = (style: any) => ({ kind: "Stub", props: {style}}) 35 | 36 | describe("styleFromComponent", () => { 37 | it("handles deeply nested arrays of styles", () => { 38 | const style = [ 39 | { 40 | fontSize: 30, 41 | color: "black", 42 | textAlign: "left", 43 | paddingLeft: 20, 44 | paddingRight: 20 45 | }, 46 | [ 47 | { 48 | textAlign: "center", 49 | fontSize: 30, 50 | lineHeight: 32, 51 | width: 280, 52 | marginTop: 35, 53 | alignSelf: "center" 54 | }, 55 | null 56 | ], 57 | { fontFamily: "AGaramondPro-Regular" } 58 | ] 59 | const component = componentWithStyle(style) 60 | expect(styleFromComponent).toMatchSnapshot() 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /src/_tests/_node-instance-count.test.tsx: -------------------------------------------------------------------------------- 1 | import "../index" 2 | 3 | import * as yoga from "yoga-layout" 4 | 5 | import * as React from "react" 6 | import { View } from "react-native" 7 | import * as renderer from "react-test-renderer" 8 | 9 | import treeToSVG from "../tree-to-svg" 10 | 11 | describe("Counting nodes", () => { 12 | it("it is good with memory", () => { 13 | 14 | expect(yoga.getInstanceCount()).toEqual(0) 15 | 16 | const jsx = 17 | 23 | 24 | < View style={{width: 50, height: 50, backgroundColor: "skyblue"}} /> 25 | 26 | 27 | 28 | const component = renderer.create(jsx).toJSON() 29 | expect(component).toMatchSVGSnapshot(320, 480) 30 | 31 | expect(yoga.getInstanceCount()).toEqual(0) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/_tests/_node-to-svg.test.ts: -------------------------------------------------------------------------------- 1 | import * as yoga from "yoga-layout" 2 | 3 | import nodeToSVG from "../node-to-svg" 4 | 5 | const component = (name) => ({ 6 | type: name, 7 | props: {}, 8 | children: [], 9 | textContent: undefined, 10 | layout: { 11 | left: 0, 12 | right: 6, 13 | top: 0, 14 | bottom: 100, 15 | width: 600, 16 | height: 400 17 | } 18 | }) 19 | 20 | describe("nodeToSVG", () => { 21 | it("handles a simple square", () => { 22 | const rootNode = component("my component") 23 | 24 | const settings = { 25 | width: 1024, 26 | height: 768, 27 | wireframe: false 28 | } 29 | 30 | const results = nodeToSVG(0, rootNode, settings) 31 | expect(results).toMatchSnapshot() 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/_tests/_tree-to-svg-sub-funcs.test.ts: -------------------------------------------------------------------------------- 1 | const mockNodeToSVG = jest.fn() 2 | jest.mock("../node-to-svg.ts", () => ({ default: mockNodeToSVG })) 3 | 4 | import * as yoga from "yoga-layout" 5 | import { RenderedComponent } from "../index" 6 | import { recurseTree, svgWrapper } from "../tree-to-svg" 7 | 8 | describe("svgWrapper", () => { 9 | it("wraps whatever text you pass into it with an SVG schema", () => { 10 | const body = "[My Body]" 11 | const settings = { 12 | width: 444, 13 | height: 555, 14 | wireframe: false 15 | } 16 | 17 | const results = svgWrapper(body, settings) 18 | expect(results).toMatchSnapshot() 19 | }) 20 | }) 21 | 22 | const component = (name) => ({ 23 | type: name, 24 | props: {}, 25 | children: [], 26 | textContent: undefined, 27 | layout: { 28 | left: 2, 29 | right: 6, 30 | top: 80, 31 | bottom: 100, 32 | width: 200, 33 | height: 200 34 | } 35 | }) as RenderedComponent 36 | 37 | describe("recurseTree", () => { 38 | beforeEach(() => { 39 | mockNodeToSVG.mockReset() 40 | }) 41 | 42 | it("Calls nodeToSVG for it's first node", () => { 43 | const root = component("main") 44 | 45 | const settings = { 46 | width: 1024, 47 | height: 768, 48 | wireframe: false 49 | } 50 | const results = recurseTree(0, root, settings) 51 | expect(mockNodeToSVG.mock.calls.length).toEqual(1) 52 | }) 53 | 54 | it("Calls nodeToSVG for it's children nodes", () => { 55 | const root = component("main") 56 | root.children = [component("1"), component("2")] 57 | 58 | const settings = { 59 | width: 1024, 60 | height: 768, 61 | wireframe: false, 62 | styleMap: new WeakMap() 63 | } 64 | 65 | const results = recurseTree(0, root, settings) 66 | expect(mockNodeToSVG.mock.calls.length).toEqual(3) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /src/_tests/_tree-to-svg.test.ts: -------------------------------------------------------------------------------- 1 | import * as yoga from "yoga-layout" 2 | 3 | import treeToSVG from "../tree-to-svg" 4 | 5 | describe("treeToSVG", () => { 6 | it("wraps whatever text you pass into it with an SVG schema", () => { 7 | const renderedComponent = { 8 | type: "my component", 9 | props: {}, 10 | children: [], 11 | textContent: undefined, 12 | layout: { 13 | left: 2, 14 | right: 6, 15 | top: 80, 16 | bottom: 100, 17 | width: 200, 18 | height: 200 19 | } 20 | } 21 | 22 | const settings = { 23 | width: 1024, 24 | height: 768, 25 | wireframe: false 26 | } 27 | 28 | const results = treeToSVG(renderedComponent, settings) 29 | expect(results).toMatchSnapshot() 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_align-items.test.tsx-renders-three-vertically-horizontally-centeredblocks.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_borders.test.tsx-border-radius-larger-than-height.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_borders.test.tsx-border-radius-larger-than-width.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_borders.test.tsx-no-border-radius.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_borders.test.tsx-small-border-radius.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_borders.test.tsx-varying-border-colors,-radii-and-widths.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_borders.test.tsx-varying-border-colors-and-widths.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_borders.test.tsx-varying-border-colors-dashed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_borders.test.tsx-varying-border-colors-dotted.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_borders.test.tsx-varying-border-colors.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_borders.test.tsx-varying-border-radii-dashed.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_borders.test.tsx-varying-border-radii-dotted.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_borders.test.tsx-varying-border-radii.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_borders.test.tsx-varying-border-widths.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_flex-direction.test.tsx-renders-three-blocks-in-a-row.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_justify-contents.test.tsx-splits-the-layout-vertically-across.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_position.test.tsx-renders-views-on-bottom-position.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_position.test.tsx-renders-views-on-left-position.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_position.test.tsx-renders-views-on-top-position.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_text.test.tsx-does-not-break-if-a-text-tag-is-empty.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_text.test.tsx-renders-a-line-of-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Hello world 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_text.test.tsx-renders-multiple-lines-of-text-with-multiple-styles.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Lorem ipsum 8 | dolor sit amet, 9 | consectetur 10 | adipiscing elit. 11 | 12 | Sed eleifend 13 | congue 14 | faucibus. 15 | 16 | In 17 | eget tortor in 18 | odio luctus 19 | eleifend. 20 | Nullam pretium 21 | justo nisi, nec 22 | volutpat turpis 23 | tempor et. 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_text.test.tsx-renders-multiple-lines-of-text-with-text-align.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Lorem ipsum 11 | dolor sit amet, 12 | consectetur 13 | adipiscing elit. 14 | 15 | 16 | 17 | 18 | 19 | Lorem ipsum 20 | dolor sit amet, 21 | consectetur 22 | adipiscing elit. 23 | 24 | 25 | 26 | 27 | 28 | Lorem ipsum 29 | dolor sit amet, 30 | consectetur 31 | adipiscing elit. 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_text.test.tsx-renders-multiple-lines-of-text.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Lorem ipsum 8 | dolor sit amet, 9 | consectetur 10 | adipiscing elit. 11 | Sed eleifend 12 | congue 13 | faucibus. In 14 | 15 | eget tortor in 16 | odio luctus 17 | eleifend. 18 | Nullam pretium 19 | justo nisi, nec 20 | volutpat turpis 21 | 22 | tempor et. 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_text.test.tsx-renders-text-in-the-correct-place-when-positioned-with-flex.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | Test 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_text.test.tsx-renders-text-using-the-specified-default-font.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | Test setDefaultFont 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_text.test.tsx-renders-text-with-different-colors.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Hello 11 | World 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_text.test.tsx-renders-text-with-different-sizes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Hello 11 | World 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_text.test.tsx-renders-text-with-forced-break.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Hello 8 | 9 | world 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_wireframe.test.tsx-nested-wireframe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_wireframe.test.tsx-simple-wireframe-with-background-color.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_wireframe.test.tsx-simple-wireframe-with-border-radius.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/__snapshots__/_wireframe.test.tsx-simple-wireframe.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/_align-items.test.tsx: -------------------------------------------------------------------------------- 1 | import "../../index" 2 | 3 | import * as React from "react" 4 | import { View } from "react-native" 5 | import * as renderer from "react-test-renderer" 6 | 7 | // https://facebook.github.io/react-native/docs/flexbox.html 8 | 9 | it("Renders three vertically/horizontally centeredblocks", () => { 10 | const jsx = 11 | 17 | 18 | 19 | 20 | 21 | 22 | const component = renderer.create(jsx).toJSON() 23 | expect(component).toMatchSVGSnapshot(320, 480, { wireframe: true }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/_borders.test.tsx: -------------------------------------------------------------------------------- 1 | import "../../index" 2 | 3 | import * as React from "react" 4 | import { View } from "react-native" 5 | import * as renderer from "react-test-renderer" 6 | 7 | // https://facebook.github.io/react-native/docs/flexbox.html 8 | 9 | it("No border radius", () => { 10 | const jsx = 11 | 12 | const component = renderer.create(jsx).toJSON() 13 | expect(component).toMatchSVGSnapshot(320, 480) 14 | }) 15 | 16 | it("Small border radius", () => { 17 | const jsx = 18 | 19 | const component = renderer.create(jsx).toJSON() 20 | expect(component).toMatchSVGSnapshot(320, 480) 21 | }) 22 | 23 | it("Border radius larger than height", () => { 24 | const jsx = 25 | 26 | const component = renderer.create(jsx).toJSON() 27 | expect(component).toMatchSVGSnapshot(320, 480) 28 | }) 29 | 30 | it("Border radius larger than width", () => { 31 | const jsx = 32 | 33 | const component = renderer.create(jsx).toJSON() 34 | expect(component).toMatchSVGSnapshot(320, 480) 35 | }) 36 | 37 | it("Varying border radii", () => { 38 | const jsx = ( 39 | 52 | ) 53 | 54 | const component = renderer.create(jsx).toJSON() 55 | expect(component).toMatchSVGSnapshot(320, 480) 56 | }) 57 | 58 | it("Varying border radii dashed", () => { 59 | const jsx = ( 60 | 74 | ) 75 | 76 | const component = renderer.create(jsx).toJSON() 77 | expect(component).toMatchSVGSnapshot(320, 480) 78 | }) 79 | 80 | it("Varying border radii dotted", () => { 81 | const jsx = ( 82 | 96 | ) 97 | 98 | const component = renderer.create(jsx).toJSON() 99 | expect(component).toMatchSVGSnapshot(320, 480) 100 | }) 101 | 102 | it("Varying border widths", () => { 103 | const jsx = ( 104 | 117 | ) 118 | 119 | const component = renderer.create(jsx).toJSON() 120 | expect(component).toMatchSVGSnapshot(320, 480) 121 | }) 122 | 123 | it("Varying border colors", () => { 124 | const jsx = ( 125 | 138 | ) 139 | 140 | const component = renderer.create(jsx).toJSON() 141 | expect(component).toMatchSVGSnapshot(320, 480) 142 | }) 143 | 144 | it("Varying border colors dashed", () => { 145 | const jsx = ( 146 | 160 | ) 161 | 162 | const component = renderer.create(jsx).toJSON() 163 | expect(component).toMatchSVGSnapshot(320, 480) 164 | }) 165 | 166 | it("Varying border colors dotted", () => { 167 | const jsx = ( 168 | 182 | ) 183 | 184 | const component = renderer.create(jsx).toJSON() 185 | expect(component).toMatchSVGSnapshot(320, 480) 186 | }) 187 | 188 | it("Varying border colors and widths", () => { 189 | const jsx = ( 190 | 206 | ) 207 | 208 | const component = renderer.create(jsx).toJSON() 209 | expect(component).toMatchSVGSnapshot(320, 480) 210 | }) 211 | 212 | it("Varying border colors, radii and widths", () => { 213 | const jsx = ( 214 | 233 | ) 234 | 235 | const component = renderer.create(jsx).toJSON() 236 | expect(component).toMatchSVGSnapshot(320, 480) 237 | }) 238 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/_flex-direction.test.tsx: -------------------------------------------------------------------------------- 1 | import "../../index" 2 | 3 | import * as React from "react" 4 | import { View } from "react-native" 5 | import * as renderer from "react-test-renderer" 6 | 7 | // https://facebook.github.io/react-native/docs/flexbox.html 8 | 9 | it("Renders three blocks in a row", () => { 10 | const jsx = 11 | 12 | 13 | 14 | 15 | 16 | 17 | const component = renderer.create(jsx).toJSON() 18 | expect(component).toMatchSVGSnapshot(320, 480, { wireframe: true }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/_justify-contents.test.tsx: -------------------------------------------------------------------------------- 1 | import "../../index" 2 | 3 | import * as React from "react" 4 | import { View } from "react-native" 5 | import * as renderer from "react-test-renderer" 6 | 7 | // https://facebook.github.io/react-native/docs/flexbox.html 8 | 9 | it("Splits the layout vertically across", () => { 10 | const jsx = 11 | 12 | 13 | 14 | 15 | 16 | 17 | const component = renderer.create(jsx).toJSON() 18 | expect(component).toMatchSVGSnapshot(320, 480, { wireframe: true }) 19 | }) 20 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/_position.test.tsx: -------------------------------------------------------------------------------- 1 | import "../../index" 2 | 3 | import * as React from "react" 4 | import { Text, View } from "react-native" 5 | import * as renderer from "react-test-renderer" 6 | 7 | it("Renders views on top position", () => { 8 | const jsx = 9 | 10 | 17 | 24 | 25 | const component = renderer.create(jsx).toJSON() 26 | expect(component).toMatchSVGSnapshot(320, 480, { wireframe: true }) 27 | }) 28 | 29 | it("Renders views on bottom position", () => { 30 | const jsx = 31 | 32 | 39 | 46 | 47 | const component = renderer.create(jsx).toJSON() 48 | expect(component).toMatchSVGSnapshot(320, 480, { wireframe: true }) 49 | }) 50 | 51 | it("Renders views on left position", () => { 52 | const jsx = 53 | 54 | 61 | 62 | const component = renderer.create(jsx).toJSON() 63 | expect(component).toMatchSVGSnapshot(320, 480, { wireframe: true }) 64 | }) 65 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/_text.test.tsx: -------------------------------------------------------------------------------- 1 | import "../../index" 2 | 3 | import * as fs from "fs" 4 | import * as path from "path" 5 | import * as React from "react" 6 | import { Text, View } from "react-native" 7 | import * as renderer from "react-test-renderer" 8 | 9 | import { addFontFallback, loadFont, setDefaultFont } from "../../index" 10 | 11 | loadFont(fs.readFileSync(path.join(__dirname, "../Arimo/Arimo-Regular.ttf"))) 12 | loadFont(fs.readFileSync(path.join(__dirname, "../Arimo/Arimo-Bold.ttf"))) 13 | loadFont(fs.readFileSync(path.join(__dirname, "../Arimo/Arimo-Italic.ttf"))) 14 | loadFont(fs.readFileSync(path.join(__dirname, "../Arimo/Arimo-BoldItalic.ttf"))) 15 | addFontFallback("Arimo", "'Helvetica', 'Arial', sans-serif") 16 | 17 | it("Renders a line of text", () => { 18 | const jsx = 19 | 20 | Hello world 21 | 22 | 23 | const component = renderer.create(jsx).toJSON() 24 | expect(component).toMatchSVGSnapshot(320, 480) 25 | }) 26 | 27 | it("Renders multiple lines of text", () => { 28 | const jsx = 29 | 30 | 31 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed eleifend congue faucibus. In 32 | {" "}eget tortor in odio luctus eleifend. Nullam pretium justo nisi, nec volutpat turpis 33 | {" "}tempor et. 34 | 35 | 36 | 37 | const component = renderer.create(jsx).toJSON() 38 | expect(component).toMatchSVGSnapshot(320, 480) 39 | }) 40 | 41 | it("Renders text with forced break", () => { 42 | const jsx = 43 | 44 | Hello{"\n"}world 45 | 46 | 47 | const component = renderer.create(jsx).toJSON() 48 | expect(component).toMatchSVGSnapshot(320, 480) 49 | }) 50 | 51 | it("Renders multiple lines of text with multiple styles", () => { 52 | const jsx = 53 | 54 | 55 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 56 | {" "}Sed eleifend congue faucibus. 57 | {" "}In eget tortor in odio luctus eleifend. Nullam pretium justo nisi, nec volutpat turpis tempor et. 58 | 59 | 60 | 61 | const component = renderer.create(jsx).toJSON() 62 | expect(component).toMatchSVGSnapshot(320, 480) 63 | }) 64 | 65 | it("Renders multiple lines of text with text align", () => { 66 | const jsx = 67 | 68 | 69 | 70 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 71 | 72 | 73 | 74 | 75 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 76 | 77 | 78 | 79 | 80 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. 81 | 82 | 83 | 84 | 85 | const component = renderer.create(jsx).toJSON() 86 | expect(component).toMatchSVGSnapshot(320, 480) 87 | }) 88 | 89 | it("Renders text with different sizes", () => { 90 | const jsx = 91 | 92 | 93 | 94 | Hello World 95 | 96 | 97 | 98 | 99 | const component = renderer.create(jsx).toJSON() 100 | expect(component).toMatchSVGSnapshot(320, 480) 101 | }) 102 | 103 | it("Renders text with different colors", () => { 104 | const jsx = 105 | 106 | 107 | 108 | Hello World 109 | 110 | 111 | 112 | 113 | const component = renderer.create(jsx).toJSON() 114 | expect(component).toMatchSVGSnapshot(320, 480) 115 | }) 116 | 117 | it("Renders text in the correct place when positioned with flex", () => { 118 | const jsx = 119 | 120 | 121 | Test 122 | 123 | 124 | 125 | const component = renderer.create(jsx).toJSON() 126 | expect(component).toMatchSVGSnapshot(320, 480) 127 | }) 128 | 129 | it("Renders text using the specified default font", () => { 130 | setDefaultFont("Arimo") 131 | 132 | const jsx = 133 | 134 | 135 | Test setDefaultFont 136 | 137 | 138 | 139 | const component = renderer.create(jsx).toJSON() 140 | expect(component).toMatchSVGSnapshot(320, 480) 141 | }) 142 | 143 | it("Does not break if a Text tag is empty", () => { 144 | const jsx = 145 | 146 | 147 | 148 | 149 | const component = renderer.create(jsx).toJSON() 150 | expect(component).toMatchSVGSnapshot(320, 480) 151 | }) 152 | -------------------------------------------------------------------------------- /src/_tests/example_layouts/_wireframe.test.tsx: -------------------------------------------------------------------------------- 1 | import "../../index" 2 | 3 | import * as React from "react" 4 | import { View } from "react-native" 5 | import * as renderer from "react-test-renderer" 6 | 7 | it("Simple wireframe", () => { 8 | const jsx = 9 | 10 | const component = renderer.create(jsx).toJSON() 11 | expect(component).toMatchSVGSnapshot(320, 480, { wireframe: true }) 12 | }) 13 | 14 | it("Simple wireframe with background color", () => { 15 | const jsx = 16 | 17 | const component = renderer.create(jsx).toJSON() 18 | expect(component).toMatchSVGSnapshot(320, 480, { wireframe: true }) 19 | }) 20 | 21 | it("Nested wireframe", () => { 22 | const jsx = 23 | 24 | 25 | 26 | 27 | 28 | 29 | const component = renderer.create(jsx).toJSON() 30 | expect(component).toMatchSVGSnapshot(320, 480, { wireframe: true }) 31 | }) 32 | 33 | it("Simple wireframe with border radius", () => { 34 | const jsx = 35 | 36 | const component = renderer.create(jsx).toJSON() 37 | expect(component).toMatchSVGSnapshot(320, 480, { wireframe: true }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/_tests/render.test.tsx: -------------------------------------------------------------------------------- 1 | import "../index" 2 | 3 | import * as React from "react" 4 | import { View } from "react-native" 5 | import * as renderer from "react-test-renderer" 6 | 7 | import componentTreeToNodeTree from "../component-tree-to-nodes" 8 | import renderedComponentTree from "../reapply-layouts-to-components" 9 | import treeToSVG from "../tree-to-svg" 10 | 11 | import * as fs from "fs" 12 | import * as yoga from "yoga-layout" 13 | 14 | it("handles some simple JSX", () => { 15 | const jsx = ( 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | 36 | const component = renderer.create(jsx).toJSON() 37 | const settings = { 38 | width: 600, 39 | height: 400, 40 | wireframe: false 41 | } 42 | 43 | const rootNode = componentTreeToNodeTree(component, settings) 44 | const rendered = renderedComponentTree(component, rootNode) 45 | const results = treeToSVG(rendered, settings) 46 | expect(results).toMatchSnapshot() 47 | 48 | expect(component).toMatchSVGSnapshot(1024, 768) 49 | rootNode.freeRecursive() 50 | }) 51 | -------------------------------------------------------------------------------- /src/ambient.d.ts: -------------------------------------------------------------------------------- 1 | // Initial stub of the Yoga type system 2 | // Based on https://facebook.github.io/yoga/docs/api/javascript/ 3 | // 4 | 5 | declare module "yoga-layout" { 6 | 7 | // https://github.com/facebook/yoga/blob/master/javascript/sources/YGEnums.js 8 | // and https://github.com/facebook/yoga/blob/master/gentest/gentest-javascript.js 5 9 | 10 | const UNDEFINED: number 11 | 12 | const ALIGN_COUNT = 8 13 | const ALIGN_AUTO = 0 14 | const ALIGN_FLEX_START = 1 15 | const ALIGN_CENTER = 2 16 | const ALIGN_FLEX_END = 3 17 | const ALIGN_STRETCH = 4 18 | const ALIGN_BASELINE = 5 19 | const ALIGN_SPACE_BETWEEN = 6 20 | const ALIGN_SPACE_AROUND = 7 21 | 22 | /** Do not use this in your code */ 23 | enum Align { 24 | Count = 8, 25 | Auto = 0, 26 | FlexStart = 1, 27 | Center = 2, 28 | FlexEnd = 3, 29 | Stretch = 4, 30 | Baseline = 5, 31 | SpaceBetween = 6, 32 | SpaceAround = 7, 33 | } 34 | 35 | const DIMENSION_COUNT = 2 36 | const DIMENSION_WIDTH = 0 37 | const DIMENSION_HEIGHT = 1 38 | 39 | const DIRECTION_COUNT = 3 40 | const DIRECTION_INHERIT = 0 41 | const DIRECTION_LTR = 1 42 | const DIRECTION_RTL = 2 43 | 44 | /** Do not use this in your code */ 45 | enum Direction { 46 | Count = 3, 47 | Inherit = 0, 48 | LTR = 1, 49 | RTL = 2, 50 | } 51 | 52 | const DISPLAY_COUNT = 2 53 | const DISPLAY_FLEX = 0 54 | const DISPLAY_NONE = 1 55 | 56 | /** Do not use this in your code */ 57 | enum Display { 58 | Count = 2, 59 | Flex = 0, 60 | None = 1, 61 | } 62 | 63 | const EDGE_COUNT = 9 64 | const EDGE_LEFT = 0 65 | const EDGE_TOP = 1 66 | const EDGE_RIGHT = 2 67 | const EDGE_BOTTOM = 3 68 | const EDGE_START = 4 69 | const EDGE_END = 5 70 | const EDGE_HORIZONTAL = 6 71 | const EDGE_VERTICAL = 7 72 | const EDGE_ALL = 8 73 | 74 | /** Do not use this in your code */ 75 | enum Edge { 76 | Count = 9, 77 | Left = 0, 78 | Top = 1, 79 | Right = 2, 80 | Bottom = 3, 81 | Start = 4, 82 | End = 5, 83 | Horizontal = 6, 84 | Vertical = 7, 85 | All = 8, 86 | } 87 | 88 | const EXPERIMENTAL_FEATURE_COUNT = 1 89 | const EXPERIMENTAL_FEATURE_WEB_FLEX_BASIS = 0 90 | 91 | const FLEX_DIRECTION_COUNT = 4 92 | const FLEX_DIRECTION_COLUMN = 0 93 | const FLEX_DIRECTION_COLUMN_REVERSE = 1 94 | const FLEX_DIRECTION_ROW = 2 95 | const FLEX_DIRECTION_ROW_REVERSE = 3 96 | 97 | /** Do not use this in your code */ 98 | enum FlexDirection { 99 | Count = 4, 100 | Column = 0, 101 | ColumnReverse = 1, 102 | Row = 2, 103 | RowReverse = 3, 104 | } 105 | 106 | const JUSTIFY_COUNT = 5 107 | const JUSTIFY_FLEX_START = 0 108 | const JUSTIFY_CENTER = 1 109 | const JUSTIFY_FLEX_END = 2 110 | const JUSTIFY_SPACE_BETWEEN = 3 111 | const JUSTIFY_SPACE_AROUND = 4 112 | 113 | /** Do not use this in your code */ 114 | enum Justify { 115 | Count = 5, 116 | FlexStart = 0, 117 | Center = 1, 118 | FlexEnd = 2, 119 | SpaceBetween = 3, 120 | SpaceAround = 3, 121 | } 122 | 123 | const LOG_LEVEL_COUNT = 6 124 | const LOG_LEVEL_ERROR = 0 125 | const LOG_LEVEL_WARN = 1 126 | const LOG_LEVEL_INFO = 2 127 | const LOG_LEVEL_DEBUG = 3 128 | const LOG_LEVEL_VERBOSE = 4 129 | const LOG_LEVEL_FATAL = 5 130 | 131 | const MEASURE_MODE_COUNT = 3 132 | const MEASURE_MODE_UNDEFINED = 0 133 | const MEASURE_MODE_EXACTLY = 1 134 | const MEASURE_MODE_AT_MOST = 2 135 | 136 | const NODE_TYPE_COUNT = 2 137 | const NODE_TYPE_DEFAULT = 0 138 | const NODE_TYPE_TEXT = 1 139 | 140 | const OVERFLOW_COUNT = 3 141 | const OVERFLOW_VISIBLE = 0 142 | const OVERFLOW_HIDDEN = 1 143 | const OVERFLOW_SCROLL = 2 144 | 145 | const POSITION_TYPE_COUNT = 2 146 | const POSITION_TYPE_RELATIVE = 0 147 | const POSITION_TYPE_ABSOLUTE = 1 148 | 149 | enum PositionType { 150 | RELATIVE = 0, 151 | ABSOLUTE = 1, 152 | } 153 | 154 | const PRINT_OPTIONS_COUNT = 3 155 | const PRINT_OPTIONS_LAYOUT = 1 156 | const PRINT_OPTIONS_STYLE = 2 157 | const PRINT_OPTIONS_CHILDREN = 4 158 | 159 | const UNIT_COUNT = 4 160 | const UNIT_UNDEFINED = 0 161 | const UNIT_POINT = 1 162 | const UNIT_PERCENT = 2 163 | const UNIT_AUTO = 3 164 | 165 | /** Do not use this in your code */ 166 | enum Unit { 167 | Count = 3, 168 | Undefined = 0, 169 | Point = 1, 170 | Percent = 2, 171 | Auto = 3, 172 | } 173 | 174 | const WRAP_COUNT = 3 175 | const WRAP_NO_WRAP = 0 176 | const WRAP_WRAP = 1 177 | const WRAP_WRAP_REVERSE = 2 178 | 179 | /** Do not use this in your code */ 180 | enum Wrap { 181 | Count = 3, 182 | No = 0, 183 | Wrap = 1, 184 | WrapReverse = 2, 185 | } 186 | 187 | class Layout { 188 | left: number 189 | right: number 190 | top: number 191 | bottom: number 192 | width: number 193 | height: number 194 | 195 | constructor(left: number, right: number, top: number, bottom: number, width: number, height: number) 196 | fromJS(expose: () => void) 197 | toString(): string 198 | } 199 | 200 | interface Sizable { 201 | width: number 202 | height: number 203 | } 204 | 205 | class Size { 206 | static fromJS(Sizeable): Size 207 | 208 | width: number 209 | height: number 210 | constructor(width: number, height: number) 211 | fromJS(expose: () => void) 212 | toString(): string 213 | } 214 | 215 | class Value { 216 | unit: Unit 217 | value: any 218 | 219 | constructor(unit: Unit, value: any) 220 | 221 | fromJS(expose: () => void) 222 | toString(): string 223 | valueOf(): any 224 | } 225 | 226 | interface NodeInstance { 227 | 228 | setWidth(width: number) 229 | setHeight(height: number) 230 | setMinWidth(width: number) 231 | setMinHeight(height: number) 232 | setMaxWidth(height: number) 233 | setMaxHeight(height: number) 234 | setPadding(edge: Edge, value: number) 235 | setMargin(edge: Edge, value: number) 236 | setBorder(edge: Edge, value: number) 237 | setDisplay(display: Display) 238 | setPositionType(positionType: PositionType) 239 | setPosition(edge: Edge, position: number) 240 | 241 | setFlex(ordinal: number) 242 | setFlexGrow(ordinal: number) 243 | setFlexShrink(ordinal: number) 244 | setFlexBasis(ordinal: number) 245 | setFlexDirection(direct: FlexDirection) 246 | setJustifyContent(justify: Justify) 247 | setAlignItems(alignment: number) 248 | setAlignSelf(alignment: number) 249 | 250 | setMeasureFunc(func: (width: number) => { width: number, height: number }) 251 | 252 | insertChild(node: NodeInstance, index: number) 253 | removeChild(node: NodeInstance) 254 | 255 | getComputedLeft(): number 256 | getComputedRight(): number 257 | getComputedTop(): number 258 | getComputedBottom(): number 259 | getComputedWidth(): number 260 | getComputedHeight(): number 261 | 262 | getChild(index: number): NodeInstance 263 | getChildCount(): number 264 | 265 | free() 266 | freeRecursive() 267 | 268 | // Triggers a layout pass, but doesn't give you the results 269 | calculateLayout(width: number, height: number, direction: Direction) 270 | // Generates the layout 271 | getComputedLayout(): Layout 272 | } 273 | 274 | interface NodeFactory { 275 | create: () => NodeInstance 276 | destroy(child: NodeInstance) 277 | } 278 | 279 | const Node: NodeFactory 280 | 281 | // Globals 282 | function getInstanceCount(): number 283 | } 284 | 285 | declare module "font-manager" { 286 | function findFontSync(style: any): { path: string } | null 287 | } 288 | 289 | // import {ViewStyle} from "react-native" 290 | 291 | // interface NodeInstance { 292 | // style: ViewStyle 293 | // } 294 | -------------------------------------------------------------------------------- /src/component-to-node.ts: -------------------------------------------------------------------------------- 1 | import * as yoga from "yoga-layout" 2 | import extractText from "./extract-text" 3 | import { flattenStyles } from "./flatten-styles" 4 | import { Component, Settings } from "./index" 5 | import { breakLines, measureLines } from "./text-layout" 6 | 7 | export const textLines = Symbol("textLines") 8 | 9 | const isNotEmpty = prop => typeof prop !== "undefined" && prop !== null 10 | 11 | const componentToNode = (component: Component, settings: Settings): yoga.NodeInstance => { 12 | // Do we need to pass in the parent node too? 13 | const node = yoga.Node.create() 14 | const hasStyle = component.props && component.props.style 15 | const style = hasStyle ? styleFromComponent(component) : {} 16 | 17 | if (hasStyle) { 18 | // http://facebook.github.io/react-native/releases/0.44/docs/layout-props.html 19 | 20 | if (isNotEmpty(style.width)) { node.setWidth(style.width) } 21 | if (isNotEmpty(style.height)) { node.setHeight(style.height) } 22 | 23 | if (isNotEmpty(style.minHeight)) { node.setMinHeight(style.minHeight) } 24 | if (isNotEmpty(style.minWidth)) { node.setMinWidth(style.minWidth) } 25 | 26 | if (isNotEmpty(style.maxHeight)) { node.setMaxHeight(style.maxHeight) } 27 | if (isNotEmpty(style.maxWidth)) { node.setMaxWidth(style.maxWidth) } 28 | 29 | if (isNotEmpty(style.margin)) { node.setMargin(yoga.EDGE_ALL, style.margin) } 30 | if (isNotEmpty(style.marginTop)) { node.setMargin(yoga.EDGE_TOP, style.marginTop) } 31 | if (isNotEmpty(style.marginBottom)) { node.setMargin(yoga.EDGE_BOTTOM, style.marginBottom) } 32 | if (isNotEmpty(style.marginLeft)) { node.setMargin(yoga.EDGE_LEFT, style.marginLeft) } 33 | if (isNotEmpty(style.marginRight)) { node.setMargin(yoga.EDGE_RIGHT, style.marginRight) } 34 | if (isNotEmpty(style.marginVertical)) { node.setMargin(yoga.EDGE_VERTICAL, style.marginVertical) } 35 | if (isNotEmpty(style.marginHorizontal)) { node.setMargin(yoga.EDGE_HORIZONTAL, style.marginHorizontal) } 36 | 37 | if (isNotEmpty(style.padding)) { node.setPadding(yoga.EDGE_ALL, style.padding) } 38 | if (isNotEmpty(style.paddingTop)) { node.setPadding(yoga.EDGE_TOP, style.paddingTop) } 39 | if (isNotEmpty(style.paddingBottom)) { node.setPadding(yoga.EDGE_BOTTOM, style.paddingBottom) } 40 | if (isNotEmpty(style.paddingLeft)) { node.setPadding(yoga.EDGE_LEFT, style.paddingLeft) } 41 | if (isNotEmpty(style.paddingRight)) { node.setPadding(yoga.EDGE_RIGHT, style.paddingRight) } 42 | if (isNotEmpty(style.paddingVertical)) { node.setPadding(yoga.EDGE_VERTICAL, style.paddingVertical) } 43 | if (isNotEmpty(style.paddingHorizontal)) { node.setPadding(yoga.EDGE_HORIZONTAL, style.paddingHorizontal) } 44 | 45 | if (isNotEmpty(style.borderWidth)) { node.setBorder(yoga.EDGE_ALL, style.borderWidth) } 46 | if (isNotEmpty(style.borderTopWidth)) { node.setBorder(yoga.EDGE_TOP, style.borderTopWidth) } 47 | if (isNotEmpty(style.borderBottomWidth)) { node.setBorder(yoga.EDGE_BOTTOM, style.borderBottomWidth) } 48 | if (isNotEmpty(style.borderLeftWidth)) { node.setBorder(yoga.EDGE_LEFT, style.borderLeftWidth) } 49 | if (isNotEmpty(style.borderRightWidth)) { node.setBorder(yoga.EDGE_RIGHT, style.borderRightWidth) } 50 | 51 | if (isNotEmpty(style.flex)) { node.setFlex(style.flex) } 52 | if (isNotEmpty(style.flexGrow)) { node.setFlexGrow(style.flexGrow) } 53 | if (isNotEmpty(style.flexShrink)) { node.setFlexShrink(style.flexShrink) } 54 | if (isNotEmpty(style.flexBasis)) { node.setFlexBasis(style.flexBasis) } 55 | 56 | if (style.position === "absolute") { 57 | node.setPositionType(yoga.POSITION_TYPE_ABSOLUTE) 58 | } 59 | if (isNotEmpty(style.top)) { node.setPosition(yoga.EDGE_TOP, style.top) } 60 | if (isNotEmpty(style.left)) { node.setPosition(yoga.EDGE_LEFT, style.left) } 61 | if (isNotEmpty(style.right)) { node.setPosition(yoga.EDGE_RIGHT, style.right) } 62 | if (isNotEmpty(style.bottom)) { node.setPosition(yoga.EDGE_BOTTOM, style.bottom) } 63 | 64 | const flexDirection = style.flexDirection 65 | if (flexDirection) { 66 | if (flexDirection === "row") { node.setFlexDirection(yoga.FLEX_DIRECTION_ROW) } 67 | if (flexDirection === "column") { node.setFlexDirection(yoga.FLEX_DIRECTION_COLUMN) } 68 | if (flexDirection === "row-reverse") { node.setFlexDirection(yoga.FLEX_DIRECTION_ROW_REVERSE) } 69 | if (flexDirection === "column-reverse") { node.setFlexDirection(yoga.FLEX_DIRECTION_COLUMN_REVERSE) } 70 | } 71 | 72 | const justifyContent = style.justifyContent 73 | if (justifyContent) { 74 | if (justifyContent === "flex-start") { node.setJustifyContent(yoga.JUSTIFY_FLEX_START) } 75 | if (justifyContent === "flex-end") { node.setJustifyContent(yoga.JUSTIFY_FLEX_END) } 76 | if (justifyContent === "center") { node.setJustifyContent(yoga.JUSTIFY_CENTER) } 77 | if (justifyContent === "space-between") { node.setJustifyContent(yoga.JUSTIFY_SPACE_BETWEEN) } 78 | if (justifyContent === "space-around") { node.setJustifyContent(yoga.JUSTIFY_SPACE_AROUND) } 79 | } 80 | 81 | const alignItems = style.alignItems 82 | if (alignItems) { 83 | if (alignItems === "flex-start") { node.setAlignItems(yoga.ALIGN_FLEX_END) } 84 | if (alignItems === "flex-end") { node.setAlignItems(yoga.ALIGN_FLEX_END) } 85 | if (alignItems === "center") { node.setAlignItems(yoga.ALIGN_CENTER) } 86 | if (alignItems === "stretch") { node.setAlignItems(yoga.ALIGN_STRETCH) } 87 | if (alignItems === "baseline") { node.setAlignItems(yoga.ALIGN_BASELINE) } 88 | } 89 | 90 | // TODO: De-dupe with above 91 | const alignSelf = style.alignSelf 92 | if (alignSelf) { 93 | if (alignSelf === "flex-start") { node.setAlignSelf(yoga.ALIGN_FLEX_END) } 94 | if (alignSelf === "flex-end") { node.setAlignSelf(yoga.ALIGN_FLEX_END) } 95 | if (alignSelf === "center") { node.setAlignSelf(yoga.ALIGN_CENTER) } 96 | if (alignSelf === "stretch") { node.setAlignSelf(yoga.ALIGN_STRETCH) } 97 | if (alignSelf === "baseline") { node.setAlignSelf(yoga.ALIGN_BASELINE) } 98 | } 99 | } 100 | 101 | // We're in a node showing Text 102 | if (component && component.type === "Text") { 103 | const styledText = extractText(component) 104 | component[textLines] = null 105 | node.setMeasureFunc(width => { 106 | const lines = breakLines(styledText, width) 107 | component[textLines] = lines 108 | return measureLines(lines) 109 | }) 110 | } 111 | 112 | return node 113 | } 114 | 115 | export const styleFromComponent = (component: Component) => { 116 | let style = component.props.style 117 | 118 | if (Array.isArray(style)) { 119 | style = flattenStyles(style) 120 | } 121 | 122 | return style 123 | } 124 | 125 | export default componentToNode 126 | -------------------------------------------------------------------------------- /src/component-tree-to-nodes.ts: -------------------------------------------------------------------------------- 1 | import * as yoga from "yoga-layout" 2 | 3 | import componentToNode from "./component-to-node" 4 | import { Component, Settings } from "./index" 5 | 6 | const treeToNodes = (root: Component, settings: Settings) => recurseTree(root, settings) 7 | 8 | export default treeToNodes 9 | 10 | export const recurseTree = (component: Component, settings: Settings) => { 11 | const node = componentToNode(component, settings) 12 | 13 | if (component.type !== "Text" && component.children) { 14 | // Don't go into Text nodes 15 | for (let index = 0; index < component.children.length; index++) { 16 | const childComponent = component.children[index] 17 | if (typeof childComponent === "string") { 18 | throw new Error("Unexpected string child in non-Text node") 19 | } 20 | const childNode = recurseTree(childComponent, settings) 21 | node.insertChild(childNode, index) 22 | } 23 | } 24 | 25 | return node 26 | } 27 | -------------------------------------------------------------------------------- /src/extract-text.ts: -------------------------------------------------------------------------------- 1 | import { flattenStyles } from "./flatten-styles" 2 | import { getDefaultFont, registerFontUsed } from "./font-loader" 3 | export interface AttributedStyle { start: number, end: number, style: any } 4 | 5 | export interface TextWithAttributedStyle { text: string, attributedStyles: AttributedStyle[] } 6 | 7 | const getStyles = component => flattenStyles(component.props.style) 8 | 9 | const mergeStyles = (a, b) => Object.keys(b).length > 0 ? { ...a, ...b } : a 10 | 11 | const defaultStyles = { 12 | color: "black", 13 | fontSize: 14, 14 | fontStyle: "normal", 15 | fontWeight: "normal", 16 | lineHeight: 18, 17 | textAlign: "left", 18 | } 19 | 20 | const appendStyleTo = ( 21 | attributedStyles: AttributedStyle[], 22 | text: string, 23 | style: object 24 | ) => { 25 | const lastAttributedStyle = attributedStyles.length > 0 26 | ? attributedStyles[attributedStyles.length - 1] 27 | : null 28 | 29 | if (lastAttributedStyle !== null && style === lastAttributedStyle) { 30 | lastAttributedStyle.end += text.length 31 | } else { 32 | const start = lastAttributedStyle ? lastAttributedStyle.end : 0 33 | const end = start + text.length 34 | attributedStyles.push({ start, end, style }) 35 | } 36 | } 37 | 38 | export default (component): TextWithAttributedStyle => { 39 | let text = "" 40 | const attributedStyles: AttributedStyle[] = [] 41 | 42 | const defaultStylesWithFont = mergeStyles(defaultStyles, { fontFamily: getDefaultFont() }) 43 | const iterate = (c, style = mergeStyles(defaultStylesWithFont, getStyles(c))) => { 44 | registerFontUsed(style.fontFamily, style.fontWeight, style.fontStyle); 45 | (c.children || []).forEach(child => { 46 | if (child == null) { 47 | /* Do nothing */ 48 | } else if (typeof child !== "object") { 49 | const childText = String(child) // child might be a number 50 | text += childText 51 | appendStyleTo(attributedStyles, childText, style) 52 | } else { 53 | iterate(child, mergeStyles(style, getStyles(child))) 54 | } 55 | }) 56 | } 57 | 58 | iterate(component) 59 | 60 | return { text, attributedStyles } 61 | } 62 | -------------------------------------------------------------------------------- /src/flatten-styles.ts: -------------------------------------------------------------------------------- 1 | 2 | export function flattenStyles(style): object { 3 | if (style === null || typeof style !== "object") { 4 | return {} 5 | } 6 | 7 | if (!Array.isArray(style)) { 8 | return style 9 | } 10 | 11 | const result = {} 12 | for (let i = 0, styleLength = style.length; i < styleLength; ++i) { 13 | const computedStyle = flattenStyles(style[i]) 14 | if (computedStyle) { 15 | Object.keys(computedStyle).forEach((key) => result[key] = computedStyle[key]) 16 | } 17 | } 18 | return result 19 | } 20 | -------------------------------------------------------------------------------- /src/font-loader.ts: -------------------------------------------------------------------------------- 1 | import * as fontManager from "font-manager" 2 | import * as fontkit from "fontkit" 3 | import * as fs from "fs" 4 | 5 | const weights = { 6 | normal: "400", 7 | bold: "700" 8 | } 9 | 10 | export interface FontStyle { 11 | fontFamily?: string, 12 | fontWeight?: string, 13 | fontStyle?: string, 14 | postscriptName?: string 15 | } 16 | const fonts = {} 17 | const fontFallbacks = {} 18 | let defaultFont = "Proza Libre" 19 | const svgFonts: { [key: string]: { path: string, style: FontStyle } } = {} 20 | const usedFonts: string[] = [] 21 | 22 | export const getDefaultFont = () => defaultFont 23 | export const setDefaultFont = (fontName: string) => defaultFont = fontName 24 | export const getSvgFonts = () => svgFonts 25 | 26 | export const registerFontUsed = ( 27 | fontFamily: string, 28 | fontStyle: string = "normal", 29 | fontWeight: string = "normal") => { 30 | const key = keyFor({ fontFamily, fontWeight, fontStyle }) 31 | if (!usedFonts.includes(key)) { 32 | usedFonts.push(key) 33 | } 34 | } 35 | export const getUsedFontKeys = () => usedFonts 36 | 37 | const numberWeight = weight => weights[weight] || weight 38 | 39 | const keyFor = ({ fontFamily, fontWeight, fontStyle }) => 40 | `${fontFamily} (weight: ${numberWeight(fontWeight)} style: ${fontStyle})` 41 | 42 | interface NameMatch { match: string, value: string } 43 | 44 | const weightNames = [ 45 | { match: "thin", value: "100" }, 46 | { match: "ultra light", value: "200" }, 47 | { match: "light", value: "300" }, 48 | { match: "normal", value: "400" }, 49 | { match: "medium", value: "500" }, 50 | { match: "semi bold", value: "600" }, 51 | { match: "bold", value: "700" }, 52 | { match: "ultra bold", value: "800" }, 53 | { match: "heavy", value: "900" }, 54 | { match: "100", value: "100" }, 55 | { match: "200", value: "200" }, 56 | { match: "300", value: "300" }, 57 | { match: "400", value: "400" }, 58 | { match: "500", value: "500" }, 59 | { match: "600", value: "600" }, 60 | { match: "700", value: "700" }, 61 | { match: "800", value: "800" }, 62 | { match: "900", value: "900" }, 63 | ] 64 | 65 | const italicNames = [ 66 | { match: "italic", value: "italic" }, 67 | { match: "oblique", value: "italic" }, 68 | ] 69 | 70 | const matchNames = (target: string, names: NameMatch[], defaultValue: string): string => { 71 | const match = names.find(name => target.toLowerCase().includes(name.match)) 72 | return match ? match.value : defaultValue 73 | } 74 | 75 | const getFontStyle = (font, style): FontStyle => { 76 | const fontFamily = style.fontFamily || font.familyName 77 | const fontWeight = style.fontWeight || matchNames(font.subfamilyName, weightNames, "400") 78 | const fontStyle = style.fontStyle || matchNames(font.subfamilyName, italicNames, "normal") 79 | 80 | return { fontFamily, fontWeight, fontStyle, postscriptName: style.postscriptName } 81 | } 82 | 83 | const getFontKey = (font, style) => { 84 | const { fontFamily, fontWeight, fontStyle } = getFontStyle(font, style) 85 | const key = keyFor({ fontFamily, fontWeight, fontStyle }) 86 | 87 | if (!fontFamily || !fontWeight || !fontStyle) { 88 | throw new Error(`Could not find styles for font: ${key}`) 89 | } 90 | 91 | return key 92 | } 93 | 94 | const addFont = (font, style: FontStyle) => { 95 | const key = getFontKey(font, style) 96 | fonts[key] = font 97 | } 98 | 99 | export const loadFont = (fontFile, style: FontStyle = {}) => { 100 | const font = fontkit.create(fontFile, style.postscriptName) 101 | if (font.fonts) { 102 | font.fonts.forEach(f => addFont(f, { fontFamily: style.fontFamily })) 103 | } else { 104 | addFont(font, style) 105 | } 106 | } 107 | 108 | export const addFontToSvg = (fontPath: string, style: FontStyle = {}) => { 109 | const resolvedPath = require.resolve(fontPath) 110 | const fontFile = fs.readFileSync(resolvedPath) 111 | const font = fontkit.create(fontFile, style.postscriptName) 112 | const key = getFontKey(font, style) 113 | svgFonts[key] = { path: resolvedPath, style: getFontStyle(font, style) } 114 | if (font.fonts) { 115 | font.fonts.forEach(f => addFont(f, { fontFamily: style.fontFamily })) 116 | } else { 117 | addFont(font, style) 118 | } 119 | } 120 | 121 | export const addFontFallback = (fontFamily: string, fallback: string) => { 122 | fontFallbacks[fontFamily] = fallback 123 | } 124 | 125 | export const fontForStyle = (style, force = false) => { 126 | const key = keyFor(style) 127 | if (fonts[key]) { 128 | return fonts[key] 129 | } else if (force) { 130 | throw new Error(`No font defined for ${key}`) 131 | } 132 | 133 | const fontDescriptor = fontManager.findFontSync({ 134 | family: style.fontFamily, 135 | weight: Number(numberWeight(style.fontWeight)), 136 | italic: style.fontStyle === "italic", 137 | }) 138 | 139 | if (fontDescriptor) { 140 | loadFont(fs.readFileSync(fontDescriptor.path)) 141 | } 142 | 143 | return fontForStyle(style, true) 144 | } 145 | 146 | export const fontWithFallbacks = (fontFamily: string): string => ( 147 | fontFallbacks[fontFamily] ? `'${fontFamily}', ${fontFallbacks[fontFamily]}` : `'${fontFamily}'` 148 | ) 149 | 150 | // Default font family to provide for jest snapshots testing. 151 | addFontToSvg("./font/proza-libre/ProzaLibre-Bold.ttf") 152 | addFontToSvg("./font/proza-libre/ProzaLibre-BoldItalic.ttf") 153 | addFontToSvg("./font/proza-libre/ProzaLibre-ExtraBold.ttf") 154 | addFontToSvg("./font/proza-libre/ProzaLibre-ExtraBoldItalic.ttf") 155 | addFontToSvg("./font/proza-libre/ProzaLibre-Italic.ttf") 156 | addFontToSvg("./font/proza-libre/ProzaLibre-Light.ttf") 157 | addFontToSvg("./font/proza-libre/ProzaLibre-LightItalic.ttf") 158 | addFontToSvg("./font/proza-libre/ProzaLibre-Medium.ttf") 159 | addFontToSvg("./font/proza-libre/ProzaLibre-MediumItalic.ttf") 160 | addFontToSvg("./font/proza-libre/ProzaLibre-Regular.ttf") 161 | addFontToSvg("./font/proza-libre/ProzaLibre-SemiBold.ttf") 162 | addFontToSvg("./font/proza-libre/ProzaLibre-SemiBoldItalic.ttf") 163 | -------------------------------------------------------------------------------- /src/font/proza-libre/ProzaLibre-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/jest-snapshots-svg/b647082cc5b67bb21a187749f0840755a840ca54/src/font/proza-libre/ProzaLibre-Bold.ttf -------------------------------------------------------------------------------- /src/font/proza-libre/ProzaLibre-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/jest-snapshots-svg/b647082cc5b67bb21a187749f0840755a840ca54/src/font/proza-libre/ProzaLibre-BoldItalic.ttf -------------------------------------------------------------------------------- /src/font/proza-libre/ProzaLibre-ExtraBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/jest-snapshots-svg/b647082cc5b67bb21a187749f0840755a840ca54/src/font/proza-libre/ProzaLibre-ExtraBold.ttf -------------------------------------------------------------------------------- /src/font/proza-libre/ProzaLibre-ExtraBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/jest-snapshots-svg/b647082cc5b67bb21a187749f0840755a840ca54/src/font/proza-libre/ProzaLibre-ExtraBoldItalic.ttf -------------------------------------------------------------------------------- /src/font/proza-libre/ProzaLibre-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/jest-snapshots-svg/b647082cc5b67bb21a187749f0840755a840ca54/src/font/proza-libre/ProzaLibre-Italic.ttf -------------------------------------------------------------------------------- /src/font/proza-libre/ProzaLibre-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/jest-snapshots-svg/b647082cc5b67bb21a187749f0840755a840ca54/src/font/proza-libre/ProzaLibre-Light.ttf -------------------------------------------------------------------------------- /src/font/proza-libre/ProzaLibre-LightItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/jest-snapshots-svg/b647082cc5b67bb21a187749f0840755a840ca54/src/font/proza-libre/ProzaLibre-LightItalic.ttf -------------------------------------------------------------------------------- /src/font/proza-libre/ProzaLibre-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/jest-snapshots-svg/b647082cc5b67bb21a187749f0840755a840ca54/src/font/proza-libre/ProzaLibre-Medium.ttf -------------------------------------------------------------------------------- /src/font/proza-libre/ProzaLibre-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/jest-snapshots-svg/b647082cc5b67bb21a187749f0840755a840ca54/src/font/proza-libre/ProzaLibre-MediumItalic.ttf -------------------------------------------------------------------------------- /src/font/proza-libre/ProzaLibre-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/jest-snapshots-svg/b647082cc5b67bb21a187749f0840755a840ca54/src/font/proza-libre/ProzaLibre-Regular.ttf -------------------------------------------------------------------------------- /src/font/proza-libre/ProzaLibre-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/jest-snapshots-svg/b647082cc5b67bb21a187749f0840755a840ca54/src/font/proza-libre/ProzaLibre-SemiBold.ttf -------------------------------------------------------------------------------- /src/font/proza-libre/ProzaLibre-SemiBoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/jest-snapshots-svg/b647082cc5b67bb21a187749f0840755a840ca54/src/font/proza-libre/ProzaLibre-SemiBoldItalic.ttf -------------------------------------------------------------------------------- /src/font/proza-libre/SIL Open Font License.txt: -------------------------------------------------------------------------------- 1 | SIL Open Font License 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs" 2 | import * as path from "path" 3 | 4 | import * as yoga from "yoga-layout" 5 | 6 | // Add toMatchSVGSnapshot to jest definitions. Specified here so that it is output into 7 | // the index.d.ts definitions provided for projects to use. 8 | declare global { 9 | namespace jest { 10 | interface Matchers { 11 | /** Checks and sets up SVG rendering for React Components. */ 12 | toMatchSVGSnapshot(width: number, height: number, settings?: { wireframe?: boolean }): void; 13 | } 14 | } 15 | } 16 | 17 | export interface Component { 18 | type: string 19 | props: any 20 | children: Component[] | string[] | null 21 | } 22 | 23 | export interface RenderedComponent { 24 | type: string 25 | props: any 26 | textContent: string | undefined 27 | children: RenderedComponent[] 28 | layout: { 29 | left: number 30 | right: number 31 | top: number 32 | bottom: number 33 | width: number 34 | height: number 35 | } 36 | } 37 | 38 | export interface Settings { 39 | width: number 40 | height: number 41 | wireframe: boolean 42 | } 43 | 44 | import componentTreeToNodeTree from "./component-tree-to-nodes" 45 | import renderedComponentTree from "./reapply-layouts-to-components" 46 | import treeToSVG from "./tree-to-svg" 47 | 48 | // toMatchSVGSnapshot(1024, 768) 49 | 50 | const fail = (msg) => ({ message: () => msg, pass: false }) 51 | 52 | export { addFontFallback, addFontToSvg, loadFont, setDefaultFont } from "./font-loader" 53 | 54 | expect.extend({ 55 | toMatchSVGSnapshot( 56 | root: Component, 57 | width, 58 | height, 59 | { wireframe = false }: { wireframe?: boolean } = {} 60 | ) { 61 | if (!root) { return fail("A falsy Component was passed to toMatchSVGSnapshot") } 62 | if (!root.props) { return fail("A Component without props was passed to toMatchSVGSnapshot") } 63 | if (!root.type) { return fail("A Component without a type was passed to toMatchSVGSnapshot") } 64 | 65 | // getState isn't in the d.ts for Jest, this is ok though. 66 | const state = (expect as any).getState() 67 | const currentTest = state.testPath as string 68 | const currentTestName = state.currentTestName as string 69 | 70 | const testFile = currentTestName.replace(/\s+/g, "-").replace(/\//g, "-").toLowerCase() 71 | 72 | // Figure out the paths 73 | const snapshotsDir = path.join(currentTest, "..", "__snapshots__") 74 | const expectedSnapshot = path.join(snapshotsDir, path.basename(currentTest) + "-" + testFile + ".svg") 75 | 76 | // Make our folder if it's needed 77 | if (!fs.existsSync(snapshotsDir)) { fs.mkdirSync(snapshotsDir) } 78 | 79 | // We will need to do something smarter in the future, these snapshots need to be 1 file per test 80 | // whereas jest-snapshots can be multi-test per file. 81 | 82 | const settings: Settings = { width, height, wireframe } 83 | const rootNode = componentTreeToNodeTree(root, settings) 84 | if (!rootNode) { return } 85 | 86 | try { 87 | // This will mutate the node tree, we cannot trust that the nodes in the original tree will 88 | // still exist. 89 | rootNode.calculateLayout(settings.width, settings.height, yoga.DIRECTION_LTR) 90 | } catch (e) { 91 | // Clean up the root node after the failure to calculate layout then rethrow the error 92 | if (rootNode) { 93 | rootNode.freeRecursive() 94 | } 95 | throw e 96 | } 97 | 98 | // Generate a tree of components with the layout baked into it, them clean up yog memory 99 | const renderedComponentRoot = renderedComponentTree(root, rootNode) 100 | rootNode.freeRecursive() 101 | 102 | const svgText = treeToSVG(renderedComponentRoot, settings, snapshotsDir) 103 | 104 | // TODO: Determine if Jest is in `-u`? 105 | // can be done via the private API 106 | // state.snapshotState._updateSnapshot === "all" 107 | 108 | // Are we in write mode? 109 | if (!fs.existsSync(expectedSnapshot)) { 110 | fs.writeFileSync(expectedSnapshot, svgText) 111 | return { 112 | message: () => "Created a new Snapshot for you", 113 | pass: false 114 | } 115 | } else { 116 | const contents = fs.readFileSync(expectedSnapshot, "utf8") 117 | if (contents !== svgText) { 118 | fs.writeFileSync(expectedSnapshot, svgText) 119 | return { message: () => `SVG Snapshot failed: we have updated it for you`, pass: false } 120 | } else { 121 | return { message: () => "All good", pass: true } 122 | } 123 | } 124 | } 125 | } as any) 126 | -------------------------------------------------------------------------------- /src/node-to-svg.ts: -------------------------------------------------------------------------------- 1 | import { ViewStyle } from "react-native" 2 | import * as yoga from "yoga-layout" 3 | import { textLines } from "./component-to-node" 4 | 5 | import { styleFromComponent } from "./component-to-node" 6 | import { RenderedComponent, Settings } from "./index" 7 | import text from "./svg/Text" 8 | import view from "./svg/View" 9 | import viewWireframe from "./svg/ViewWireframe" 10 | import wsp from "./whitespace" 11 | 12 | const nodeToSVG = (indent: number, node: RenderedComponent, settings: Settings) => { 13 | const { layout, props } = node 14 | const style = styleFromComponent(node) || {} 15 | const { top, left, width, height } = layout 16 | 17 | // TODO: Enable this 18 | // if (!style.backgroundColor && sidesEqual(borderWidths) && borderWidths[0] === 0) { 19 | // return "" 20 | // } 21 | 22 | let svgText = "" 23 | if (node[textLines] && node[textLines].length > 0) { 24 | svgText = text(layout.left, layout.top, layout.width, layout.height, node[textLines]) 25 | } else if (!settings.wireframe) { 26 | svgText = view(layout as yoga.Layout, style) 27 | } else { 28 | svgText = viewWireframe(layout as yoga.Layout, style) 29 | } 30 | 31 | return "\n" 32 | + wsp(indent) 33 | + svgText 34 | } 35 | 36 | export default nodeToSVG 37 | -------------------------------------------------------------------------------- /src/reapply-layouts-to-components.ts: -------------------------------------------------------------------------------- 1 | import {ViewStyle} from "react-native" 2 | import * as yoga from "yoga-layout" 3 | import { textLines } from "./component-to-node" 4 | 5 | import { Component, RenderedComponent } from "./index" 6 | 7 | const renderedComponentTree = (root: Component, node: yoga.NodeInstance) => recurseTree(root, node) 8 | 9 | export default renderedComponentTree 10 | 11 | export const recurseTree = (component: Component, node: yoga.NodeInstance) => { 12 | 13 | const newChildren = [] as RenderedComponent[] 14 | 15 | if (component.children) { 16 | for (let index = 0; index < component.children.length; index++) { 17 | const childComponent = component.children[index] 18 | const childNode = node.getChild(index) 19 | // Don't go into Text nodes 20 | if (component.type !== "Text" && typeof childComponent !== "string") { 21 | const renderedChildComponent = recurseTree(childComponent, childNode) 22 | newChildren.push(renderedChildComponent) 23 | } 24 | } 25 | } 26 | 27 | return { 28 | type: component.type, 29 | props: component.props, 30 | children: newChildren, 31 | [textLines]: component[textLines], 32 | layout : { 33 | left: node.getComputedLeft(), 34 | right: node.getComputedRight(), 35 | top: node.getComputedTop(), 36 | bottom: node.getComputedBottom(), 37 | width: node.getComputedWidth(), 38 | height: node.getComputedHeight() 39 | } 40 | } as RenderedComponent 41 | } 42 | -------------------------------------------------------------------------------- /src/svg/Text.ts: -------------------------------------------------------------------------------- 1 | import { TextWithAttributedStyle } from "../extract-text" 2 | import { fontWithFallbacks } from "../font-loader" 3 | import { lineBaseline, lineFontSize, lineHeight } from "../text-layout" 4 | import { $ } from "./util" 5 | 6 | const textStyles = style => ({ 7 | "font-family": fontWithFallbacks(style.fontFamily), 8 | "font-weight": style.fontWeight, 9 | "font-style": style.fontStyle, 10 | "font-size": style.fontSize, 11 | }) 12 | 13 | const textAligns = { 14 | left: 0, 15 | center: 0.5, 16 | right: 1, 17 | } 18 | 19 | const textAnchors = { 20 | left: "start", 21 | center: "middle", 22 | right: "end", 23 | } 24 | 25 | export default (x, y, width, height, lines: TextWithAttributedStyle[]): string => { 26 | const { textAlign = "left" as string } = lines[0].attributedStyles[0].style 27 | const originX = x + width * textAligns[textAlign] 28 | 29 | const { textLines } = lines.reduce((accum, line) => { 30 | const { text, attributedStyles } = line 31 | const originY = accum.y + lineBaseline(line) + (lineHeight(line) - lineFontSize(line)) / 2 32 | 33 | const tspans = attributedStyles.map(({ start, end, style }, i) => ( 34 | $("tspan", { 35 | x: i === 0 ? originX : undefined, 36 | y: i === 0 ? originY : undefined, 37 | fill: style.color, 38 | ...textStyles(style), 39 | }, i === attributedStyles.length - 1 40 | ? text.slice(start, end).replace(/\s*$/, "") 41 | : text.slice(start, end) 42 | ) 43 | )) 44 | 45 | return { 46 | y: accum.y + lineHeight(line), 47 | textLines: accum.textLines.concat(tspans.join("\n")) 48 | } 49 | }, { 50 | y, 51 | textLines: [] 52 | } as { y: number, textLines: string[] }) 53 | 54 | return $("text", { 55 | x, 56 | y, 57 | "text-anchor": textAlign !== "left" ? textAnchors[textAlign as string] : undefined, 58 | }, textLines.join("\n")) 59 | } 60 | -------------------------------------------------------------------------------- /src/svg/View.ts: -------------------------------------------------------------------------------- 1 | import { ViewStyle } from "react-native" 2 | import * as yoga from "yoga-layout" 3 | 4 | import { styleFromComponent } from "../component-to-node" 5 | import { RenderedComponent } from "../index" 6 | import wsp from "../whitespace" 7 | import { 8 | dashStyles, 9 | filledPathForSide, 10 | getBorderColor, 11 | getBorderWidth, 12 | getScaledBorderRadius, 13 | pathForRect, 14 | scaleSides, 15 | sidesEqual, 16 | strokedPathForSide, 17 | } from "./borders" 18 | import { $ } from "./util" 19 | 20 | export default ({ top, left, width, height }: yoga.Layout, style: any) => { 21 | const attributes: any = { 22 | type: "View", 23 | } 24 | 25 | const borderWidths = getBorderWidth(style) 26 | const borderColors = getBorderColor(style) 27 | const borderRadii = getScaledBorderRadius(style, width, height) 28 | 29 | const borderStyle: string = style.borderStyle || "solid" 30 | const fill = style.backgroundColor || "none" 31 | 32 | if ( 33 | sidesEqual(borderWidths) && 34 | sidesEqual(borderColors) && 35 | sidesEqual(borderRadii) && 36 | borderStyle === "solid" 37 | ) { 38 | const borderWidth = borderWidths[0] 39 | const borderRadius = borderRadii[0] 40 | // Offset size by half border radius, as RN draws border inside, whereas SVG draws on both sides 41 | return $("rect", { 42 | ...attributes, 43 | "x": left + borderWidth * 0.5, 44 | "y": top + borderWidth * 0.5, 45 | "width": width - borderWidth, 46 | "height": height - borderWidth, 47 | fill, 48 | "stroke": borderWidth ? borderColors[0] : undefined, 49 | "stroke-width": borderWidth || undefined, 50 | "rx": borderRadius ? (borderRadius - borderWidth * 0.5) : undefined, 51 | "ry": borderRadius ? (borderRadius - borderWidth * 0.5) : undefined, 52 | }) 53 | } else if (sidesEqual(borderWidths) && sidesEqual(borderColors) && borderStyle === "solid") { 54 | const borderWidth = borderWidths[0] 55 | return $("path", { 56 | ...attributes, 57 | fill, 58 | "stroke": borderWidth ? borderColors[0] : undefined, 59 | "stroke-width": borderWidth || undefined, 60 | "d": pathForRect(left, top, width, height, borderRadii, scaleSides(borderWidths, 0.5)) 61 | }) 62 | } else if (sidesEqual(borderColors) && borderStyle === "solid") { 63 | const backgroundShape = $("path", { 64 | ...attributes, 65 | fill, 66 | d: pathForRect(left, top, width, height, borderRadii, scaleSides(borderWidths, 0.5)) 67 | }) 68 | const borderShape = $("path", { 69 | fill: borderColors[0], 70 | d: pathForRect(left, top, width, height, borderRadii, [0, 0, 0, 0]) + 71 | pathForRect(left, top, width, height, borderRadii, borderWidths, true) 72 | }) 73 | return backgroundShape + borderShape 74 | } else if (sidesEqual(borderWidths) && sidesEqual(borderColors)) { 75 | const borderWidth = borderWidths[0] 76 | const backgroundShape = $("path", { 77 | ...attributes, 78 | fill, 79 | d: pathForRect(left, top, width, height, borderRadii, [0, 0, 0, 0]) 80 | }) 81 | const borderShape = $("path", { 82 | ...dashStyles[borderStyle](borderWidth), 83 | "fill": "none", 84 | "stroke": borderWidth ? borderColors[0] : undefined, 85 | "stroke-width": borderWidth || undefined, 86 | "d": pathForRect(left, top, width, height, borderRadii, scaleSides(borderWidths, 0.5)) 87 | }) 88 | return backgroundShape + borderShape 89 | } else if (borderStyle === "solid") { 90 | const backgroundShape = $("path", { 91 | ...attributes, 92 | fill, 93 | d: pathForRect(left, top, width, height, borderRadii, scaleSides(borderWidths, 0.5)), 94 | }) 95 | const borders = borderColors.map((borderColor, side) => ( 96 | $("path", { 97 | fill: borderColor || "none", 98 | d: filledPathForSide(left, top, width, height, borderRadii, borderWidths, side), 99 | }) 100 | )) 101 | return backgroundShape + borders.join("") 102 | } else { 103 | const backgroundShape = $("path", { 104 | ...attributes, 105 | fill, 106 | d: pathForRect(left, top, width, height, borderRadii, [0, 0, 0, 0]), 107 | }) 108 | const borders = borderColors.map((borderColor, side) => ( 109 | $("path", { 110 | ...dashStyles[borderStyle](borderWidths[side]), 111 | "stroke": borderColor, 112 | "stroke-width": borderWidths[side], 113 | "d": strokedPathForSide(left, top, width, height, borderRadii, borderWidths, side), 114 | }) 115 | )) 116 | return backgroundShape + borders.join("") 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/svg/ViewWireframe.ts: -------------------------------------------------------------------------------- 1 | import { ViewStyle } from "react-native" 2 | import * as yoga from "yoga-layout" 3 | 4 | import { styleFromComponent } from "../component-to-node" 5 | import { RenderedComponent } from "../index" 6 | import wsp from "../whitespace" 7 | import { 8 | dashStyles, 9 | filledPathForSide, 10 | getScaledBorderRadius, 11 | pathForRect, 12 | scaleSides, 13 | Sides, 14 | sidesEqual, 15 | strokedPathForSide, 16 | } from "./borders" 17 | import { $ } from "./util" 18 | 19 | export default ({ top, left, width, height }: yoga.Layout, style: any) => { 20 | const borderRadii = getScaledBorderRadius(style, width, height) 21 | const borderWidths: Sides = [1, 1, 1, 1] 22 | const borderWidth = 1 23 | 24 | const attributes: any = { 25 | "type": "View", 26 | "fill": style.backgroundColor || "rgba(0, 0, 0, 0.1)", 27 | "stroke": "black", 28 | "stroke-width": borderWidth, 29 | } 30 | 31 | if (sidesEqual(borderRadii)) { 32 | const borderRadius = borderRadii[0] 33 | // Offset size by half border radius, as RN draws border inside, whereas SVG draws on both sides 34 | return $("rect", { 35 | ...attributes, 36 | x: left + borderWidth * 0.5, 37 | y: top + borderWidth * 0.5, 38 | width: width - borderWidth, 39 | height: height - borderWidth, 40 | rx: borderRadius ? borderRadius - borderWidth * 0.5 : undefined, 41 | ry: borderRadius ? borderRadius - borderWidth * 0.5 : undefined, 42 | }) 43 | } else { 44 | return $("path", { 45 | ...attributes, 46 | d: pathForRect(left, top, width, height, borderRadii, scaleSides(borderWidths, 0.5)) 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/svg/borders.ts: -------------------------------------------------------------------------------- 1 | import {ViewStyle} from "react-native" 2 | import * as yoga from "yoga-layout" 3 | 4 | enum Side { 5 | All, 6 | Vertical, 7 | Horizontal, 8 | Top, 9 | Right, 10 | Bottom, 11 | Left 12 | } 13 | 14 | // Done in CSS order 15 | export type Sides = [T, T, T, T] 16 | 17 | const applySides = (sides: Sides, value: T | null | undefined, side: Side): Sides => { 18 | if (value == null) { 19 | return sides 20 | } 21 | 22 | const [top, right, bottom, left] = sides 23 | switch (side) { 24 | case Side.All: return [value, value, value, value] 25 | case Side.Vertical: return [value, right, value, left] 26 | case Side.Horizontal: return [top, value, bottom, value] 27 | case Side.Top: return [value, right, bottom, left] 28 | case Side.Right: return [top, value, bottom, left] 29 | case Side.Bottom: return [top, right, value, left] 30 | case Side.Left: return [top, right, bottom, value] 31 | } 32 | } 33 | 34 | export const getBorderWidth = (style: any): Sides => { 35 | let sides: Sides = [0, 0, 0, 0] 36 | sides = applySides(sides, style.borderWidth, Side.All) 37 | sides = applySides(sides, style.borderTopWidth, Side.Top) 38 | sides = applySides(sides, style.borderRightWidth, Side.Right) 39 | sides = applySides(sides, style.borderBottomWidth, Side.Bottom) 40 | sides = applySides(sides, style.borderLeftWidth, Side.Left) 41 | return sides 42 | } 43 | 44 | export const getBorderColor = (style: any): Sides => { 45 | let sides: Sides = ["black", "black", "black", "black"] 46 | sides = applySides(sides, style.borderColor, Side.All) 47 | sides = applySides(sides, style.borderTopColor, Side.Top) 48 | sides = applySides(sides, style.borderRightColor, Side.Right) 49 | sides = applySides(sides, style.borderBottomColor, Side.Bottom) 50 | sides = applySides(sides, style.borderLeftColor, Side.Left) 51 | return sides 52 | } 53 | 54 | export const getBorderRadius = (style: any): Sides => { 55 | let sides: Sides = [0, 0, 0, 0] 56 | // Close enough. We get the right result. 57 | sides = applySides(sides, style.borderRadius, Side.All) 58 | sides = applySides(sides, style.borderTopLeftRadius, Side.Top) 59 | sides = applySides(sides, style.borderTopRightRadius, Side.Right) 60 | sides = applySides(sides, style.borderBottomRightRadius, Side.Bottom) 61 | sides = applySides(sides, style.borderBottomLeftRadius, Side.Left) 62 | return sides 63 | } 64 | 65 | export const getScaledBorderRadius = (style: any, width: number, height: number): Sides => { 66 | let borderRadii = getBorderRadius(style) 67 | 68 | const borderScale = Math.max( 69 | (borderRadii[0] + borderRadii[2]) / width, 70 | (borderRadii[1] + borderRadii[3]) / width, 71 | (borderRadii[0] + borderRadii[3]) / height, 72 | (borderRadii[1] + borderRadii[2]) / height, 73 | 1 74 | ) 75 | 76 | if (borderScale > 1) { 77 | borderRadii = scaleSides(borderRadii, 1 / borderScale) 78 | } 79 | 80 | return borderRadii 81 | } 82 | 83 | export const sidesEqual = (sides: Sides): boolean => 84 | sides[0] === sides[1] && 85 | sides[0] === sides[2] && 86 | sides[0] === sides[3] 87 | 88 | export const scaleSides = (sides: Sides, scale: number): Sides => 89 | [sides[0] * scale, sides[1] * scale, sides[2] * scale, sides[3] * scale] 90 | 91 | interface Corner { rx: number, ry: number, x: number, y: number } 92 | 93 | const cornerEllipseAtSide = ( 94 | x: number, 95 | y: number, 96 | width: number, 97 | height: number, 98 | radii: Sides, 99 | insets: Sides, 100 | side: number, 101 | ): Corner => { 102 | const radius = radii[side] 103 | const insetBefore = insets[(side + 3) % 4] 104 | const insetAfter = insets[side] 105 | return { 106 | rx: Math.max(radius - (side % 2 === 0 ? insetBefore : insetAfter), 0), 107 | ry: Math.max(radius - (side % 2 === 0 ? insetAfter : insetBefore), 0), 108 | x: x + [0, 1, 1, 0][side] * width + [1, -1, -1, 1][side] * radius, 109 | y: y + [0, 0, 1, 1][side] * height + [1, 1, -1, -1][side] * radius, 110 | } 111 | } 112 | 113 | const to6Dp = x => Math.round(x * 1E6) / 1E6 114 | 115 | const positionOnCorner = (angle: number, corner: Corner) => ({ 116 | x: to6Dp(corner.x + corner.rx * Math.cos(angle)), 117 | y: to6Dp(corner.y + corner.ry * Math.sin(angle)) 118 | }) 119 | 120 | const drawSide = ( 121 | x: number, 122 | y: number, 123 | width: number, 124 | height: number, 125 | radii: Sides, 126 | insets: Sides, 127 | side: number, 128 | { 129 | startCompletion = 0.5, 130 | endCompletion = 0.5, 131 | anticlockwise = false, 132 | moveCommand = "", 133 | }: { 134 | startCompletion?: number, 135 | endCompletion?: number, 136 | moveCommand?: "M" | "L" | "", 137 | anticlockwise?: boolean, 138 | } = {} 139 | ) => { 140 | const baseAngle = (side + 3) * (Math.PI / 2) 141 | 142 | const startSide = anticlockwise ? (side + 1) % 4 : side 143 | const endSide = anticlockwise ? side : (side + 1) % 4 144 | const sweep = anticlockwise ? 0 : 1 145 | const completionFactor = Math.PI / 2 * (anticlockwise ? -1 : 1) 146 | 147 | let path = "" 148 | const startCorner = cornerEllipseAtSide(x, y, width, height, radii, insets, startSide) 149 | 150 | if (moveCommand !== "") { 151 | const moveAngle = baseAngle - startCompletion * completionFactor 152 | const move = positionOnCorner(moveAngle, startCorner) 153 | path += `${moveCommand}${move.x},${move.y}` 154 | } 155 | 156 | if (startCompletion > 0) { 157 | const start = positionOnCorner(baseAngle, startCorner) 158 | path += `A${startCorner.rx},${startCorner.ry} 0 0,${sweep} ${start.x},${start.y}` 159 | } 160 | 161 | const endCorner = cornerEllipseAtSide(x, y, width, height, radii, insets, endSide) 162 | const mid = positionOnCorner(baseAngle, endCorner) 163 | path += `L${mid.x},${mid.y}` 164 | 165 | if (endCompletion > 0) { 166 | const endAngle = baseAngle + endCompletion * completionFactor 167 | const end = positionOnCorner(endAngle, endCorner) 168 | path += `A${endCorner.rx},${endCorner.ry} 0 0,${sweep} ${end.x},${end.y}` 169 | } 170 | 171 | return path 172 | } 173 | 174 | export const pathForRect = (x, y, width, height, radii, insets, anticlockwise: boolean = false) => { 175 | const sideIndices = [0, 1, 2, 3] 176 | 177 | if (anticlockwise) { 178 | sideIndices.reverse() 179 | } 180 | 181 | const sides = sideIndices.map((side, index) => ( 182 | drawSide(x, y, width, height, radii, insets, side, { 183 | startCompletion: 0, 184 | endCompletion: 1, 185 | moveCommand: index === 0 ? "M" : "", 186 | anticlockwise, 187 | }) 188 | )) 189 | return sides.join("") + "Z" 190 | } 191 | 192 | export const filledPathForSide = (x, y, width, height, radii, insets, side) => ( 193 | drawSide(x, y, width, height, radii, [0, 0, 0, 0], side, { moveCommand: "M" }) + 194 | drawSide(x, y, width, height, radii, insets, side, { moveCommand: "L", anticlockwise: true }) + 195 | "Z" 196 | ) 197 | 198 | export const strokedPathForSide = (x, y, width, height, radii, insets, side) => ( 199 | drawSide(x, y, width, height, radii, scaleSides(insets, 0.5), side, { moveCommand: "M" }) 200 | ) 201 | 202 | export const dashStyles = { 203 | dotted: width => ({ "stroke-linecap": "round", "stroke-dasharray": `0, ${width * 1.5}`}), 204 | dashed: width => ({ "stroke-dasharray": `${width * 2}, ${width}`}), 205 | } 206 | -------------------------------------------------------------------------------- /src/svg/util.ts: -------------------------------------------------------------------------------- 1 | export const $ = (type: string, attributes: object, children?: string): string => { 2 | const opening = Object.keys(attributes).reduce((accum, key) => ( 3 | attributes[key] != null ? `${accum} ${key}="${attributes[key]}"` : accum 4 | ), `<${type}`) 5 | return children ? `${opening}>${children}` : `${opening}/>` 6 | } 7 | -------------------------------------------------------------------------------- /src/text-layout.ts: -------------------------------------------------------------------------------- 1 | import * as LineBreaker from "linebreak" 2 | import { AttributedStyle, TextWithAttributedStyle } from "./extract-text" 3 | import { fontForStyle } from "./font-loader" 4 | 5 | export const lineWidth = ({ text, attributedStyles }: TextWithAttributedStyle): number => 6 | attributedStyles.reduce((x, { start, end, style }, i) => { 7 | let body = text.slice(start, end) 8 | // Trim trailling whitespace 9 | if (i === attributedStyles.length - 1) { 10 | body = body.replace(/\s+$/, "") 11 | } 12 | const font = fontForStyle(style) 13 | return x + font.layout(body).advanceWidth / font.unitsPerEm * style.fontSize 14 | }, 0) 15 | 16 | export const lineHeight = (line: TextWithAttributedStyle): number => 17 | Math.max( 18 | 0, 19 | ...line.attributedStyles.map(({ style }) => style.lineHeight) 20 | ) 21 | 22 | export const lineFontSize = (line: TextWithAttributedStyle): number => 23 | Math.max( 24 | 0, 25 | ...line.attributedStyles.map(({ style }) => style.fontSize) 26 | ) 27 | 28 | const baselineForAttributedStyle = ({ style }: AttributedStyle): number => { 29 | const font = fontForStyle(style) 30 | return font.ascent / font.unitsPerEm * style.fontSize 31 | } 32 | 33 | export const lineBaseline = (line: TextWithAttributedStyle): number => 34 | Math.max(0, ...line.attributedStyles.map(baselineForAttributedStyle)) 35 | 36 | const textSlice = ( 37 | textStyle: TextWithAttributedStyle, 38 | start: number, 39 | end: number 40 | ): TextWithAttributedStyle => ({ 41 | text: textStyle.text.slice(start, end), 42 | attributedStyles: textStyle.attributedStyles 43 | .filter(a => a.end > start && a.start < end) 44 | .map(a => ({ 45 | start: Math.max(a.start - start, 0), 46 | end: Math.min(a.end - start, end - start), 47 | style: a.style, 48 | })) 49 | }) 50 | 51 | export const breakLines = ( 52 | textStyle: TextWithAttributedStyle, 53 | width: number 54 | ): TextWithAttributedStyle[] => { 55 | const { text, attributedStyles } = textStyle 56 | const breaker = new LineBreaker(text) 57 | 58 | const lines: TextWithAttributedStyle[] = [] 59 | let lineStart = 0 60 | let lastPosition = 0 61 | let lastLine: TextWithAttributedStyle | null = null 62 | let shouldBreak = false 63 | 64 | let bk: any = breaker.nextBreak() 65 | while (bk != null) { 66 | const { position, required } = bk 67 | const testLine = textSlice(textStyle, lineStart, position) 68 | if (lastLine === null || (!shouldBreak && lineWidth(testLine) <= width)) { 69 | lastLine = testLine 70 | } else { 71 | lines.push(lastLine) 72 | lineStart = lastPosition 73 | lastLine = textSlice(textStyle, lineStart, position) 74 | } 75 | lastPosition = position 76 | shouldBreak = required 77 | bk = breaker.nextBreak() 78 | } 79 | 80 | if (lastLine !== null) { 81 | lines.push(lastLine) 82 | } 83 | 84 | return lines 85 | } 86 | 87 | export const measureLines = lines => ({ 88 | width: Math.max(0, ...lines.map(lineWidth)), 89 | height: lines.reduce((a, b) => a + lineHeight(b), 0), 90 | }) 91 | -------------------------------------------------------------------------------- /src/tree-to-svg.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path" 2 | import * as yoga from "yoga-layout" 3 | import { getSvgFonts, getUsedFontKeys } from "./font-loader" 4 | import { RenderedComponent, Settings } from "./index" 5 | import nodeToSVG from "./node-to-svg" 6 | import wsp from "./whitespace" 7 | 8 | export const recurseTree = 9 | (indent: number, root: RenderedComponent, settings: Settings) => { 10 | 11 | const nodeString = nodeToSVG(indent, root, settings) 12 | 13 | const childrenCount = root.children.length 14 | if (!childrenCount) { return nodeString } 15 | 16 | return nodeString + groupWrap(root, indent, () => { 17 | let childGroups = "" 18 | 19 | for (let index = 0; index < childrenCount; index++) { 20 | const child = root.children[index] 21 | // Don't go into Text nodes 22 | if (!(typeof child === "string")) { 23 | childGroups += recurseTree(indent + 1, child, settings) 24 | } 25 | } 26 | 27 | return childGroups 28 | }) 29 | } 30 | 31 | const svgFonts = (snapshotsDir?: string) => { 32 | if (!snapshotsDir) { 33 | return "" 34 | } 35 | 36 | const fonts = getSvgFonts() 37 | const used = getUsedFontKeys() 38 | const keys = Object.keys(fonts).filter((key) => used.includes(key)) 39 | if (keys.length === 0) { 40 | return "" 41 | } 42 | 43 | let svgText = ` 44 | 63 | 64 | ` 65 | return svgText 66 | } 67 | 68 | export const svgWrapper = (bodyText: string, settings: Settings, snapshotsDir?: string) => 69 | ` 70 | 71 | ${svgFonts(snapshotsDir)}${bodyText} 72 | 73 | ` 74 | 75 | export const groupWrap = (node: RenderedComponent, indent: number, recurse: () => string) => ` 76 | 77 | ${wsp(indent)}${recurse()} 78 | ${wsp(indent)} 79 | ` 80 | 81 | const treeToSVG = (root: RenderedComponent, settings: Settings, snapshotsDir?: string) => { 82 | return svgWrapper(recurseTree(0, root, settings), settings, snapshotsDir) 83 | } 84 | 85 | export default treeToSVG 86 | -------------------------------------------------------------------------------- /src/whitespace.ts: -------------------------------------------------------------------------------- 1 | export default function(indent: number) { 2 | return Array(indent * 2 + 2).join(" ") 3 | } 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "rootDir": "src", 6 | "outDir": "build", 7 | "pretty": true, 8 | "moduleResolution": "node", 9 | "strictNullChecks": true, 10 | "declaration": true, 11 | "jsx": "react" 12 | }, 13 | "lib":["es2017"], 14 | "include": [ 15 | "src/**/*.ts", 16 | "src/**/*.tsx", 17 | "dangerfile.ts" 18 | ], 19 | "exclude": [ 20 | "node_modules", 21 | "build", 22 | "dangerfile.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:recommended" 4 | ], 5 | "rules": { 6 | // These can be ignored, I started a PR to fix them in schemastore but damn, it was a timesink with no obvious end 7 | "completed-docs": [ 8 | true, 9 | { 10 | "functions": { 11 | "visibilities" : ["exported"] 12 | }, 13 | "methods": { 14 | "visibilities" : ["exported"] 15 | } 16 | } 17 | ], 18 | "no-console": [ 19 | false 20 | ], 21 | "arrow-parens": false, 22 | "interface-name": [ 23 | true, 24 | "never-prefix" 25 | ], 26 | "max-classes-per-file": [ 27 | false 28 | ], 29 | "member-access": [ 30 | false, 31 | "check-accessor", 32 | "check-constructor" 33 | ], 34 | // Disabled till there’s an auto-fixer for this. 35 | // https://github.com/palantir/tslint/blob/master/src/rules/objectLiteralSortKeysRule.ts 36 | "object-literal-sort-keys": false, 37 | "semicolon": [ 38 | true, 39 | "never", 40 | "ignore-interfaces", 41 | "ignore-bound-class-methods" 42 | ], 43 | "switch-default": false, 44 | "trailing-comma": [ 45 | false 46 | ], 47 | "no-namespace": [false] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /web/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jest-community/jest-snapshots-svg/b647082cc5b67bb21a187749f0840755a840ca54/web/screenshot.png --------------------------------------------------------------------------------