├── .babelrc ├── .prettierignore ├── .gitignore ├── src ├── utils │ ├── allEqual.js │ ├── values.js │ ├── camelCase.js │ ├── sortRules.js │ └── sortRules.spec.js ├── transforms │ ├── rem.js │ └── media-queries │ │ ├── features.js │ │ └── types.js ├── index.js └── index.spec.js ├── index.d.ts ├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── test.yml │ └── codeql-analysis.yml ├── LICENSE.md ├── package.json ├── CHANGELOG.md └── README.md /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"] 3 | } 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | package.json 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | yarn-error.log 4 | .coverage 5 | -------------------------------------------------------------------------------- /src/utils/allEqual.js: -------------------------------------------------------------------------------- 1 | export const allEqual = (arr) => arr.every((v) => v === arr[0]); 2 | -------------------------------------------------------------------------------- /src/utils/values.js: -------------------------------------------------------------------------------- 1 | export const values = (obj) => Object.keys(obj).map((key) => obj[key]); 2 | -------------------------------------------------------------------------------- /src/transforms/rem.js: -------------------------------------------------------------------------------- 1 | export const remToPx = (value) => { 2 | return value.replace( 3 | /(\d*\.?\d+)rem/g, 4 | (match, m1) => parseFloat(m1, 10) * 16 + "px", 5 | ); 6 | }; 7 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export default function transform( 2 | css: string, 3 | options?: { 4 | ignoreRule?: (selector: string) => boolean; 5 | parseMediaQueries?: boolean; 6 | }, 7 | ): { [selector: string]: unknown }; 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /src/transforms/media-queries/features.js: -------------------------------------------------------------------------------- 1 | export const dimensionFeatures = [ 2 | "width", 3 | "height", 4 | "device-width", 5 | "device-height", 6 | ]; 7 | export const mediaQueryFeatures = [ 8 | "orientation", 9 | "scan", 10 | "resolution", 11 | "aspect-ratio", 12 | "device-aspect-ratio", 13 | "grid", 14 | "color", 15 | "color-index", 16 | "monochrome", 17 | ].concat(dimensionFeatures); 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /src/transforms/media-queries/types.js: -------------------------------------------------------------------------------- 1 | export const defaultTypes = [ 2 | "all", 3 | "braille", 4 | "embossed", 5 | "handheld", 6 | "print", 7 | "projection", 8 | "screen", 9 | "speech", 10 | "tty", 11 | "tv", 12 | ]; 13 | export const cssnextMediaQueryTypes = ["pointer", "hover", "block-overflow"]; 14 | export const reactNativeMediaQueryTypes = [ 15 | "android", 16 | "dom", 17 | "ios", 18 | "macos", 19 | "web", 20 | "windows", 21 | ]; 22 | export const mediaQueryTypes = defaultTypes 23 | .concat(cssnextMediaQueryTypes) 24 | .concat(reactNativeMediaQueryTypes); 25 | -------------------------------------------------------------------------------- /src/utils/camelCase.js: -------------------------------------------------------------------------------- 1 | export function camelCase(str) { 2 | // Lower cases the string 3 | return ( 4 | str 5 | .toLowerCase() 6 | // Replaces any - or _ characters with a space 7 | .replace(/[-_]+/g, " ") 8 | // Removes any non alphanumeric characters 9 | .replace(/[^\w\s]/g, "") 10 | // Uppercases the first character in each group immediately following a space 11 | // (delimited by spaces) 12 | .replace(/ (.)/g, function ($1) { 13 | return $1.toUpperCase(); 14 | }) 15 | // Removes spaces 16 | .replace(/ /g, "") 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/sortRules.js: -------------------------------------------------------------------------------- 1 | function isExport(n) { 2 | return Array.isArray(n) && n[0] === ":export"; 3 | } 4 | 5 | function byExport(a, b) { 6 | if (!isExport(a.selectors) && isExport(b.selectors)) { 7 | return -1; 8 | } 9 | if (isExport(a.selectors) && !isExport(b.selectors)) { 10 | return 1; 11 | } 12 | return 0; 13 | } 14 | 15 | function byLine(a, b) { 16 | if (isExport(a.selectors) && isExport(b.selectors)) { 17 | if (a.position.start.line > b.position.start.line) { 18 | return 1; 19 | } 20 | if (a.position.start.line < b.position.start.line) { 21 | return -1; 22 | } 23 | } 24 | return 0; 25 | } 26 | 27 | export function sortRules(rules) { 28 | return rules.sort(byExport).sort(byLine); 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License 2 | 3 | Copyright (c) 2017 Krister Kari 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push, pull_request] 3 | env: 4 | CI: true 5 | NODE_COV: 20 # The Node.js version to run coverage on 6 | 7 | jobs: 8 | run: 9 | name: Node ${{ matrix.node }} on ${{ matrix.os }} 10 | runs-on: ${{ matrix.os }} 11 | 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | node: [18, 20] 16 | os: [ubuntu-latest, windows-latest] 17 | 18 | steps: 19 | - name: Clone repository 20 | uses: actions/checkout@v2 21 | 22 | - name: Set Node.js version 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: ${{ matrix.node }} 26 | 27 | - name: Install yarn dependencies 28 | run: yarn install 29 | 30 | - name: Run Jest tests 31 | run: yarn test --runInBand 32 | if: "!(startsWith(matrix.os, 'ubuntu') && matrix.node == env.NODE_COV)" 33 | 34 | - name: Run Jest tests with coverage 35 | run: yarn test --runInBand --coverage 36 | if: startsWith(matrix.os, 'ubuntu') && matrix.node == env.NODE_COV 37 | 38 | - name: Run Coveralls 39 | uses: coverallsapp/github-action@master 40 | if: startsWith(matrix.os, 'ubuntu') && matrix.node == env.NODE_COV 41 | with: 42 | github-token: "${{ secrets.GITHUB_TOKEN }}" 43 | path-to-lcov: "./.coverage/lcov.info" 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-to-react-native-transform", 3 | "description": "Convert CSS text to a React Native stylesheet object", 4 | "version": "2.1.0", 5 | "main": "dist/index.js", 6 | "author": "Krister Kari", 7 | "license": "MIT", 8 | "scripts": { 9 | "prettify": "prettier --write '**/*.@(js|json|md)'", 10 | "precommit": "lint-staged", 11 | "build": "babel src --ignore *.spec.js --out-dir dist", 12 | "test": "jest --coverage", 13 | "prepublish": "npm run build", 14 | "release": "np" 15 | }, 16 | "devDependencies": { 17 | "@babel/cli": "^7.14.5", 18 | "@babel/core": "^7.14.6", 19 | "@babel/preset-env": "^7.14.7", 20 | "babel-jest": "^29.1.2", 21 | "coveralls": "^3.0.6", 22 | "husky": "^9.0.11", 23 | "jest": "^29.2.0", 24 | "lint-staged": "^15.2.2", 25 | "np": "^10.0.6", 26 | "prettier": "^3.0.0" 27 | }, 28 | "jest": { 29 | "transform": { 30 | "^.+\\.jsx?$": "babel-jest" 31 | }, 32 | "coverageDirectory": "./.coverage/", 33 | "coverageReporters": [ 34 | "lcov", 35 | "text" 36 | ], 37 | "coverageThreshold": { 38 | "global": { 39 | "branches": 75, 40 | "functions": 75, 41 | "lines": 75, 42 | "statements": 75 43 | } 44 | }, 45 | "testPathIgnorePatterns": [ 46 | "/dist", 47 | "/node_modules" 48 | ] 49 | }, 50 | "lint-staged": { 51 | "*.{js,json,md}": [ 52 | "prettier --write", 53 | "git add" 54 | ] 55 | }, 56 | "prettier": { 57 | "trailingComma": "all" 58 | }, 59 | "dependencies": { 60 | "css": "^3.0.0", 61 | "css-mediaquery": "^0.1.2", 62 | "css-to-react-native": "^3.2.0" 63 | }, 64 | "repository": { 65 | "type": "git", 66 | "url": "git+https://github.com/kristerkari/css-to-react-native-transform.git" 67 | }, 68 | "keywords": [ 69 | "React", 70 | "ReactNative", 71 | "styles", 72 | "CSS" 73 | ], 74 | "files": [ 75 | "dist", 76 | "src", 77 | "index.d.ts", 78 | "CHANGELOG.md", 79 | "README.md" 80 | ] 81 | } 82 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v2.1.0 2 | 3 | - Added: `ignoreRule` option 4 | - Updated: `css-to-react-native` dependency to v3.2.0 5 | 6 | ## v2.0.0 7 | 8 | - Breaking: updated `css-to-react-native` dependency to v3.0.0. This version removes the support for line-height with multiplier, adds support for `place-content` CSS property, and more. 9 | - Breaking: updated `css` dependency to v3.0.0. 10 | 11 | ## v1.9.0 12 | 13 | - Added: Typescript type definitions. 14 | 15 | ## v1.8.1 16 | 17 | - Fixed: flex-box shorthands missing `flex-basis: auto` in some cases. 18 | - Fixed: sorting :export blocks on Node.js version 11 was not working. 19 | 20 | ## v1.8.0 21 | 22 | - Added: support for parsing CSS modules (ICSS) `:export` blocks. 23 | 24 | ## v1.7.1 25 | 26 | - Fixed: an error is now thrown for a `line-height` value without unit (`line-height` multiplier value is not supported in React Native). 27 | - Updated: `css-to-react-native` dependency to v2.2.2 and `css` dependency to v2.2.4. 28 | 29 | ## v1.7.0 30 | 31 | - Updated: `css-to-react-native` dependency to v2.2.1. 32 | - Added: support for more platforms in CSS media queries. 33 | 34 | ## v1.6.0 35 | 36 | - Added: a feature flag for CSS viewport units. 37 | 38 | ## v1.5.0 39 | 40 | - Updated: `css-to-react-native` dependency to v2.2.0. 41 | - Fixed: the parser now allows passing through CSS units that are not supported by React Native. 42 | 43 | ## v1.4.0 44 | 45 | - Added: skip parsing of all other selector types than class selectors. 46 | 47 | ## v1.3.1 48 | 49 | - Fixed: allow multiple parts for parsed media queries to support `OR` media queries. 50 | 51 | ## v1.3.0 52 | 53 | - Added: transformation result now includes parsed CSS media queries under `__mediaQueries` object. This removes the need to re-parse media queries after transforming CSS. 54 | 55 | ## v1.2.0 56 | 57 | - Added: validate that CSS Media Queries have correct syntax. 58 | 59 | ## v1.1.0 60 | 61 | - Added: experimental support for parsing CSS Media Queries. Use `parseMediaQueries: true` to enable parsing media queries. 62 | 63 | ## v1.0.8 64 | 65 | - Updated: `css-to-react-native` dependency to v2.1.2. 66 | 67 | ## v1.0.7 68 | 69 | - Updated: `css-to-react-native` dependency to v2.1.1. 70 | 71 | ## v1.0.6 72 | 73 | - Fixed: Only apply `Image` styling fix for shorthand border props with a single value. 74 | 75 | ## v1.0.5 76 | 77 | - Fixed: Get rid of the requirement for `Symbol` by replacing `for of` loops with `for in` loops. 78 | 79 | ## v1.0.4 80 | 81 | - Fixed: `Image` styling fix for shorthand border props. 82 | 83 | ## v1.0.3 84 | 85 | - Fixed: Add missing `shadowOpacity` prop. 86 | - Fixed: box-shadow:support for rgb and rgba colors. 87 | 88 | ## v1.0.2 89 | 90 | - Fixed: Support unitless values for `box-shadow`. 91 | 92 | ## v1.0.1 93 | 94 | - Fixed: Remove array destructuring to make transpiled code ES5 compatible. 95 | 96 | ## v1.0.0 97 | 98 | - Initial release 99 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '25 21 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # css-to-react-native-transform 2 | 3 | [![NPM version](http://img.shields.io/npm/v/css-to-react-native-transform.svg)](https://www.npmjs.org/package/css-to-react-native-transform) 4 | [![Build Status](https://github.com/kristerkari/css-to-react-native-transform/workflows/Tests/badge.svg)](https://github.com/kristerkari/css-to-react-native-transform/actions?workflow=Tests) 5 | [![Coverage Status](https://coveralls.io/repos/github/kristerkari/css-to-react-native-transform/badge.svg?branch=master)](https://coveralls.io/github/kristerkari/css-to-react-native-transform?branch=master) 6 | [![Downloads per month](https://img.shields.io/npm/dm/css-to-react-native-transform.svg)](http://npmcharts.com/compare/css-to-react-native-transform) 7 | [![contributions welcome](https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=flat)](https://egghead.io/courses/how-to-contribute-to-an-open-source-project-on-github) 8 | 9 | A lightweight wrapper on top of 10 | [css-to-react-native](https://github.com/styled-components/css-to-react-native) 11 | to allow valid CSS to be turned into React Native Stylesheet objects. 12 | 13 | To keep things simple it only transforms class selectors (e.g. `.myClass {}`) and grouped class selectors (e.g. `.myClass, .myOtherClass {}`). Parsing of more complex selectors can be added as a new feature behind a feature flag (e.g. `transform(css, { parseAllSelectors: true })`) in the future if needed. 14 | 15 | Example: 16 | 17 | ```css 18 | .myClass { 19 | font-size: 18px; 20 | line-height: 24px; 21 | color: red; 22 | } 23 | 24 | .other { 25 | padding: 1rem; 26 | } 27 | ``` 28 | 29 | is transformed to: 30 | 31 | ```js 32 | { 33 | myClass: { 34 | fontSize: 18, 35 | lineHeight: 24, 36 | color: "red" 37 | }, 38 | other: { 39 | paddingBottom: 16, 40 | paddingLeft: 16, 41 | paddingRight: 16, 42 | paddingTop: 16 43 | } 44 | } 45 | ``` 46 | 47 | ## API 48 | 49 | ### Transform CSS 50 | 51 | ```js 52 | import transform from "css-to-react-native-transform"; 53 | // or const transform = require("css-to-react-native-transform").default; 54 | 55 | transform(` 56 | .foo { 57 | color: #f00; 58 | } 59 | `); 60 | ``` 61 | 62 | ↓ ↓ ↓ ↓ ↓ ↓ 63 | 64 | ```js 65 | { 66 | foo: { 67 | color: "#f00"; 68 | } 69 | } 70 | ``` 71 | 72 | ### `ignoreRule` option 73 | 74 | ```js 75 | transform( 76 | ` 77 | .foo { 78 | color: red; 79 | } 80 | .bar { 81 | font-size: 12px; 82 | } 83 | `, 84 | { 85 | ignoreRule: (selector) => { 86 | if (selector === ".foo") { 87 | return true; 88 | } 89 | }, 90 | }, 91 | ); 92 | ``` 93 | 94 | ↓ ↓ ↓ ↓ ↓ ↓ 95 | 96 | ```js 97 | { 98 | bar: { 99 | fontSize: 12; 100 | } 101 | } 102 | ``` 103 | 104 | ### CSS Modules :export block 105 | 106 | Parsing the [CSS Modules (ICSS) :export](https://github.com/css-modules/icss#export) is supported. The `:export` is often used to share variables from CSS or from a preprocessor like Sass/Less/Stylus to Javascript: 107 | 108 | ```js 109 | transform(` 110 | .foo { 111 | color: #f00; 112 | } 113 | 114 | :export { 115 | myProp: #fff; 116 | } 117 | `); 118 | ``` 119 | 120 | ↓ ↓ ↓ ↓ ↓ ↓ 121 | 122 | ```js 123 | { 124 | foo: { 125 | color: "#f00"; 126 | }, 127 | myProp: "#fff"; 128 | } 129 | ``` 130 | 131 | ### CSS Media Queries (experimental) 132 | 133 | _The API and parsed syntax for CSS Media Queries might change in the future_ 134 | 135 | ```js 136 | transform( 137 | ` 138 | .container { 139 | background-color: #f00; 140 | } 141 | 142 | @media (orientation: landscape) { 143 | .container { 144 | background-color: #00f; 145 | } 146 | } 147 | `, 148 | { parseMediaQueries: true }, 149 | ); 150 | ``` 151 | 152 | ↓ ↓ ↓ ↓ ↓ ↓ 153 | 154 | ```js 155 | { 156 | __mediaQueries: { 157 | "@media (orientation: landscape)": [{ 158 | expressions: [ 159 | { 160 | feature: "orientation", 161 | modifier: undefined, 162 | value: "landscape", 163 | }, 164 | ], 165 | inverse: false, 166 | type: "all", 167 | }], 168 | }, 169 | container: { 170 | backgroundColor: "#f00", 171 | }, 172 | "@media (orientation: landscape)": { 173 | container: { 174 | backgroundColor: "#00f", 175 | }, 176 | }, 177 | } 178 | ``` 179 | 180 | You can also speficy a platform as the media query type ("android", "dom", "ios", "macos", "web", "windows"): 181 | 182 | ```js 183 | transform( 184 | ` 185 | .container { 186 | background-color: #f00; 187 | } 188 | 189 | @media android and (orientation: landscape) { 190 | .container { 191 | background-color: #00f; 192 | } 193 | } 194 | `, 195 | { parseMediaQueries: true }, 196 | ); 197 | ``` 198 | 199 | ### CSS Viewport Units (experimental) 200 | 201 | When [CSS Viewport Units](https://caniuse.com/#feat=viewport-units) are used, a special `__viewportUnits` feature flag is added to the result. This is done so that the implementation that transforms viewport units to pixels knows that the style object has viewport units inside it, and can avoid doing extra work if the style object does not contain any viewport units. 202 | 203 | ```js 204 | transform(`.foo { font-size: 1vh; }`); 205 | ``` 206 | 207 | ↓ ↓ ↓ ↓ ↓ ↓ 208 | 209 | ```js 210 | { 211 | __viewportUnits: true, 212 | foo: { 213 | fontSize: "1vh"; 214 | } 215 | } 216 | ``` 217 | 218 | ## Limitations 219 | 220 | - For `rem` unit the root element `font-size` is currently set to 16 pixels. A 221 | setting needs to be implemented to allow the user to define the root element 222 | `font-size`. 223 | - There is also support for the `box-shadow` shorthand, and this converts into 224 | `shadow-` properties. Note that these only work on iOS. 225 | 226 | ## Dependencies 227 | 228 | This library has the following packages as dependencies: 229 | 230 | - [css](https://github.com/reworkcss/css#readme) - CSS parser / stringifier 231 | - [css-mediaquery](https://github.com/ericf/css-mediaquery) - Parses and determines if a given CSS Media Query matches a set of values. 232 | - [css-to-react-native](https://github.com/styled-components/css-to-react-native) - Convert CSS text to a React Native stylesheet object 233 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import mediaQuery from "css-mediaquery"; 2 | import transformCSS from "css-to-react-native"; 3 | import parseCSS from "css/lib/parse"; 4 | import { 5 | dimensionFeatures, 6 | mediaQueryFeatures, 7 | } from "./transforms/media-queries/features"; 8 | import { mediaQueryTypes } from "./transforms/media-queries/types"; 9 | import { remToPx } from "./transforms/rem"; 10 | import { allEqual } from "./utils/allEqual"; 11 | import { camelCase } from "./utils/camelCase"; 12 | import { sortRules } from "./utils/sortRules"; 13 | import { values } from "./utils/values"; 14 | 15 | const lengthRe = /^(0$|(?:[+-]?(?:\d*\.)?\d+(?:[Ee][+-]?\d+)?)(?=px|rem$))/; 16 | const viewportUnitRe = /^([+-]?[0-9.]+)(vh|vw|vmin|vmax)$/; 17 | const percentRe = /^([+-]?(?:\d*\.)?\d+(?:[Ee][+-]?\d+)?%)$/; 18 | const unsupportedUnitRe = 19 | /^([+-]?(?:\d*\.)?\d+(?:[Ee][+-]?\d+)?(ch|em|ex|cm|mm|in|pc|pt))$/; 20 | const shorthandBorderProps = [ 21 | "border-radius", 22 | "border-width", 23 | "border-color", 24 | "border-style", 25 | ]; 26 | 27 | const transformDecls = (styles, declarations, result) => { 28 | for (const d in declarations) { 29 | const declaration = declarations[d]; 30 | if (declaration.type !== "declaration") continue; 31 | 32 | const property = declaration.property; 33 | const value = remToPx(declaration.value); 34 | 35 | const isLengthUnit = lengthRe.test(value); 36 | const isViewportUnit = viewportUnitRe.test(value); 37 | const isPercent = percentRe.test(value); 38 | const isUnsupportedUnit = unsupportedUnitRe.test(value); 39 | 40 | if ( 41 | property === "line-height" && 42 | !isLengthUnit && 43 | !isViewportUnit && 44 | !isPercent && 45 | !isUnsupportedUnit 46 | ) { 47 | throw new Error(`Failed to parse declaration "${property}: ${value}"`); 48 | } 49 | 50 | if (!result.__viewportUnits && isViewportUnit) { 51 | result.__viewportUnits = true; 52 | } 53 | 54 | if (shorthandBorderProps.indexOf(property) > -1) { 55 | // transform single value shorthand border properties back to 56 | // shorthand form to support styling `Image`. 57 | const transformed = transformCSS([[property, value]]); 58 | const vals = values(transformed); 59 | if (allEqual(vals)) { 60 | const replacement = {}; 61 | replacement[camelCase(property)] = vals[0]; 62 | Object.assign(styles, replacement); 63 | } else { 64 | Object.assign(styles, transformed); 65 | } 66 | } else { 67 | Object.assign(styles, transformCSS([[property, value]])); 68 | } 69 | } 70 | }; 71 | 72 | const transform = (css, options) => { 73 | const { stylesheet } = parseCSS(css); 74 | const rules = sortRules(stylesheet.rules); 75 | 76 | const result = {}; 77 | 78 | for (const r in rules) { 79 | const rule = rules[r]; 80 | for (const s in rule.selectors) { 81 | if (rule.selectors[s] === ":export") { 82 | if (!result.__exportProps) { 83 | result.__exportProps = {}; 84 | } 85 | 86 | rule.declarations.forEach(({ property, value }) => { 87 | const isAlreadyDefinedAsClass = 88 | result[property] !== undefined && 89 | result.__exportProps[property] === undefined; 90 | 91 | if (isAlreadyDefinedAsClass) { 92 | throw new Error( 93 | `Failed to parse :export block because a CSS class in the same file is already using the name "${property}"`, 94 | ); 95 | } 96 | 97 | result.__exportProps[property] = value; 98 | }); 99 | continue; 100 | } 101 | 102 | if ( 103 | rule.selectors[s].indexOf(".") !== 0 || 104 | rule.selectors[s].indexOf(":") !== -1 || 105 | rule.selectors[s].indexOf("[") !== -1 || 106 | rule.selectors[s].indexOf("~") !== -1 || 107 | rule.selectors[s].indexOf(">") !== -1 || 108 | rule.selectors[s].indexOf("+") !== -1 || 109 | rule.selectors[s].indexOf(" ") !== -1 110 | ) { 111 | continue; 112 | } 113 | 114 | if ( 115 | typeof options?.ignoreRule === "function" && 116 | options.ignoreRule(rule.selectors[s]) === true 117 | ) { 118 | continue; 119 | } 120 | 121 | const selector = rule.selectors[s].replace(/^\./, ""); 122 | const styles = (result[selector] = result[selector] || {}); 123 | transformDecls(styles, rule.declarations, result); 124 | } 125 | 126 | if ( 127 | rule.type == "media" && 128 | options != null && 129 | options.parseMediaQueries === true 130 | ) { 131 | const parsed = mediaQuery.parse(rule.media); 132 | 133 | parsed.forEach((mq) => { 134 | if (mediaQueryTypes.indexOf(mq.type) === -1) { 135 | throw new Error(`Failed to parse media query type "${mq.type}"`); 136 | } 137 | 138 | mq.expressions.forEach((e) => { 139 | const mf = e.modifier ? `${e.modifier}-${e.feature}` : e.feature; 140 | const val = e.value ? `: ${e.value}` : ""; 141 | 142 | if (mediaQueryFeatures.indexOf(e.feature) === -1) { 143 | throw new Error(`Failed to parse media query feature "${mf}"`); 144 | } 145 | 146 | if ( 147 | dimensionFeatures.indexOf(e.feature) > -1 && 148 | lengthRe.test(e.value) === false 149 | ) { 150 | throw new Error( 151 | `Failed to parse media query expression "(${mf}${val})"`, 152 | ); 153 | } 154 | }); 155 | }); 156 | 157 | const media = "@media " + rule.media; 158 | 159 | result.__mediaQueries = result.__mediaQueries || {}; 160 | result.__mediaQueries[media] = parsed; 161 | 162 | for (const r in rule.rules) { 163 | const ruleRule = rule.rules[r]; 164 | for (const s in ruleRule.selectors) { 165 | if ( 166 | typeof options?.ignoreRule === "function" && 167 | options.ignoreRule(ruleRule.selectors[s]) === true 168 | ) { 169 | continue; 170 | } 171 | 172 | result[media] = result[media] || {}; 173 | const selector = ruleRule.selectors[s].replace(/^\./, ""); 174 | const mediaStyles = (result[media][selector] = 175 | result[media][selector] || {}); 176 | transformDecls(mediaStyles, ruleRule.declarations, result); 177 | } 178 | } 179 | } 180 | } 181 | 182 | if (result.__exportProps) { 183 | Object.assign(result, result.__exportProps); 184 | delete result.__exportProps; 185 | } 186 | 187 | return result; 188 | }; 189 | 190 | export default transform; 191 | -------------------------------------------------------------------------------- /src/utils/sortRules.spec.js: -------------------------------------------------------------------------------- 1 | import { sortRules } from "./sortRules"; 2 | 3 | describe("sortRules", () => { 4 | it("should sort :export to be the last rule", () => { 5 | expect( 6 | sortRules([ 7 | { 8 | type: "rule", 9 | selectors: [":export"], 10 | declarations: [ 11 | { 12 | type: "declaration", 13 | property: "foo", 14 | value: "1", 15 | position: { 16 | start: { line: 3, column: 9 }, 17 | end: { line: 3, column: 15 }, 18 | }, 19 | }, 20 | ], 21 | position: { 22 | start: { line: 2, column: 7 }, 23 | end: { line: 4, column: 8 }, 24 | }, 25 | }, 26 | { 27 | type: "rule", 28 | selectors: [".foo"], 29 | declarations: [ 30 | { 31 | type: "declaration", 32 | property: "color", 33 | value: "red", 34 | position: { 35 | start: { line: 7, column: 9 }, 36 | end: { line: 7, column: 19 }, 37 | }, 38 | }, 39 | ], 40 | position: { 41 | start: { line: 6, column: 7 }, 42 | end: { line: 8, column: 8 }, 43 | }, 44 | }, 45 | ]), 46 | ).toEqual([ 47 | { 48 | type: "rule", 49 | selectors: [".foo"], 50 | declarations: [ 51 | { 52 | type: "declaration", 53 | property: "color", 54 | value: "red", 55 | position: { 56 | start: { line: 7, column: 9 }, 57 | end: { line: 7, column: 19 }, 58 | }, 59 | }, 60 | ], 61 | position: { 62 | start: { line: 6, column: 7 }, 63 | end: { line: 8, column: 8 }, 64 | }, 65 | }, 66 | { 67 | type: "rule", 68 | selectors: [":export"], 69 | declarations: [ 70 | { 71 | type: "declaration", 72 | property: "foo", 73 | value: "1", 74 | position: { 75 | start: { line: 3, column: 9 }, 76 | end: { line: 3, column: 15 }, 77 | }, 78 | }, 79 | ], 80 | position: { 81 | start: { line: 2, column: 7 }, 82 | end: { line: 4, column: 8 }, 83 | }, 84 | }, 85 | ]); 86 | 87 | expect( 88 | sortRules([ 89 | { 90 | type: "rule", 91 | selectors: [".foo"], 92 | declarations: [ 93 | { 94 | type: "declaration", 95 | property: "color", 96 | value: "red", 97 | position: { 98 | start: { line: 7, column: 9 }, 99 | end: { line: 7, column: 19 }, 100 | }, 101 | }, 102 | ], 103 | position: { 104 | start: { line: 6, column: 7 }, 105 | end: { line: 8, column: 8 }, 106 | }, 107 | }, 108 | { 109 | type: "rule", 110 | selectors: [":export"], 111 | declarations: [ 112 | { 113 | type: "declaration", 114 | property: "foo", 115 | value: "1", 116 | position: { 117 | start: { line: 3, column: 9 }, 118 | end: { line: 3, column: 15 }, 119 | }, 120 | }, 121 | ], 122 | position: { 123 | start: { line: 2, column: 7 }, 124 | end: { line: 4, column: 8 }, 125 | }, 126 | }, 127 | { 128 | type: "rule", 129 | selectors: [".foo"], 130 | declarations: [ 131 | { 132 | type: "declaration", 133 | property: "color", 134 | value: "red", 135 | position: { 136 | start: { line: 7, column: 9 }, 137 | end: { line: 7, column: 19 }, 138 | }, 139 | }, 140 | ], 141 | position: { 142 | start: { line: 6, column: 7 }, 143 | end: { line: 8, column: 8 }, 144 | }, 145 | }, 146 | ]), 147 | ).toEqual([ 148 | { 149 | type: "rule", 150 | selectors: [".foo"], 151 | declarations: [ 152 | { 153 | type: "declaration", 154 | property: "color", 155 | value: "red", 156 | position: { 157 | start: { line: 7, column: 9 }, 158 | end: { line: 7, column: 19 }, 159 | }, 160 | }, 161 | ], 162 | position: { 163 | start: { line: 6, column: 7 }, 164 | end: { line: 8, column: 8 }, 165 | }, 166 | }, 167 | { 168 | type: "rule", 169 | selectors: [".foo"], 170 | declarations: [ 171 | { 172 | type: "declaration", 173 | property: "color", 174 | value: "red", 175 | position: { 176 | start: { line: 7, column: 9 }, 177 | end: { line: 7, column: 19 }, 178 | }, 179 | }, 180 | ], 181 | position: { 182 | start: { line: 6, column: 7 }, 183 | end: { line: 8, column: 8 }, 184 | }, 185 | }, 186 | { 187 | type: "rule", 188 | selectors: [":export"], 189 | declarations: [ 190 | { 191 | type: "declaration", 192 | property: "foo", 193 | value: "1", 194 | position: { 195 | start: { line: 3, column: 9 }, 196 | end: { line: 3, column: 15 }, 197 | }, 198 | }, 199 | ], 200 | position: { 201 | start: { line: 2, column: 7 }, 202 | end: { line: 4, column: 8 }, 203 | }, 204 | }, 205 | ]); 206 | }); 207 | 208 | it("should sort :export blocks to be after classes, but sorted by start line", () => { 209 | expect( 210 | sortRules([ 211 | { 212 | type: "rule", 213 | selectors: [":export"], 214 | declarations: [ 215 | { 216 | type: "declaration", 217 | property: "bar", 218 | value: "1", 219 | position: { 220 | start: { line: 11, column: 3 }, 221 | end: { line: 11, column: 9 }, 222 | }, 223 | }, 224 | { 225 | type: "declaration", 226 | property: "bar", 227 | value: "2", 228 | position: { 229 | start: { line: 12, column: 3 }, 230 | end: { line: 12, column: 9 }, 231 | }, 232 | }, 233 | ], 234 | position: { 235 | start: { line: 10, column: 1 }, 236 | end: { line: 13, column: 2 }, 237 | }, 238 | }, 239 | { 240 | type: "rule", 241 | selectors: [".foo"], 242 | declarations: [ 243 | { 244 | type: "declaration", 245 | property: "color", 246 | value: "blue", 247 | position: { 248 | start: { line: 7, column: 3 }, 249 | end: { line: 7, column: 14 }, 250 | }, 251 | }, 252 | ], 253 | position: { 254 | start: { line: 6, column: 1 }, 255 | end: { line: 8, column: 2 }, 256 | }, 257 | }, 258 | { 259 | type: "rule", 260 | selectors: [":export"], 261 | declarations: [ 262 | { 263 | type: "declaration", 264 | property: "bar", 265 | value: "3", 266 | position: { 267 | start: { line: 3, column: 3 }, 268 | end: { line: 3, column: 9 }, 269 | }, 270 | }, 271 | ], 272 | position: { 273 | start: { line: 2, column: 1 }, 274 | end: { line: 4, column: 2 }, 275 | }, 276 | }, 277 | ]), 278 | ).toEqual([ 279 | { 280 | declarations: [ 281 | { 282 | position: { 283 | end: { column: 14, line: 7 }, 284 | start: { column: 3, line: 7 }, 285 | }, 286 | property: "color", 287 | type: "declaration", 288 | value: "blue", 289 | }, 290 | ], 291 | position: { 292 | end: { column: 2, line: 8 }, 293 | start: { column: 1, line: 6 }, 294 | }, 295 | selectors: [".foo"], 296 | type: "rule", 297 | }, 298 | { 299 | declarations: [ 300 | { 301 | position: { 302 | end: { column: 9, line: 3 }, 303 | start: { column: 3, line: 3 }, 304 | }, 305 | property: "bar", 306 | type: "declaration", 307 | value: "3", 308 | }, 309 | ], 310 | position: { 311 | end: { column: 2, line: 4 }, 312 | start: { column: 1, line: 2 }, 313 | }, 314 | selectors: [":export"], 315 | type: "rule", 316 | }, 317 | { 318 | declarations: [ 319 | { 320 | position: { 321 | end: { column: 9, line: 11 }, 322 | start: { column: 3, line: 11 }, 323 | }, 324 | property: "bar", 325 | type: "declaration", 326 | value: "1", 327 | }, 328 | { 329 | position: { 330 | end: { column: 9, line: 12 }, 331 | start: { column: 3, line: 12 }, 332 | }, 333 | property: "bar", 334 | type: "declaration", 335 | value: "2", 336 | }, 337 | ], 338 | position: { 339 | end: { column: 2, line: 13 }, 340 | start: { column: 1, line: 10 }, 341 | }, 342 | selectors: [":export"], 343 | type: "rule", 344 | }, 345 | ]); 346 | }); 347 | 348 | it("should do nothing with an empty array", () => { 349 | expect(sortRules([])).toEqual([]); 350 | }); 351 | }); 352 | -------------------------------------------------------------------------------- /src/index.spec.js: -------------------------------------------------------------------------------- 1 | import transform from "./index"; 2 | 3 | describe("misc", () => { 4 | it("returns empty object when input is empty", () => { 5 | expect(transform(``)).toEqual({}); 6 | }); 7 | 8 | it("transforms numbers", () => { 9 | expect( 10 | transform(` 11 | .test { 12 | z-index: 0 13 | } 14 | `), 15 | ).toEqual({ test: { zIndex: 0 } }); 16 | }); 17 | 18 | it("ignores unsupported at-rules", () => { 19 | expect(transform(`@charset "utf-8";`)).toEqual({}); 20 | expect( 21 | transform(` 22 | @supports (display: grid) { 23 | div { 24 | display: grid; 25 | } 26 | } 27 | `), 28 | ).toEqual({}); 29 | }); 30 | 31 | it("allows pixels in unspecialized transform", () => { 32 | expect( 33 | transform(` 34 | .test { 35 | top: 0px; 36 | } 37 | `), 38 | ).toEqual({ 39 | test: { top: 0 }, 40 | }); 41 | }); 42 | 43 | it("allows percent in unspecialized transform", () => { 44 | expect( 45 | transform(` 46 | .test { 47 | top: 0%; 48 | } 49 | `), 50 | ).toEqual({ 51 | test: { top: "0%" }, 52 | }); 53 | }); 54 | 55 | it("allows decimal values", () => { 56 | expect( 57 | transform(` 58 | .test { 59 | margin-top: 0.5px; 60 | } 61 | `), 62 | ).toEqual({ 63 | test: { marginTop: 0.5 }, 64 | }); 65 | expect( 66 | transform(` 67 | .test { 68 | margin-top: 100.5px; 69 | } 70 | `), 71 | ).toEqual({ 72 | test: { marginTop: 100.5 }, 73 | }); 74 | expect( 75 | transform(` 76 | .test { 77 | margin-top: -0.5px; 78 | } 79 | `), 80 | ).toEqual({ 81 | test: { marginTop: -0.5 }, 82 | }); 83 | expect( 84 | transform(` 85 | .test { 86 | margin-top: -100.5px; 87 | } 88 | `), 89 | ).toEqual({ 90 | test: { marginTop: -100.5 }, 91 | }); 92 | expect( 93 | transform(` 94 | .test { 95 | margin-top: .5px; 96 | } 97 | `), 98 | ).toEqual({ 99 | test: { marginTop: 0.5 }, 100 | }); 101 | expect( 102 | transform(` 103 | .test { 104 | margin-top: -.5px; 105 | } 106 | `), 107 | ).toEqual({ 108 | test: { marginTop: -0.5 }, 109 | }); 110 | }); 111 | 112 | it("allows decimal values in transformed values", () => { 113 | expect( 114 | transform(` 115 | .test { 116 | border-radius: 1.5px; 117 | } 118 | `), 119 | ).toEqual({ 120 | test: { 121 | borderRadius: 1.5, 122 | }, 123 | }); 124 | }); 125 | 126 | it("allows negative values in transformed values", () => { 127 | expect( 128 | transform(` 129 | .test { 130 | border-radius: -1.5px; 131 | } 132 | `), 133 | ).toEqual({ 134 | test: { 135 | borderRadius: -1.5, 136 | }, 137 | }); 138 | }); 139 | 140 | it("allows uppercase units", () => { 141 | expect( 142 | transform(` 143 | .test { 144 | top: 0PX 145 | } 146 | `), 147 | ).toEqual({ test: { top: 0 } }); 148 | expect( 149 | transform(` 150 | .test { 151 | transform: rotate(30DEG) 152 | } 153 | `), 154 | ).toEqual({ 155 | test: { 156 | transform: [{ rotate: "30deg" }], 157 | }, 158 | }); 159 | }); 160 | 161 | it("allows percent values in transformed values", () => { 162 | expect( 163 | transform(` 164 | .test { 165 | margin: 10%; 166 | } 167 | `), 168 | ).toEqual({ 169 | test: { 170 | marginTop: "10%", 171 | marginRight: "10%", 172 | marginBottom: "10%", 173 | marginLeft: "10%", 174 | }, 175 | }); 176 | }); 177 | 178 | it("allows color values in transformed border-color values", () => { 179 | expect( 180 | transform(` 181 | .test { 182 | border-color: red 183 | } 184 | `), 185 | ).toEqual({ 186 | test: { 187 | borderColor: "red", 188 | }, 189 | }); 190 | }); 191 | 192 | it("allows omitting units for 0", () => { 193 | expect( 194 | transform(` 195 | .test { 196 | margin: 10px 0; 197 | } 198 | `), 199 | ).toEqual({ 200 | test: { 201 | marginTop: 10, 202 | marginRight: 0, 203 | marginBottom: 10, 204 | marginLeft: 0, 205 | }, 206 | }); 207 | }); 208 | 209 | it("converts to camel-case", () => { 210 | expect( 211 | transform(` 212 | .test { 213 | background-color: red; 214 | } 215 | `), 216 | ).toEqual({ 217 | test: { 218 | backgroundColor: "red", 219 | }, 220 | }); 221 | }); 222 | 223 | it("transforms shadow offsets", () => { 224 | expect( 225 | transform(` 226 | .test { 227 | shadow-offset: 10px 5px; 228 | } 229 | `), 230 | ).toEqual({ 231 | test: { shadowOffset: { width: 10, height: 5 } }, 232 | }); 233 | }); 234 | 235 | it("transforms text shadow offsets", () => { 236 | expect( 237 | transform(` 238 | .test { 239 | text-shadow-offset: 10px 5px; 240 | } 241 | `), 242 | ).toEqual({ 243 | test: { textShadowOffset: { width: 10, height: 5 } }, 244 | }); 245 | }); 246 | 247 | it("transforms a block of css", () => { 248 | expect( 249 | transform(` 250 | .description { 251 | margin-bottom: 20px; 252 | font-size: 18px; 253 | text-align: center; 254 | color: #656656; 255 | box-shadow: 10px 20px 30px #fff; 256 | } 257 | 258 | .container { 259 | padding: 30px; 260 | margin-top: 65px; 261 | align-items: center; 262 | border: 2px dashed #f00; 263 | } 264 | `), 265 | ).toEqual({ 266 | description: { 267 | marginBottom: 20, 268 | fontSize: 18, 269 | textAlign: "center", 270 | color: "#656656", 271 | shadowColor: "#fff", 272 | shadowOffset: { height: 20, width: 10 }, 273 | shadowRadius: 30, 274 | shadowOpacity: 1, 275 | }, 276 | container: { 277 | paddingBottom: 30, 278 | paddingLeft: 30, 279 | paddingRight: 30, 280 | paddingTop: 30, 281 | marginTop: 65, 282 | alignItems: "center", 283 | borderColor: "#f00", 284 | borderStyle: "dashed", 285 | borderWidth: 2, 286 | }, 287 | }); 288 | }); 289 | 290 | it("throws useful errors", () => { 291 | expect(() => { 292 | transform(` 293 | .test { 294 | margin: 10; 295 | } 296 | `); 297 | }).toThrowError('Failed to parse declaration "margin: 10"'); 298 | }); 299 | 300 | it("when there are selectors with the same name, merges the common props", () => { 301 | expect( 302 | transform(` 303 | .test { 304 | margin: 10px; 305 | background-color: #f00; 306 | } 307 | .test { 308 | padding: 10px; 309 | font-size: 20px; 310 | margin: 5px; 311 | } 312 | `), 313 | ).toEqual({ 314 | test: { 315 | backgroundColor: "#f00", 316 | paddingBottom: 10, 317 | paddingLeft: 10, 318 | paddingRight: 10, 319 | paddingTop: 10, 320 | fontSize: 20, 321 | marginBottom: 5, 322 | marginLeft: 5, 323 | marginRight: 5, 324 | marginTop: 5, 325 | }, 326 | }); 327 | }); 328 | 329 | it("supports group of selectors", () => { 330 | expect( 331 | transform(` 332 | .test1, .test2 { 333 | color: red; 334 | } 335 | `), 336 | ).toEqual({ 337 | test1: { 338 | color: "red", 339 | }, 340 | test2: { 341 | color: "red", 342 | }, 343 | }); 344 | }); 345 | }); 346 | 347 | describe("selectors", () => { 348 | it("supports dash in class names", () => { 349 | expect( 350 | transform(` 351 | .test-1-2 { 352 | color: red; 353 | } 354 | `), 355 | ).toEqual({ 356 | "test-1-2": { 357 | color: "red", 358 | }, 359 | }); 360 | }); 361 | 362 | it("supports underscore in class names", () => { 363 | expect( 364 | transform(` 365 | .test_1 { 366 | color: red; 367 | } 368 | `), 369 | ).toEqual({ 370 | test_1: { 371 | color: "red", 372 | }, 373 | }); 374 | }); 375 | 376 | it("supports grouping selectors", () => { 377 | expect( 378 | transform(` 379 | .test, .test2, .test3 { 380 | color: red; 381 | } 382 | `), 383 | ).toEqual({ 384 | test: { 385 | color: "red", 386 | }, 387 | test2: { 388 | color: "red", 389 | }, 390 | test3: { 391 | color: "red", 392 | }, 393 | }); 394 | }); 395 | 396 | it("ignores grouping of ID selectors", () => { 397 | expect( 398 | transform(` 399 | .test { 400 | color: red; 401 | } 402 | #test1, #test2, #test3 { 403 | color: red; 404 | } 405 | `), 406 | ).toEqual({ 407 | test: { 408 | color: "red", 409 | }, 410 | }); 411 | }); 412 | 413 | it("ignores grouping of element selectors", () => { 414 | expect( 415 | transform(` 416 | .test { 417 | color: red; 418 | } 419 | p, h1, input { 420 | color: red; 421 | } 422 | `), 423 | ).toEqual({ 424 | test: { 425 | color: "red", 426 | }, 427 | }); 428 | }); 429 | 430 | it("ignores ID selectors", () => { 431 | expect( 432 | transform(` 433 | .test { 434 | color: red; 435 | } 436 | #foo { 437 | color: blue; 438 | } 439 | `), 440 | ).toEqual({ 441 | test: { 442 | color: "red", 443 | }, 444 | }); 445 | }); 446 | 447 | it("ignores type selectors", () => { 448 | expect( 449 | transform(` 450 | .test { 451 | color: red; 452 | } 453 | input[type=text] { 454 | color: blue; 455 | } 456 | `), 457 | ).toEqual({ 458 | test: { 459 | color: "red", 460 | }, 461 | }); 462 | expect( 463 | transform(` 464 | .test { 465 | color: red; 466 | } 467 | [class^="test"] { 468 | color: blue; 469 | } 470 | `), 471 | ).toEqual({ 472 | test: { 473 | color: "red", 474 | }, 475 | }); 476 | expect( 477 | transform(` 478 | .test { 479 | color: red; 480 | } 481 | .foo[class^="test"] { 482 | color: blue; 483 | } 484 | `), 485 | ).toEqual({ 486 | test: { 487 | color: "red", 488 | }, 489 | }); 490 | }); 491 | 492 | it("ignores universal selectors", () => { 493 | expect( 494 | transform(` 495 | .test { 496 | color: red; 497 | } 498 | * { 499 | color: blue; 500 | } 501 | `), 502 | ).toEqual({ 503 | test: { 504 | color: "red", 505 | }, 506 | }); 507 | }); 508 | 509 | it("ignores descendant selectors", () => { 510 | expect( 511 | transform(` 512 | .test { 513 | color: red; 514 | } 515 | .foo .bar { 516 | color: blue; 517 | } 518 | `), 519 | ).toEqual({ 520 | test: { 521 | color: "red", 522 | }, 523 | }); 524 | }); 525 | 526 | it("ignores direct child selectors", () => { 527 | expect( 528 | transform(` 529 | .test { 530 | color: red; 531 | } 532 | .foo > .bar { 533 | color: blue; 534 | } 535 | `), 536 | ).toEqual({ 537 | test: { 538 | color: "red", 539 | }, 540 | }); 541 | }); 542 | 543 | it("ignores adjancent sibling selectors", () => { 544 | expect( 545 | transform(` 546 | .test { 547 | color: red; 548 | } 549 | .foo + .bar { 550 | color: blue; 551 | } 552 | `), 553 | ).toEqual({ 554 | test: { 555 | color: "red", 556 | }, 557 | }); 558 | }); 559 | 560 | it("ignores general sibling selectors", () => { 561 | expect( 562 | transform(` 563 | .test { 564 | color: red; 565 | } 566 | .foo ~ .bar { 567 | color: blue; 568 | } 569 | `), 570 | ).toEqual({ 571 | test: { 572 | color: "red", 573 | }, 574 | }); 575 | }); 576 | 577 | it("ignores qualified selectors", () => { 578 | expect( 579 | transform(` 580 | .test { 581 | color: red; 582 | } 583 | p.bar { 584 | color: blue; 585 | } 586 | `), 587 | ).toEqual({ 588 | test: { 589 | color: "red", 590 | }, 591 | }); 592 | }); 593 | 594 | it("ignores element selectors", () => { 595 | expect( 596 | transform(` 597 | .test { 598 | color: red; 599 | } 600 | p { 601 | color: blue; 602 | } 603 | `), 604 | ).toEqual({ 605 | test: { 606 | color: "red", 607 | }, 608 | }); 609 | }); 610 | 611 | it("ignores pseudo selectors", () => { 612 | expect( 613 | transform(` 614 | .test { 615 | color: red; 616 | } 617 | .test1:hover { 618 | color: blue; 619 | } 620 | .test2::before { 621 | color: blue; 622 | } 623 | `), 624 | ).toEqual({ 625 | test: { 626 | color: "red", 627 | }, 628 | }); 629 | }); 630 | }); 631 | 632 | describe("colors", () => { 633 | it("transforms named colors", () => { 634 | expect( 635 | transform(` 636 | .test { 637 | color: red; 638 | } 639 | `), 640 | ).toEqual({ 641 | test: { 642 | color: "red", 643 | }, 644 | }); 645 | }); 646 | 647 | it("transforms hex colors", () => { 648 | expect( 649 | transform(` 650 | .test { 651 | color: #f00; 652 | } 653 | `), 654 | ).toEqual({ 655 | test: { 656 | color: "#f00", 657 | }, 658 | }); 659 | }); 660 | 661 | it("transforms rgb colors", () => { 662 | expect( 663 | transform(` 664 | .test { 665 | color: rgb(255, 0, 0); 666 | } 667 | `), 668 | ).toEqual({ 669 | test: { 670 | color: "rgb(255, 0, 0)", 671 | }, 672 | }); 673 | }); 674 | 675 | it("transforms rgba colors", () => { 676 | expect( 677 | transform(` 678 | .test { 679 | color: rgba(255, 0, 0, 0); 680 | } 681 | `), 682 | ).toEqual({ 683 | test: { 684 | color: "rgba(255, 0, 0, 0)", 685 | }, 686 | }); 687 | }); 688 | }); 689 | 690 | describe("transform", () => { 691 | it("transforms a single transform value with number", () => { 692 | expect( 693 | transform(` 694 | .test { 695 | transform: scaleX(5); 696 | } 697 | `), 698 | ).toEqual({ 699 | test: { transform: [{ scaleX: 5 }] }, 700 | }); 701 | }); 702 | 703 | it("transforms a single transform value with string", () => { 704 | expect( 705 | transform(` 706 | .test { 707 | transform: rotate(5deg); 708 | } 709 | `), 710 | ).toEqual({ 711 | test: { transform: [{ rotate: "5deg" }] }, 712 | }); 713 | }); 714 | 715 | it("transforms multiple transform values", () => { 716 | expect( 717 | transform(` 718 | .test { 719 | transform: scaleX(5) skewX(1deg); 720 | } 721 | `), 722 | ).toEqual({ 723 | test: { transform: [{ skewX: "1deg" }, { scaleX: 5 }] }, 724 | }); 725 | }); 726 | 727 | it("transforms scale(number, number) to scaleX and scaleY", () => { 728 | expect( 729 | transform(` 730 | .test { 731 | transform: scale(2, 3); 732 | } 733 | `), 734 | ).toEqual({ 735 | test: { transform: [{ scaleY: 3 }, { scaleX: 2 }] }, 736 | }); 737 | }); 738 | 739 | it("transforms translate(length, length) to translateX and translateY", () => { 740 | expect( 741 | transform(` 742 | .test { 743 | transform: translate(2px, 3px); 744 | } 745 | `), 746 | ).toEqual({ 747 | test: { transform: [{ translateY: 3 }, { translateX: 2 }] }, 748 | }); 749 | }); 750 | 751 | it("transforms translate(length) to translateX and translateY", () => { 752 | expect( 753 | transform(` 754 | .test { 755 | transform: translate(5px); 756 | } 757 | `), 758 | ).toEqual({ 759 | test: { transform: [{ translateY: 0 }, { translateX: 5 }] }, 760 | }); 761 | }); 762 | 763 | it("transforms skew(angle, angle) to skewX and skewY", () => { 764 | expect( 765 | transform(` 766 | .test { 767 | transform: skew(2deg, 3deg); 768 | } 769 | `), 770 | ).toEqual({ 771 | test: { transform: [{ skewY: "3deg" }, { skewX: "2deg" }] }, 772 | }); 773 | }); 774 | 775 | it("transforms skew(angle) to skewX and skewY", () => { 776 | expect( 777 | transform(` 778 | .test { 779 | transform: skew(5deg); 780 | } 781 | `), 782 | ).toEqual({ 783 | test: { transform: [{ skewY: "0deg" }, { skewX: "5deg" }] }, 784 | }); 785 | }); 786 | }); 787 | 788 | describe("border", () => { 789 | it("transforms border none", () => { 790 | expect( 791 | transform(` 792 | .test { 793 | border: none 794 | } 795 | `), 796 | ).toEqual({ 797 | test: { 798 | borderWidth: 0, 799 | borderColor: "black", 800 | borderStyle: "solid", 801 | }, 802 | }); 803 | }); 804 | 805 | it("transforms border shorthand", () => { 806 | expect( 807 | transform(` 808 | .test { 809 | border: 2px dashed #f00; 810 | } 811 | `), 812 | ).toEqual({ 813 | test: { borderWidth: 2, borderColor: "#f00", borderStyle: "dashed" }, 814 | }); 815 | }); 816 | 817 | it("transforms border shorthand in other order", () => { 818 | expect( 819 | transform(` 820 | .test { 821 | border: #f00 2px dashed; 822 | } 823 | `), 824 | ).toEqual({ 825 | test: { borderWidth: 2, borderColor: "#f00", borderStyle: "dashed" }, 826 | }); 827 | }); 828 | 829 | it("transforms border shorthand missing color", () => { 830 | expect( 831 | transform(` 832 | .test { 833 | border: 2px dashed; 834 | } 835 | `), 836 | ).toEqual({ 837 | test: { borderWidth: 2, borderColor: "black", borderStyle: "dashed" }, 838 | }); 839 | }); 840 | 841 | it("transforms border shorthand missing style", () => { 842 | expect( 843 | transform(` 844 | .test { 845 | border: 2px #f00; 846 | } 847 | `), 848 | ).toEqual({ 849 | test: { borderWidth: 2, borderColor: "#f00", borderStyle: "solid" }, 850 | }); 851 | }); 852 | 853 | it("transforms border shorthand missing width", () => { 854 | expect( 855 | transform(` 856 | .test { 857 | border: #f00 dashed; 858 | } 859 | `), 860 | ).toEqual({ 861 | test: { borderWidth: 1, borderColor: "#f00", borderStyle: "dashed" }, 862 | }); 863 | }); 864 | 865 | it("transforms border shorthand missing color & width", () => { 866 | expect( 867 | transform(` 868 | .test { 869 | border: dashed; 870 | } 871 | `), 872 | ).toEqual({ 873 | test: { borderWidth: 1, borderColor: "black", borderStyle: "dashed" }, 874 | }); 875 | }); 876 | 877 | it("transforms border shorthand missing style & width", () => { 878 | expect( 879 | transform(` 880 | .test { 881 | border: #f00; 882 | } 883 | `), 884 | ).toEqual({ 885 | test: { borderWidth: 1, borderColor: "#f00", borderStyle: "solid" }, 886 | }); 887 | }); 888 | 889 | it("transforms border shorthand missing color & style", () => { 890 | expect( 891 | transform(` 892 | .test { 893 | border: 2px; 894 | } 895 | `), 896 | ).toEqual({ 897 | test: { borderWidth: 2, borderColor: "black", borderStyle: "solid" }, 898 | }); 899 | }); 900 | 901 | it("transforms border for unsupported units", () => { 902 | expect( 903 | transform(` 904 | .test { 905 | border: 3em solid black 906 | } 907 | `), 908 | ).toEqual({ 909 | test: { 910 | borderWidth: "3em", 911 | borderColor: "black", 912 | borderStyle: "solid", 913 | }, 914 | }); 915 | }); 916 | 917 | it("does not transform border with percentage width", () => { 918 | expect(() => 919 | transform(` 920 | .test { 921 | border: 3% solid black 922 | } 923 | `), 924 | ).toThrow(); 925 | }); 926 | 927 | describe("shorthand border properties related to Image elements", () => { 928 | it("transforms border-radius", () => { 929 | expect( 930 | transform(` 931 | .test { 932 | border-radius: 6px; 933 | } 934 | `), 935 | ).toEqual({ 936 | test: { borderRadius: 6 }, 937 | }); 938 | }); 939 | 940 | it("transforms border-radius with multiple values", () => { 941 | expect( 942 | transform(` 943 | .test { 944 | border-radius: 10px 5%; 945 | } 946 | `), 947 | ).toEqual({ 948 | test: { 949 | borderBottomLeftRadius: "5%", 950 | borderBottomRightRadius: 10, 951 | borderTopLeftRadius: 10, 952 | borderTopRightRadius: "5%", 953 | }, 954 | }); 955 | expect( 956 | transform(` 957 | .test { 958 | border-radius: 2px 4px 2px; 959 | } 960 | `), 961 | ).toEqual({ 962 | test: { 963 | borderBottomLeftRadius: 4, 964 | borderBottomRightRadius: 2, 965 | borderTopLeftRadius: 2, 966 | borderTopRightRadius: 4, 967 | }, 968 | }); 969 | expect( 970 | transform(` 971 | .test { 972 | border-radius: 1px 0 3px 4px; 973 | } 974 | `), 975 | ).toEqual({ 976 | test: { 977 | borderBottomLeftRadius: 4, 978 | borderBottomRightRadius: 3, 979 | borderTopLeftRadius: 1, 980 | borderTopRightRadius: 0, 981 | }, 982 | }); 983 | }); 984 | 985 | it("transforms border-color", () => { 986 | expect( 987 | transform(` 988 | .test { 989 | border-color: #fff; 990 | } 991 | `), 992 | ).toEqual({ 993 | test: { borderColor: "#fff" }, 994 | }); 995 | }); 996 | 997 | it("transforms border-color with multiple values", () => { 998 | expect( 999 | transform(` 1000 | .test { 1001 | border-color: red #f015ca; 1002 | } 1003 | `), 1004 | ).toEqual({ 1005 | test: { 1006 | borderTopColor: "red", 1007 | borderRightColor: "#f015ca", 1008 | borderBottomColor: "red", 1009 | borderLeftColor: "#f015ca", 1010 | }, 1011 | }); 1012 | expect( 1013 | transform(` 1014 | .test { 1015 | border-color: red yellow green; 1016 | } 1017 | `), 1018 | ).toEqual({ 1019 | test: { 1020 | borderTopColor: "red", 1021 | borderRightColor: "yellow", 1022 | borderBottomColor: "green", 1023 | borderLeftColor: "yellow", 1024 | }, 1025 | }); 1026 | expect( 1027 | transform(` 1028 | .test { 1029 | border-color: red yellow green blue; 1030 | } 1031 | `), 1032 | ).toEqual({ 1033 | test: { 1034 | borderTopColor: "red", 1035 | borderRightColor: "yellow", 1036 | borderBottomColor: "green", 1037 | borderLeftColor: "blue", 1038 | }, 1039 | }); 1040 | }); 1041 | 1042 | it("transforms border-color with hex color", () => { 1043 | expect( 1044 | transform(` 1045 | .test { 1046 | border-color: #f00; 1047 | } 1048 | `), 1049 | ).toEqual({ 1050 | test: { 1051 | borderColor: "#f00", 1052 | }, 1053 | }); 1054 | }); 1055 | 1056 | it("transforms border-color with rgb color", () => { 1057 | expect( 1058 | transform(` 1059 | .test { 1060 | border-color: rgb(255, 0, 0); 1061 | } 1062 | `), 1063 | ).toEqual({ 1064 | test: { 1065 | borderColor: "rgb(255, 0, 0)", 1066 | }, 1067 | }); 1068 | }); 1069 | 1070 | it("transforms border-color with rgba color", () => { 1071 | expect( 1072 | transform(` 1073 | .test { 1074 | border-color: rgba(255, 0, 0, 0.1); 1075 | } 1076 | `), 1077 | ).toEqual({ 1078 | test: { 1079 | borderColor: "rgba(255, 0, 0, 0.1)", 1080 | }, 1081 | }); 1082 | }); 1083 | 1084 | it("transforms border-width", () => { 1085 | expect( 1086 | transform(` 1087 | .test { 1088 | border-width: 4px; 1089 | } 1090 | `), 1091 | ).toEqual({ 1092 | test: { borderWidth: 4 }, 1093 | }); 1094 | }); 1095 | 1096 | it("transforms border-width with multiple values", () => { 1097 | expect( 1098 | transform(` 1099 | .test { 1100 | border-width: 2px 1.5rem; 1101 | } 1102 | `), 1103 | ).toEqual({ 1104 | test: { 1105 | borderTopWidth: 2, 1106 | borderRightWidth: 24, 1107 | borderBottomWidth: 2, 1108 | borderLeftWidth: 24, 1109 | }, 1110 | }); 1111 | expect( 1112 | transform(` 1113 | .test { 1114 | border-width: 1px 2rem 1.5rem; 1115 | } 1116 | `), 1117 | ).toEqual({ 1118 | test: { 1119 | borderTopWidth: 1, 1120 | borderRightWidth: 32, 1121 | borderBottomWidth: 24, 1122 | borderLeftWidth: 32, 1123 | }, 1124 | }); 1125 | expect( 1126 | transform(` 1127 | .test { 1128 | border-width: 1px 2rem 0 4rem; 1129 | } 1130 | `), 1131 | ).toEqual({ 1132 | test: { 1133 | borderTopWidth: 1, 1134 | borderRightWidth: 32, 1135 | borderBottomWidth: 0, 1136 | borderLeftWidth: 64, 1137 | }, 1138 | }); 1139 | }); 1140 | 1141 | it("transforms border-style", () => { 1142 | expect( 1143 | transform(` 1144 | .test { 1145 | border-style: solid; 1146 | } 1147 | `), 1148 | ).toEqual({ 1149 | test: { borderStyle: "solid" }, 1150 | }); 1151 | }); 1152 | }); 1153 | }); 1154 | 1155 | describe("font", () => { 1156 | it("transforms font weights as strings", () => { 1157 | expect( 1158 | transform(` 1159 | .test { 1160 | font-weight: 400 1161 | } 1162 | `), 1163 | ).toEqual({ 1164 | test: { fontWeight: "400" }, 1165 | }); 1166 | }); 1167 | 1168 | it("transforms font variant as an array", () => { 1169 | expect( 1170 | transform(` 1171 | .test { 1172 | font-variant: tabular-nums; 1173 | } 1174 | `), 1175 | ).toEqual({ 1176 | test: { fontVariant: ["tabular-nums"] }, 1177 | }); 1178 | }); 1179 | 1180 | it("transforms multiple font variant as an array", () => { 1181 | expect( 1182 | transform(` 1183 | .test { 1184 | font-variant: tabular-nums oldstyle-nums; 1185 | } 1186 | `), 1187 | ).toEqual({ 1188 | test: { fontVariant: ["tabular-nums", "oldstyle-nums"] }, 1189 | }); 1190 | }); 1191 | }); 1192 | 1193 | describe("background", () => { 1194 | it("transforms background to backgroundColor", () => { 1195 | expect( 1196 | transform(` 1197 | .test { 1198 | background: #f00; 1199 | } 1200 | `), 1201 | ).toEqual({ 1202 | test: { 1203 | backgroundColor: "#f00", 1204 | }, 1205 | }); 1206 | }); 1207 | 1208 | it("transforms background to backgroundColor with rgb", () => { 1209 | expect( 1210 | transform(` 1211 | .test { 1212 | background: rgb(255, 0, 0); 1213 | } 1214 | `), 1215 | ).toEqual({ 1216 | test: { 1217 | backgroundColor: "rgb(255, 0, 0)", 1218 | }, 1219 | }); 1220 | }); 1221 | 1222 | it("transforms background to backgroundColor with named colour", () => { 1223 | expect( 1224 | transform(` 1225 | .test { 1226 | background: red; 1227 | } 1228 | `), 1229 | ).toEqual({ 1230 | test: { 1231 | backgroundColor: "red", 1232 | }, 1233 | }); 1234 | }); 1235 | }); 1236 | 1237 | describe("line-height", () => { 1238 | it("transforms line-height with value and unit", () => { 1239 | expect( 1240 | transform(` 1241 | .test { 1242 | line-height: 1.5px; 1243 | } 1244 | `), 1245 | ).toEqual({ 1246 | test: { 1247 | lineHeight: 1.5, 1248 | }, 1249 | }); 1250 | }); 1251 | it("transforms line-height with rem unit", () => { 1252 | expect( 1253 | transform(` 1254 | .test { 1255 | line-height: 2rem; 1256 | } 1257 | `), 1258 | ).toEqual({ 1259 | test: { 1260 | lineHeight: 32, 1261 | }, 1262 | }); 1263 | }); 1264 | it("transforms line-height with %", () => { 1265 | expect( 1266 | transform(` 1267 | .test { 1268 | line-height: 150%; 1269 | } 1270 | `), 1271 | ).toEqual({ 1272 | test: { 1273 | lineHeight: "150%", 1274 | }, 1275 | }); 1276 | }); 1277 | it("transforms line-height with pt unit", () => { 1278 | expect( 1279 | transform(` 1280 | .test { 1281 | line-height: 2pt; 1282 | } 1283 | `), 1284 | ).toEqual({ 1285 | test: { 1286 | lineHeight: "2pt", 1287 | }, 1288 | }); 1289 | }); 1290 | it("transforms line-height with viewport unit", () => { 1291 | expect( 1292 | transform(` 1293 | .test { 1294 | line-height: 2vh; 1295 | } 1296 | `), 1297 | ).toEqual({ 1298 | __viewportUnits: true, 1299 | test: { 1300 | lineHeight: "2vh", 1301 | }, 1302 | }); 1303 | }); 1304 | it("throws for line-height with multiplier", () => { 1305 | expect(() => 1306 | transform(` 1307 | .test { 1308 | line-height: 1.5; 1309 | } 1310 | `), 1311 | ).toThrow('Failed to parse declaration "line-height: 1.5"'); 1312 | }); 1313 | }); 1314 | 1315 | describe("margin", () => { 1316 | it("transforms margin shorthands using 4 values", () => { 1317 | expect( 1318 | transform(` 1319 | .test { 1320 | margin: 10px 20px 30px 40px; 1321 | } 1322 | `), 1323 | ).toEqual({ 1324 | test: { 1325 | marginTop: 10, 1326 | marginRight: 20, 1327 | marginBottom: 30, 1328 | marginLeft: 40, 1329 | }, 1330 | }); 1331 | }); 1332 | 1333 | it("transforms margin shorthands using 3 values", () => { 1334 | expect( 1335 | transform(` 1336 | .test { 1337 | margin: 10px 20px 30px; 1338 | } 1339 | `), 1340 | ).toEqual({ 1341 | test: { 1342 | marginTop: 10, 1343 | marginRight: 20, 1344 | marginBottom: 30, 1345 | marginLeft: 20, 1346 | }, 1347 | }); 1348 | }); 1349 | 1350 | it("transforms margin shorthands using 2 values", () => { 1351 | expect( 1352 | transform(` 1353 | .test { 1354 | margin: 10px 20px; 1355 | } 1356 | `), 1357 | ).toEqual({ 1358 | test: { 1359 | marginTop: 10, 1360 | marginRight: 20, 1361 | marginBottom: 10, 1362 | marginLeft: 20, 1363 | }, 1364 | }); 1365 | }); 1366 | 1367 | it("transforms margin shorthands using 1 value", () => { 1368 | expect( 1369 | transform(` 1370 | .test { 1371 | margin: 10px; 1372 | } 1373 | `), 1374 | ).toEqual({ 1375 | test: { 1376 | marginTop: 10, 1377 | marginRight: 10, 1378 | marginBottom: 10, 1379 | marginLeft: 10, 1380 | }, 1381 | }); 1382 | }); 1383 | 1384 | it("shorthand with 1 value should override previous values", () => { 1385 | expect( 1386 | transform(` 1387 | .test { 1388 | margin-top: 2px; 1389 | margin: 1px; 1390 | } 1391 | `), 1392 | ).toEqual({ 1393 | test: { marginTop: 1, marginRight: 1, marginBottom: 1, marginLeft: 1 }, 1394 | }); 1395 | }); 1396 | 1397 | it("transforms margin shorthand with auto", () => { 1398 | expect( 1399 | transform(` 1400 | .test { 1401 | margin: auto; 1402 | } 1403 | `), 1404 | ).toEqual({ 1405 | test: { 1406 | marginTop: "auto", 1407 | marginRight: "auto", 1408 | marginBottom: "auto", 1409 | marginLeft: "auto", 1410 | }, 1411 | }); 1412 | expect( 1413 | transform(` 1414 | .test { 1415 | margin: 0 auto; 1416 | } 1417 | `), 1418 | ).toEqual({ 1419 | test: { 1420 | marginTop: 0, 1421 | marginRight: "auto", 1422 | marginBottom: 0, 1423 | marginLeft: "auto", 1424 | }, 1425 | }); 1426 | expect( 1427 | transform(` 1428 | .test { 1429 | margin: auto 0; 1430 | } 1431 | `), 1432 | ).toEqual({ 1433 | test: { 1434 | marginTop: "auto", 1435 | marginRight: 0, 1436 | marginBottom: "auto", 1437 | marginLeft: 0, 1438 | }, 1439 | }); 1440 | expect( 1441 | transform(` 1442 | .test { 1443 | margin: 2px 3px auto; 1444 | } 1445 | `), 1446 | ).toEqual({ 1447 | test: { 1448 | marginTop: 2, 1449 | marginRight: 3, 1450 | marginBottom: "auto", 1451 | marginLeft: 3, 1452 | }, 1453 | }); 1454 | expect( 1455 | transform(` 1456 | .test { 1457 | margin: 10px auto 4px; 1458 | } 1459 | `), 1460 | ).toEqual({ 1461 | test: { 1462 | marginTop: 10, 1463 | marginRight: "auto", 1464 | marginBottom: 4, 1465 | marginLeft: "auto", 1466 | }, 1467 | }); 1468 | }); 1469 | }); 1470 | 1471 | describe("text-decoration", () => { 1472 | it("transforms text-decoration into text-decoration- properties", () => { 1473 | expect( 1474 | transform(` 1475 | .test { 1476 | text-decoration: underline dotted red; 1477 | } 1478 | `), 1479 | ).toEqual({ 1480 | test: { 1481 | textDecorationLine: "underline", 1482 | textDecorationStyle: "dotted", 1483 | textDecorationColor: "red", 1484 | }, 1485 | }); 1486 | }); 1487 | 1488 | it("transforms text-decoration without color", () => { 1489 | expect( 1490 | transform(` 1491 | .test { 1492 | text-decoration: underline dotted; 1493 | } 1494 | `), 1495 | ).toEqual({ 1496 | test: { 1497 | textDecorationLine: "underline", 1498 | textDecorationStyle: "dotted", 1499 | textDecorationColor: "black", 1500 | }, 1501 | }); 1502 | }); 1503 | 1504 | it("transforms text-decoration without style", () => { 1505 | expect( 1506 | transform(` 1507 | .test { 1508 | text-decoration: underline red; 1509 | } 1510 | `), 1511 | ).toEqual({ 1512 | test: { 1513 | textDecorationLine: "underline", 1514 | textDecorationStyle: "solid", 1515 | textDecorationColor: "red", 1516 | }, 1517 | }); 1518 | }); 1519 | 1520 | it("transforms text-decoration without style and color", () => { 1521 | expect( 1522 | transform(` 1523 | .test { 1524 | text-decoration: underline; 1525 | } 1526 | `), 1527 | ).toEqual({ 1528 | test: { 1529 | textDecorationLine: "underline", 1530 | textDecorationStyle: "solid", 1531 | textDecorationColor: "black", 1532 | }, 1533 | }); 1534 | }); 1535 | 1536 | it("transforms text-decoration with two line properties", () => { 1537 | expect( 1538 | transform(` 1539 | .test { 1540 | text-decoration: underline line-through dashed red; 1541 | } 1542 | `), 1543 | ).toEqual({ 1544 | test: { 1545 | textDecorationLine: "underline line-through", 1546 | textDecorationStyle: "dashed", 1547 | textDecorationColor: "red", 1548 | }, 1549 | }); 1550 | }); 1551 | 1552 | it("transforms text-decoration in different order", () => { 1553 | expect( 1554 | transform(` 1555 | .test { 1556 | text-decoration: dashed red underline line-through; 1557 | } 1558 | `), 1559 | ).toEqual({ 1560 | test: { 1561 | textDecorationLine: "underline line-through", 1562 | textDecorationStyle: "dashed", 1563 | textDecorationColor: "red", 1564 | }, 1565 | }); 1566 | }); 1567 | 1568 | it("transforms text-decoration with ine in different order", () => { 1569 | expect( 1570 | transform(` 1571 | .test { 1572 | text-decoration: line-through underline; 1573 | } 1574 | `), 1575 | ).toEqual({ 1576 | test: { 1577 | textDecorationLine: "underline line-through", 1578 | textDecorationStyle: "solid", 1579 | textDecorationColor: "black", 1580 | }, 1581 | }); 1582 | }); 1583 | 1584 | it("transforms text-decoration with none", () => { 1585 | expect( 1586 | transform(` 1587 | .test { 1588 | text-decoration: none; 1589 | } 1590 | `), 1591 | ).toEqual({ 1592 | test: { 1593 | textDecorationLine: "none", 1594 | textDecorationStyle: "solid", 1595 | textDecorationColor: "black", 1596 | }, 1597 | }); 1598 | }); 1599 | 1600 | it("transforms text-decoration with none as part of multiple terms", () => { 1601 | expect( 1602 | transform(` 1603 | .test { 1604 | text-decoration: yellow none; 1605 | } 1606 | `), 1607 | ).toEqual({ 1608 | test: { 1609 | textDecorationLine: "none", 1610 | textDecorationStyle: "solid", 1611 | textDecorationColor: "yellow", 1612 | }, 1613 | }); 1614 | }); 1615 | 1616 | it("transforms text-decoration with none in capitals", () => { 1617 | expect( 1618 | transform(` 1619 | .test { 1620 | text-decoration: yellow NONE; 1621 | } 1622 | `), 1623 | ).toEqual({ 1624 | test: { 1625 | textDecorationLine: "none", 1626 | textDecorationStyle: "solid", 1627 | textDecorationColor: "yellow", 1628 | }, 1629 | }); 1630 | }); 1631 | 1632 | it("transforms text-decoration with style in capitals", () => { 1633 | expect( 1634 | transform(` 1635 | .test { 1636 | text-decoration: yellow UNDERLINE LINE-THROUGH; 1637 | } 1638 | `), 1639 | ).toEqual({ 1640 | test: { 1641 | textDecorationLine: "underline line-through", 1642 | textDecorationStyle: "solid", 1643 | textDecorationColor: "yellow", 1644 | }, 1645 | }); 1646 | }); 1647 | 1648 | it("does not transform text-decoration if multiple colors are used", () => { 1649 | expect(() => 1650 | transform(` 1651 | .test { 1652 | text-decoration: underline red yellow; 1653 | } 1654 | `), 1655 | ).toThrow( 1656 | 'Failed to parse declaration "textDecoration: underline red yellow"', 1657 | ); 1658 | }); 1659 | }); 1660 | 1661 | describe("text-decoration-line", () => { 1662 | it("transforms text-decoration-line with underline line-through", () => { 1663 | expect( 1664 | transform(` 1665 | .test { 1666 | text-decoration-line: underline line-through; 1667 | } 1668 | `), 1669 | ).toEqual({ 1670 | test: { 1671 | textDecorationLine: "underline line-through", 1672 | }, 1673 | }); 1674 | }); 1675 | 1676 | it("transforms text-decoration-line with line-through underline", () => { 1677 | expect( 1678 | transform(` 1679 | .test { 1680 | text-decoration-line: line-through underline; 1681 | } 1682 | `), 1683 | ).toEqual({ 1684 | test: { 1685 | textDecorationLine: "underline line-through", 1686 | }, 1687 | }); 1688 | }); 1689 | 1690 | it("transforms text-decoration-line with none", () => { 1691 | expect( 1692 | transform(` 1693 | .test { 1694 | text-decoration-line: none; 1695 | } 1696 | `), 1697 | ).toEqual({ 1698 | test: { 1699 | textDecorationLine: "none", 1700 | }, 1701 | }); 1702 | }); 1703 | }); 1704 | 1705 | describe("flex-box", () => { 1706 | it("transforms flex shorthand with 3 values", () => { 1707 | expect( 1708 | transform(` 1709 | .test { 1710 | flex: 1 2 3px; 1711 | } 1712 | `), 1713 | ).toEqual({ 1714 | test: { flexGrow: 1, flexShrink: 2, flexBasis: 3 }, 1715 | }); 1716 | }); 1717 | 1718 | it("transforms flex shorthand with 3 values in reverse order", () => { 1719 | expect( 1720 | transform(` 1721 | .test { 1722 | flex: 3px 1 2; 1723 | } 1724 | `), 1725 | ).toEqual({ 1726 | test: { flexGrow: 1, flexShrink: 2, flexBasis: 3 }, 1727 | }); 1728 | }); 1729 | 1730 | it("transforms flex shorthand with 2 values of flex-grow and flex-shrink", () => { 1731 | expect( 1732 | transform(` 1733 | .test { 1734 | flex: 1 2; 1735 | } 1736 | `), 1737 | ).toEqual({ 1738 | test: { flexGrow: 1, flexShrink: 2, flexBasis: 0 }, 1739 | }); 1740 | }); 1741 | 1742 | it("transforms flex shorthand with 2 values of flex-grow and flex-basis", () => { 1743 | expect( 1744 | transform(` 1745 | .test { 1746 | flex: 2 2px; 1747 | } 1748 | `), 1749 | ).toEqual({ 1750 | test: { flexGrow: 2, flexShrink: 1, flexBasis: 2 }, 1751 | }); 1752 | }); 1753 | 1754 | it("transforms flex shorthand with 2 values of flex-grow and flex-basis (reversed)", () => { 1755 | expect( 1756 | transform(` 1757 | .test { 1758 | flex: 2px 2; 1759 | } 1760 | `), 1761 | ).toEqual({ 1762 | test: { flexGrow: 2, flexShrink: 1, flexBasis: 2 }, 1763 | }); 1764 | }); 1765 | 1766 | it("transforms flex shorthand with 1 value of flex-grow", () => { 1767 | expect( 1768 | transform(` 1769 | .test { 1770 | flex: 2; 1771 | } 1772 | `), 1773 | ).toEqual({ 1774 | test: { flexGrow: 2, flexShrink: 1, flexBasis: 0 }, 1775 | }); 1776 | }); 1777 | 1778 | it("transforms flex shorthand with 1 value of flex-basis", () => { 1779 | expect( 1780 | transform(` 1781 | .test { 1782 | flex: 10px; 1783 | } 1784 | `), 1785 | ).toEqual({ 1786 | test: { flexGrow: 1, flexShrink: 1, flexBasis: 10 }, 1787 | }); 1788 | }); 1789 | 1790 | /* 1791 | A unitless zero that is not already preceded by two flex factors must be interpreted as a flex 1792 | factor. To avoid misinterpretation or invalid declarations, authors must specify a zero 1793 | <‘flex-basis’> component with a unit or precede it by two flex factors. 1794 | */ 1795 | it("transforms flex shorthand with flex-grow/shrink taking priority over basis", () => { 1796 | expect( 1797 | transform(` 1798 | .test { 1799 | flex: 0 1 0; 1800 | } 1801 | `), 1802 | ).toEqual({ 1803 | test: { flexGrow: 0, flexShrink: 1, flexBasis: 0 }, 1804 | }); 1805 | }); 1806 | 1807 | it("transforms flex shorthand with flex-basis set to auto", () => { 1808 | expect( 1809 | transform(` 1810 | .test { 1811 | flex: 0 1 auto; 1812 | } 1813 | `), 1814 | ).toEqual({ 1815 | test: { flexBasis: "auto", flexGrow: 0, flexShrink: 1 }, 1816 | }); 1817 | }); 1818 | 1819 | it("transforms flex shorthand with flex-basis set to percent", () => { 1820 | expect( 1821 | transform(` 1822 | .test { 1823 | flex: 1 2 30% 1824 | } 1825 | `), 1826 | ).toEqual({ 1827 | test: { 1828 | flexGrow: 1, 1829 | flexShrink: 2, 1830 | flexBasis: "30%", 1831 | }, 1832 | }); 1833 | }); 1834 | 1835 | it("transforms flex shorthand with flex-basis set to unsupported unit", () => { 1836 | expect( 1837 | transform(` 1838 | .test { 1839 | flex: 1 2 30em 1840 | } 1841 | `), 1842 | ).toEqual({ 1843 | test: { 1844 | flexGrow: 1, 1845 | flexShrink: 2, 1846 | flexBasis: "30em", 1847 | }, 1848 | }); 1849 | }); 1850 | 1851 | it("transforms flex shorthand with flex-basis set to auto appearing first", () => { 1852 | expect( 1853 | transform(` 1854 | .test { 1855 | flex: auto 0 1; 1856 | } 1857 | `), 1858 | ).toEqual({ 1859 | test: { flexBasis: "auto", flexGrow: 0, flexShrink: 1 }, 1860 | }); 1861 | }); 1862 | 1863 | it("transforms flex auto keyword", () => { 1864 | expect( 1865 | transform(` 1866 | .test { 1867 | flex: auto; 1868 | } 1869 | `), 1870 | ).toEqual({ 1871 | test: { flexBasis: "auto", flexGrow: 1, flexShrink: 1 }, 1872 | }); 1873 | }); 1874 | 1875 | it("transforms flex none keyword", () => { 1876 | expect( 1877 | transform(` 1878 | .test { 1879 | flex: none; 1880 | } 1881 | `), 1882 | ).toEqual({ 1883 | test: { flexBasis: "auto", flexGrow: 0, flexShrink: 0 }, 1884 | }); 1885 | }); 1886 | 1887 | it("transforms flexFlow shorthand with two values", () => { 1888 | expect( 1889 | transform(` 1890 | .test { 1891 | flex-flow: column wrap; 1892 | } 1893 | `), 1894 | ).toEqual({ 1895 | test: { flexDirection: "column", flexWrap: "wrap" }, 1896 | }); 1897 | }); 1898 | 1899 | it("transforms flexFlow shorthand missing flexDirection", () => { 1900 | expect( 1901 | transform(` 1902 | .test { 1903 | flex-flow: wrap; 1904 | } 1905 | `), 1906 | ).toEqual({ 1907 | test: { flexDirection: "row", flexWrap: "wrap" }, 1908 | }); 1909 | }); 1910 | 1911 | it("transforms flexFlow shorthand missing flexWrap", () => { 1912 | expect( 1913 | transform(` 1914 | .test { 1915 | flex-flow: column; 1916 | } 1917 | `), 1918 | ).toEqual({ 1919 | test: { flexDirection: "column", flexWrap: "nowrap" }, 1920 | }); 1921 | }); 1922 | 1923 | it("does not transform invalid flex'", () => { 1924 | expect(() => { 1925 | transform(` 1926 | .test { 1927 | flex: 1 2px 3; 1928 | } 1929 | `); 1930 | }).toThrowError('Failed to parse declaration "flex: 1 2px 3"'); 1931 | }); 1932 | }); 1933 | 1934 | describe("font", () => { 1935 | it("transforms font", () => { 1936 | expect( 1937 | transform(` 1938 | .test { 1939 | font: bold italic small-caps 16px/18px "Helvetica"; 1940 | } 1941 | `), 1942 | ).toEqual({ 1943 | test: { 1944 | fontFamily: "Helvetica", 1945 | fontSize: 16, 1946 | fontWeight: "bold", 1947 | fontStyle: "italic", 1948 | fontVariant: ["small-caps"], 1949 | lineHeight: 18, 1950 | }, 1951 | }); 1952 | }); 1953 | 1954 | it("transforms font missing font-variant", () => { 1955 | expect( 1956 | transform(` 1957 | .test { 1958 | font: bold italic 16px/18px "Helvetica"; 1959 | } 1960 | `), 1961 | ).toEqual({ 1962 | test: { 1963 | fontFamily: "Helvetica", 1964 | fontSize: 16, 1965 | fontWeight: "bold", 1966 | fontStyle: "italic", 1967 | fontVariant: [], 1968 | lineHeight: 18, 1969 | }, 1970 | }); 1971 | }); 1972 | 1973 | it("transforms font missing font-style", () => { 1974 | expect( 1975 | transform(` 1976 | .test { 1977 | font: bold small-caps 16px/18px "Helvetica"; 1978 | } 1979 | `), 1980 | ).toEqual({ 1981 | test: { 1982 | fontFamily: "Helvetica", 1983 | fontSize: 16, 1984 | fontWeight: "bold", 1985 | fontStyle: "normal", 1986 | fontVariant: ["small-caps"], 1987 | lineHeight: 18, 1988 | }, 1989 | }); 1990 | }); 1991 | 1992 | it("transforms font missing font-weight", () => { 1993 | expect( 1994 | transform(` 1995 | .test { 1996 | font: italic small-caps 16px/18px "Helvetica"; 1997 | } 1998 | `), 1999 | ).toEqual({ 2000 | test: { 2001 | fontFamily: "Helvetica", 2002 | fontSize: 16, 2003 | fontWeight: "normal", 2004 | fontStyle: "italic", 2005 | fontVariant: ["small-caps"], 2006 | lineHeight: 18, 2007 | }, 2008 | }); 2009 | }); 2010 | 2011 | it("transforms font with font-weight normal", () => { 2012 | expect( 2013 | transform(` 2014 | .test { 2015 | font: normal 16px/18px "Helvetica"; 2016 | } 2017 | `), 2018 | ).toEqual({ 2019 | test: { 2020 | fontFamily: "Helvetica", 2021 | fontSize: 16, 2022 | fontWeight: "normal", 2023 | fontStyle: "normal", 2024 | fontVariant: [], 2025 | lineHeight: 18, 2026 | }, 2027 | }); 2028 | }); 2029 | 2030 | it("transforms font with font-weight and font-style normal", () => { 2031 | expect( 2032 | transform(` 2033 | .test { 2034 | font: normal normal 16px/18px "Helvetica"; 2035 | } 2036 | `), 2037 | ).toEqual({ 2038 | test: { 2039 | fontFamily: "Helvetica", 2040 | fontSize: 16, 2041 | fontWeight: "normal", 2042 | fontStyle: "normal", 2043 | fontVariant: [], 2044 | lineHeight: 18, 2045 | }, 2046 | }); 2047 | }); 2048 | 2049 | it("transforms font with no font-weight, font-style, and font-variant", () => { 2050 | expect( 2051 | transform(` 2052 | .test { 2053 | font: 16px/18px "Helvetica"; 2054 | } 2055 | `), 2056 | ).toEqual({ 2057 | test: { 2058 | fontFamily: "Helvetica", 2059 | fontSize: 16, 2060 | fontWeight: "normal", 2061 | fontStyle: "normal", 2062 | fontVariant: [], 2063 | lineHeight: 18, 2064 | }, 2065 | }); 2066 | }); 2067 | 2068 | it("omits line height if not specified", () => { 2069 | expect( 2070 | transform(` 2071 | .test { 2072 | font: 16px "Helvetica"; 2073 | } 2074 | `), 2075 | ).toEqual({ 2076 | test: { 2077 | fontFamily: "Helvetica", 2078 | fontSize: 16, 2079 | fontWeight: "normal", 2080 | fontStyle: "normal", 2081 | fontVariant: [], 2082 | }, 2083 | }); 2084 | }); 2085 | 2086 | it("does not allow line height as multiple", () => { 2087 | expect(() => { 2088 | transform(` 2089 | .test { 2090 | font: 16px/1.5 "Helvetica" 2091 | } 2092 | `); 2093 | }).toThrow(); 2094 | }); 2095 | 2096 | it("transforms font without quotes", () => { 2097 | expect( 2098 | transform(` 2099 | .test { 2100 | font: bold italic small-caps 16px/18px Helvetica Neue; 2101 | } 2102 | `), 2103 | ).toEqual({ 2104 | test: { 2105 | fontFamily: "Helvetica Neue", 2106 | fontSize: 16, 2107 | fontWeight: "bold", 2108 | fontStyle: "italic", 2109 | fontVariant: ["small-caps"], 2110 | lineHeight: 18, 2111 | }, 2112 | }); 2113 | }); 2114 | 2115 | it("transforms font-family with double quotes", () => { 2116 | expect( 2117 | transform(` 2118 | .test { 2119 | font-family: "Helvetica Neue"; 2120 | } 2121 | `), 2122 | ).toEqual({ 2123 | test: { 2124 | fontFamily: "Helvetica Neue", 2125 | }, 2126 | }); 2127 | }); 2128 | 2129 | it("transforms font-family with single quotes", () => { 2130 | expect( 2131 | transform(` 2132 | .test { 2133 | font-family: 'Helvetica Neue'; 2134 | } 2135 | `), 2136 | ).toEqual({ 2137 | test: { 2138 | fontFamily: "Helvetica Neue", 2139 | }, 2140 | }); 2141 | }); 2142 | 2143 | it("transforms font-family without quotes", () => { 2144 | expect( 2145 | transform(` 2146 | .test { 2147 | font-family: Helvetica Neue; 2148 | } 2149 | `), 2150 | ).toEqual({ 2151 | test: { 2152 | fontFamily: "Helvetica Neue", 2153 | }, 2154 | }); 2155 | }); 2156 | 2157 | it("transforms font-family with quotes with otherwise invalid values", () => { 2158 | expect( 2159 | transform(` 2160 | .test { 2161 | font-family: "Goudy Bookletter 1911"; 2162 | } 2163 | `), 2164 | ).toEqual({ 2165 | test: { 2166 | fontFamily: "Goudy Bookletter 1911", 2167 | }, 2168 | }); 2169 | }); 2170 | 2171 | it("transforms font-family with quotes with escaped values", () => { 2172 | expect( 2173 | transform(` 2174 | .test { 2175 | font-family: "test\\A test"; 2176 | } 2177 | `), 2178 | ).toEqual({ 2179 | test: { 2180 | fontFamily: "test\ntest", 2181 | }, 2182 | }); 2183 | }); 2184 | 2185 | it("transforms font-family with quotes with escaped quote", () => { 2186 | expect( 2187 | transform(` 2188 | .test { 2189 | font-family: "test\\"test"; 2190 | } 2191 | `), 2192 | ).toEqual({ 2193 | test: { 2194 | fontFamily: 'test"test', 2195 | }, 2196 | }); 2197 | }); 2198 | 2199 | it("does not transform invalid unquoted font-family", () => { 2200 | expect(() => { 2201 | transform(` 2202 | .test { 2203 | font-family: Goudy Bookletter 1911; 2204 | } 2205 | `); 2206 | }).toThrowError( 2207 | 'Failed to parse declaration "fontFamily: Goudy Bookletter 1911"', 2208 | ); 2209 | }); 2210 | }); 2211 | 2212 | describe("box-shadow", () => { 2213 | it("transforms box-shadow into shadow- properties", () => { 2214 | expect( 2215 | transform(` 2216 | .test { 2217 | box-shadow: 10px 20px 30px red; 2218 | } 2219 | `), 2220 | ).toEqual({ 2221 | test: { 2222 | shadowOffset: { width: 10, height: 20 }, 2223 | shadowRadius: 30, 2224 | shadowColor: "red", 2225 | shadowOpacity: 1, 2226 | }, 2227 | }); 2228 | expect( 2229 | transform(` 2230 | .test { 2231 | box-shadow: 10px 20px 30px #f00; 2232 | } 2233 | `), 2234 | ).toEqual({ 2235 | test: { 2236 | shadowOffset: { width: 10, height: 20 }, 2237 | shadowRadius: 30, 2238 | shadowColor: "#f00", 2239 | shadowOpacity: 1, 2240 | }, 2241 | }); 2242 | }); 2243 | 2244 | it("supports rgb values", () => { 2245 | expect( 2246 | transform(` 2247 | .test { 2248 | box-shadow: 10px 20px 30px rgb(100, 100, 100); 2249 | } 2250 | `), 2251 | ).toEqual({ 2252 | test: { 2253 | shadowOffset: { width: 10, height: 20 }, 2254 | shadowRadius: 30, 2255 | shadowColor: "rgb(100, 100, 100)", 2256 | shadowOpacity: 1, 2257 | }, 2258 | }); 2259 | }); 2260 | 2261 | it("supports rgba values", () => { 2262 | expect( 2263 | transform(` 2264 | .test { 2265 | box-shadow: 10px 20px 30px rgba(100, 100, 100, 0.5); 2266 | } 2267 | `), 2268 | ).toEqual({ 2269 | test: { 2270 | shadowOffset: { width: 10, height: 20 }, 2271 | shadowRadius: 30, 2272 | shadowColor: "rgba(100, 100, 100, 0.5)", 2273 | shadowOpacity: 1, 2274 | }, 2275 | }); 2276 | }); 2277 | 2278 | it("supports box-shadow with hsl color", () => { 2279 | expect( 2280 | transform(` 2281 | .test { 2282 | box-shadow: 10px 20px 30px hsl(120, 100%, 50%); 2283 | } 2284 | `), 2285 | ).toEqual({ 2286 | test: { 2287 | shadowOffset: { width: 10, height: 20 }, 2288 | shadowRadius: 30, 2289 | shadowColor: "hsl(120, 100%, 50%)", 2290 | shadowOpacity: 1, 2291 | }, 2292 | }); 2293 | }); 2294 | 2295 | it("supports box-shadow with hsla color", () => { 2296 | expect( 2297 | transform(` 2298 | .test { 2299 | box-shadow: 10px 20px 30px hsla(120, 100%, 50%, 0.7); 2300 | } 2301 | `), 2302 | ).toEqual({ 2303 | test: { 2304 | shadowOffset: { width: 10, height: 20 }, 2305 | shadowRadius: 30, 2306 | shadowColor: "hsla(120, 100%, 50%, 0.7)", 2307 | shadowOpacity: 1, 2308 | }, 2309 | }); 2310 | }); 2311 | 2312 | it("trims values", () => { 2313 | expect( 2314 | transform(` 2315 | .test { 2316 | box-shadow: 10px 20px 30px #f00 ; 2317 | } 2318 | `), 2319 | ).toEqual({ 2320 | test: { 2321 | shadowOffset: { width: 10, height: 20 }, 2322 | shadowRadius: 30, 2323 | shadowColor: "#f00", 2324 | shadowOpacity: 1, 2325 | }, 2326 | }); 2327 | }); 2328 | 2329 | it("transforms box-shadow with 0 values", () => { 2330 | expect( 2331 | transform(` 2332 | .test { 2333 | box-shadow: 0 0 1px red; 2334 | } 2335 | `), 2336 | ).toEqual({ 2337 | test: { 2338 | shadowOffset: { width: 0, height: 0 }, 2339 | shadowRadius: 1, 2340 | shadowColor: "red", 2341 | shadowOpacity: 1, 2342 | }, 2343 | }); 2344 | expect( 2345 | transform(` 2346 | .test { 2347 | box-shadow: 0 0 0 red; 2348 | } 2349 | `), 2350 | ).toEqual({ 2351 | test: { 2352 | shadowOffset: { width: 0, height: 0 }, 2353 | shadowRadius: 0, 2354 | shadowColor: "red", 2355 | shadowOpacity: 1, 2356 | }, 2357 | }); 2358 | expect( 2359 | transform(` 2360 | .test { 2361 | box-shadow: 1px 1px 0 #00f; 2362 | } 2363 | `), 2364 | ).toEqual({ 2365 | test: { 2366 | shadowOffset: { width: 1, height: 1 }, 2367 | shadowRadius: 0, 2368 | shadowColor: "#00f", 2369 | shadowOpacity: 1, 2370 | }, 2371 | }); 2372 | }); 2373 | 2374 | it("transforms box-shadow without blur-radius", () => { 2375 | expect( 2376 | transform(` 2377 | .test { 2378 | box-shadow: 10px 20px red; 2379 | } 2380 | `), 2381 | ).toEqual({ 2382 | test: { 2383 | shadowOffset: { width: 10, height: 20 }, 2384 | shadowRadius: 0, 2385 | shadowColor: "red", 2386 | shadowOpacity: 1, 2387 | }, 2388 | }); 2389 | }); 2390 | 2391 | it("transforms box-shadow without color", () => { 2392 | expect( 2393 | transform(` 2394 | .test { 2395 | box-shadow: 10px 20px 30px; 2396 | } 2397 | `), 2398 | ).toEqual({ 2399 | test: { 2400 | shadowOffset: { width: 10, height: 20 }, 2401 | shadowRadius: 30, 2402 | shadowColor: "black", 2403 | shadowOpacity: 1, 2404 | }, 2405 | }); 2406 | }); 2407 | 2408 | it("transforms box-shadow without blur-radius, color", () => { 2409 | expect( 2410 | transform(` 2411 | .test { 2412 | box-shadow: 10px 20px; 2413 | } 2414 | `), 2415 | ).toEqual({ 2416 | test: { 2417 | shadowOffset: { width: 10, height: 20 }, 2418 | shadowRadius: 0, 2419 | shadowColor: "black", 2420 | shadowOpacity: 1, 2421 | }, 2422 | }); 2423 | }); 2424 | 2425 | it("transforms box-shadow enforces offset to be present", () => { 2426 | expect(() => { 2427 | transform(` 2428 | .test { 2429 | box-shadow: red; 2430 | } 2431 | `); 2432 | }).toThrowError('Failed to parse declaration "boxShadow: red"'); 2433 | }); 2434 | 2435 | it("transforms box-shadow and throws if multiple colors are used", () => { 2436 | expect(() => { 2437 | transform(` 2438 | .test { 2439 | box-shadow: 0 0 0 red yellow green blue; 2440 | } 2441 | `); 2442 | }).toThrowError( 2443 | 'Failed to parse declaration "boxShadow: 0 0 0 red yellow green blue"', 2444 | ); 2445 | }); 2446 | 2447 | it("transforms box-shadow and enforces offset-y if offset-x present", () => { 2448 | expect(() => { 2449 | transform(` 2450 | .test { 2451 | box-shadow: 10px; 2452 | } 2453 | `); 2454 | }).toThrowError('Failed to parse declaration "boxShadow: 10px"'); 2455 | }); 2456 | 2457 | it("transforms box-shadow and enforces units for non 0 values", () => { 2458 | expect(() => { 2459 | transform(` 2460 | .test { 2461 | box-shadow: 10 20px 30px #f00; 2462 | } 2463 | `); 2464 | }).toThrowError( 2465 | 'Failed to parse declaration "boxShadow: 10 20px 30px #f00"', 2466 | ); 2467 | expect(() => { 2468 | transform(` 2469 | .test { 2470 | box-shadow: 10px 20; 2471 | } 2472 | `); 2473 | }).toThrowError('Failed to parse declaration "boxShadow: 10px 20"'); 2474 | expect(() => { 2475 | transform(` 2476 | .test { 2477 | box-shadow: 20; 2478 | } 2479 | `); 2480 | }).toThrowError('Failed to parse declaration "boxShadow: 20"'); 2481 | }); 2482 | }); 2483 | 2484 | describe("text-shadow", () => { 2485 | it("textShadow with all values", () => { 2486 | expect( 2487 | transform(` 2488 | .test { 2489 | text-shadow: 10px 20px 30px red; 2490 | } 2491 | `), 2492 | ).toEqual({ 2493 | test: { 2494 | textShadowOffset: { width: 10, height: 20 }, 2495 | textShadowRadius: 30, 2496 | textShadowColor: "red", 2497 | }, 2498 | }); 2499 | }); 2500 | 2501 | it("textShadow omitting blur", () => { 2502 | expect( 2503 | transform(` 2504 | .test { 2505 | text-shadow: 10px 20px red; 2506 | } 2507 | `), 2508 | ).toEqual({ 2509 | test: { 2510 | textShadowOffset: { width: 10, height: 20 }, 2511 | textShadowRadius: 0, 2512 | textShadowColor: "red", 2513 | }, 2514 | }); 2515 | }); 2516 | 2517 | it("textShadow omitting color", () => { 2518 | expect( 2519 | transform(` 2520 | .test { 2521 | text-shadow: 10px 20px; 2522 | } 2523 | `), 2524 | ).toEqual({ 2525 | test: { 2526 | textShadowOffset: { width: 10, height: 20 }, 2527 | textShadowRadius: 0, 2528 | textShadowColor: "black", 2529 | }, 2530 | }); 2531 | }); 2532 | 2533 | it("textShadow enforces offset-x and offset-y", () => { 2534 | expect(() => 2535 | transform(` 2536 | .test { 2537 | text-shadow: red; 2538 | } 2539 | `), 2540 | ).toThrow('Failed to parse declaration "textShadow: red"'); 2541 | expect(() => 2542 | transform(` 2543 | .test { 2544 | text-shadow: 10px red; 2545 | } 2546 | `), 2547 | ).toThrow('Failed to parse declaration "textShadow: 10px red"'); 2548 | }); 2549 | }); 2550 | 2551 | it("transforms place content", () => { 2552 | expect( 2553 | transform(` 2554 | .test { 2555 | place-content: center center 2556 | } 2557 | `), 2558 | ).toEqual({ 2559 | test: { 2560 | alignContent: "center", 2561 | justifyContent: "center", 2562 | }, 2563 | }); 2564 | }); 2565 | 2566 | it("transforms place content with one value", () => { 2567 | expect( 2568 | transform(` 2569 | .test { 2570 | place-content: center 2571 | } 2572 | `), 2573 | ).toEqual({ 2574 | test: { 2575 | alignContent: "center", 2576 | justifyContent: "stretch", 2577 | }, 2578 | }); 2579 | }); 2580 | 2581 | it("does not allow justify content without align content", () => { 2582 | expect(() => 2583 | transform(` 2584 | .test { 2585 | place-content: space-evenly 2586 | } 2587 | `), 2588 | ).toThrow(); 2589 | }); 2590 | 2591 | describe("rem unit", () => { 2592 | it("should transform a single rem value", () => { 2593 | expect( 2594 | transform(` 2595 | .test1 { 2596 | padding: 2rem; 2597 | } 2598 | .test2 { 2599 | font-size: 1rem; 2600 | } 2601 | `), 2602 | ).toEqual({ 2603 | test1: { 2604 | paddingBottom: 32, 2605 | paddingLeft: 32, 2606 | paddingRight: 32, 2607 | paddingTop: 32, 2608 | }, 2609 | test2: { 2610 | fontSize: 16, 2611 | }, 2612 | }); 2613 | }); 2614 | 2615 | it("should transform multiple rem values", () => { 2616 | expect( 2617 | transform(` 2618 | .test1 { 2619 | transform: translate(1rem, 2rem); 2620 | } 2621 | .test2 { 2622 | box-shadow: 1rem 2rem 3rem #fff; 2623 | } 2624 | `), 2625 | ).toEqual({ 2626 | test1: { 2627 | transform: [{ translateY: 32 }, { translateX: 16 }], 2628 | }, 2629 | test2: { 2630 | shadowColor: "#fff", 2631 | shadowOffset: { height: 32, width: 16 }, 2632 | shadowRadius: 48, 2633 | shadowOpacity: 1, 2634 | }, 2635 | }); 2636 | }); 2637 | 2638 | it("should support decimal values", () => { 2639 | expect( 2640 | transform(` 2641 | .test1 { 2642 | transform: translate(0.9375rem, 1.625rem); 2643 | } 2644 | .test2 { 2645 | border-radius: 0.5625rem; 2646 | } 2647 | `), 2648 | ).toEqual({ 2649 | test1: { transform: [{ translateY: 26 }, { translateX: 15 }] }, 2650 | test2: { 2651 | borderRadius: 9, 2652 | }, 2653 | }); 2654 | 2655 | expect( 2656 | transform(` 2657 | .test1 { 2658 | transform: translate(.9375rem, 1.625rem); 2659 | } 2660 | .test2 { 2661 | border-radius: .5625rem; 2662 | } 2663 | `), 2664 | ).toEqual({ 2665 | test1: { transform: [{ translateY: 26 }, { translateX: 15 }] }, 2666 | test2: { 2667 | borderRadius: 9, 2668 | }, 2669 | }); 2670 | }); 2671 | }); 2672 | 2673 | describe("ignoreRule option", () => { 2674 | it("should allow to ignore a selector completely", () => { 2675 | expect( 2676 | transform( 2677 | ` 2678 | .foo { 2679 | color: red; 2680 | } 2681 | .bar { 2682 | font-size: 12px; 2683 | } 2684 | `, 2685 | { 2686 | ignoreRule: (selector) => { 2687 | if (selector === ".foo") { 2688 | return true; 2689 | } 2690 | }, 2691 | }, 2692 | ), 2693 | ).toEqual({ 2694 | bar: { fontSize: 12 }, 2695 | }); 2696 | }); 2697 | 2698 | it("should do nothing when returing false", () => { 2699 | expect( 2700 | transform( 2701 | ` 2702 | .foo { 2703 | color: red; 2704 | } 2705 | .bar { 2706 | font-size: 12px; 2707 | } 2708 | `, 2709 | { 2710 | ignoreRule: (selector) => { 2711 | if (selector === ".bar") { 2712 | return false; 2713 | } 2714 | if (selector === ".foo") { 2715 | return false; 2716 | } 2717 | }, 2718 | }, 2719 | ), 2720 | ).toEqual({ 2721 | bar: { fontSize: 12 }, 2722 | foo: { color: "red" }, 2723 | }); 2724 | }); 2725 | 2726 | it("should not error out even if ignoreRule is not a function", () => { 2727 | expect( 2728 | transform( 2729 | ` 2730 | .foo { 2731 | color: red; 2732 | } 2733 | .bar { 2734 | font-size: 12px; 2735 | } 2736 | `, 2737 | { 2738 | ignoreRule: true, 2739 | }, 2740 | ), 2741 | ).toEqual({ 2742 | bar: { fontSize: 12 }, 2743 | foo: { color: "red" }, 2744 | }); 2745 | }); 2746 | }); 2747 | 2748 | describe("viewport units", () => { 2749 | it("should transform viewport units", () => { 2750 | expect( 2751 | transform(` 2752 | .test { 2753 | font-size: 1vw; 2754 | line-height: 2vh; 2755 | padding: 1vmax; 2756 | margin: 1vmin; 2757 | } 2758 | `), 2759 | ).toEqual({ 2760 | __viewportUnits: true, 2761 | test: { 2762 | fontSize: "1vw", 2763 | lineHeight: "2vh", 2764 | marginBottom: "1vmin", 2765 | marginLeft: "1vmin", 2766 | marginRight: "1vmin", 2767 | marginTop: "1vmin", 2768 | paddingBottom: "1vmax", 2769 | paddingLeft: "1vmax", 2770 | paddingRight: "1vmax", 2771 | paddingTop: "1vmax", 2772 | }, 2773 | }); 2774 | }); 2775 | }); 2776 | 2777 | describe("media queries", () => { 2778 | it("transforms media queries", () => { 2779 | expect( 2780 | transform( 2781 | ` 2782 | .container { 2783 | background-color: #f00; 2784 | } 2785 | 2786 | @media (orientation: landscape) { 2787 | .container { 2788 | background-color: #00f; 2789 | } 2790 | } 2791 | `, 2792 | { 2793 | parseMediaQueries: true, 2794 | }, 2795 | ), 2796 | ).toEqual({ 2797 | __mediaQueries: { 2798 | "@media (orientation: landscape)": [ 2799 | { 2800 | expressions: [ 2801 | { 2802 | feature: "orientation", 2803 | modifier: undefined, 2804 | value: "landscape", 2805 | }, 2806 | ], 2807 | inverse: false, 2808 | type: "all", 2809 | }, 2810 | ], 2811 | }, 2812 | container: { 2813 | backgroundColor: "#f00", 2814 | }, 2815 | "@media (orientation: landscape)": { 2816 | container: { 2817 | backgroundColor: "#00f", 2818 | }, 2819 | }, 2820 | }); 2821 | }); 2822 | 2823 | it("merges media queries", () => { 2824 | expect( 2825 | transform( 2826 | ` 2827 | .container { 2828 | background-color: #f00; 2829 | } 2830 | .box { 2831 | background-color: #f00; 2832 | } 2833 | 2834 | @media (orientation: landscape) { 2835 | .container { 2836 | background-color: #00f; 2837 | } 2838 | } 2839 | @media (orientation: landscape) { 2840 | .box { 2841 | background-color: #00f; 2842 | } 2843 | } 2844 | `, 2845 | { 2846 | parseMediaQueries: true, 2847 | }, 2848 | ), 2849 | ).toEqual({ 2850 | __mediaQueries: { 2851 | "@media (orientation: landscape)": [ 2852 | { 2853 | expressions: [ 2854 | { 2855 | feature: "orientation", 2856 | modifier: undefined, 2857 | value: "landscape", 2858 | }, 2859 | ], 2860 | inverse: false, 2861 | type: "all", 2862 | }, 2863 | ], 2864 | }, 2865 | container: { 2866 | backgroundColor: "#f00", 2867 | }, 2868 | box: { 2869 | backgroundColor: "#f00", 2870 | }, 2871 | "@media (orientation: landscape)": { 2872 | container: { 2873 | backgroundColor: "#00f", 2874 | }, 2875 | box: { 2876 | backgroundColor: "#00f", 2877 | }, 2878 | }, 2879 | }); 2880 | }); 2881 | 2882 | it("does not transform media queries without option enabled", () => { 2883 | expect( 2884 | transform(` 2885 | .container { 2886 | background-color: #f00; 2887 | } 2888 | 2889 | @media (orientation: landscape) { 2890 | .container { 2891 | background-color: #00f; 2892 | } 2893 | } 2894 | `), 2895 | ).toEqual({ 2896 | container: { 2897 | backgroundColor: "#f00", 2898 | }, 2899 | }); 2900 | 2901 | expect( 2902 | transform( 2903 | ` 2904 | .container { 2905 | background-color: #f00; 2906 | } 2907 | 2908 | @media (orientation: landscape) { 2909 | .container { 2910 | background-color: #00f; 2911 | } 2912 | } 2913 | `, 2914 | { 2915 | parseMediaQueries: false, 2916 | }, 2917 | ), 2918 | ).toEqual({ 2919 | container: { 2920 | backgroundColor: "#f00", 2921 | }, 2922 | }); 2923 | }); 2924 | 2925 | it("should support screen type", () => { 2926 | expect( 2927 | transform( 2928 | ` 2929 | .foo { 2930 | color: blue; 2931 | } 2932 | @media screen and (min-height: 50px) and (max-height: 150px) { 2933 | .foo { 2934 | color: red; 2935 | } 2936 | } 2937 | @media screen and (min-height: 150px) and (max-height: 200px) { 2938 | .foo { 2939 | color: green; 2940 | } 2941 | } 2942 | `, 2943 | { 2944 | parseMediaQueries: true, 2945 | }, 2946 | ), 2947 | ).toEqual({ 2948 | __mediaQueries: { 2949 | "@media screen and (min-height: 50px) and (max-height: 150px)": [ 2950 | { 2951 | expressions: [ 2952 | { feature: "height", modifier: "min", value: "50px" }, 2953 | { feature: "height", modifier: "max", value: "150px" }, 2954 | ], 2955 | inverse: false, 2956 | type: "screen", 2957 | }, 2958 | ], 2959 | "@media screen and (min-height: 150px) and (max-height: 200px)": [ 2960 | { 2961 | expressions: [ 2962 | { feature: "height", modifier: "min", value: "150px" }, 2963 | { feature: "height", modifier: "max", value: "200px" }, 2964 | ], 2965 | inverse: false, 2966 | type: "screen", 2967 | }, 2968 | ], 2969 | }, 2970 | foo: { color: "blue" }, 2971 | "@media screen and (min-height: 50px) and (max-height: 150px)": { 2972 | foo: { color: "red" }, 2973 | }, 2974 | "@media screen and (min-height: 150px) and (max-height: 200px)": { 2975 | foo: { color: "green" }, 2976 | }, 2977 | }); 2978 | }); 2979 | 2980 | it("should support all type", () => { 2981 | expect( 2982 | transform( 2983 | ` 2984 | .foo { 2985 | color: blue; 2986 | } 2987 | @media all and (min-height: 50px) and (max-height: 150px) { 2988 | .foo { 2989 | color: red; 2990 | } 2991 | } 2992 | @media all and (min-height: 150px) and (max-height: 200px) { 2993 | .foo { 2994 | color: green; 2995 | } 2996 | } 2997 | `, 2998 | { 2999 | parseMediaQueries: true, 3000 | }, 3001 | ), 3002 | ).toEqual({ 3003 | __mediaQueries: { 3004 | "@media all and (min-height: 50px) and (max-height: 150px)": [ 3005 | { 3006 | expressions: [ 3007 | { feature: "height", modifier: "min", value: "50px" }, 3008 | { feature: "height", modifier: "max", value: "150px" }, 3009 | ], 3010 | inverse: false, 3011 | type: "all", 3012 | }, 3013 | ], 3014 | "@media all and (min-height: 150px) and (max-height: 200px)": [ 3015 | { 3016 | expressions: [ 3017 | { feature: "height", modifier: "min", value: "150px" }, 3018 | { feature: "height", modifier: "max", value: "200px" }, 3019 | ], 3020 | inverse: false, 3021 | type: "all", 3022 | }, 3023 | ], 3024 | }, 3025 | foo: { color: "blue" }, 3026 | "@media all and (min-height: 50px) and (max-height: 150px)": { 3027 | foo: { color: "red" }, 3028 | }, 3029 | "@media all and (min-height: 150px) and (max-height: 200px)": { 3030 | foo: { color: "green" }, 3031 | }, 3032 | }); 3033 | }); 3034 | 3035 | it("should support platform types", () => { 3036 | expect( 3037 | transform( 3038 | ` 3039 | @media web and (orientation: landscape) { 3040 | .container { 3041 | background-color: #00f; 3042 | } 3043 | } 3044 | @media ios and (orientation: landscape) { 3045 | .container { 3046 | background-color: #00f; 3047 | } 3048 | } 3049 | @media android and (orientation: landscape) { 3050 | .container { 3051 | background-color: #00f; 3052 | } 3053 | } 3054 | @media windows and (orientation: landscape) { 3055 | .container { 3056 | background-color: #00f; 3057 | } 3058 | } 3059 | @media macos and (orientation: landscape) { 3060 | .container { 3061 | background-color: #00f; 3062 | } 3063 | } 3064 | @media dom and (orientation: landscape) { 3065 | .container { 3066 | background-color: #00f; 3067 | } 3068 | } 3069 | `, 3070 | { 3071 | parseMediaQueries: true, 3072 | }, 3073 | ), 3074 | ).toEqual({ 3075 | "@media android and (orientation: landscape)": { 3076 | container: { backgroundColor: "#00f" }, 3077 | }, 3078 | "@media dom and (orientation: landscape)": { 3079 | container: { backgroundColor: "#00f" }, 3080 | }, 3081 | "@media ios and (orientation: landscape)": { 3082 | container: { backgroundColor: "#00f" }, 3083 | }, 3084 | "@media macos and (orientation: landscape)": { 3085 | container: { backgroundColor: "#00f" }, 3086 | }, 3087 | "@media web and (orientation: landscape)": { 3088 | container: { backgroundColor: "#00f" }, 3089 | }, 3090 | "@media windows and (orientation: landscape)": { 3091 | container: { backgroundColor: "#00f" }, 3092 | }, 3093 | __mediaQueries: { 3094 | "@media android and (orientation: landscape)": [ 3095 | { 3096 | expressions: [ 3097 | { 3098 | feature: "orientation", 3099 | modifier: undefined, 3100 | value: "landscape", 3101 | }, 3102 | ], 3103 | inverse: false, 3104 | type: "android", 3105 | }, 3106 | ], 3107 | "@media dom and (orientation: landscape)": [ 3108 | { 3109 | expressions: [ 3110 | { 3111 | feature: "orientation", 3112 | modifier: undefined, 3113 | value: "landscape", 3114 | }, 3115 | ], 3116 | inverse: false, 3117 | type: "dom", 3118 | }, 3119 | ], 3120 | "@media ios and (orientation: landscape)": [ 3121 | { 3122 | expressions: [ 3123 | { 3124 | feature: "orientation", 3125 | modifier: undefined, 3126 | value: "landscape", 3127 | }, 3128 | ], 3129 | inverse: false, 3130 | type: "ios", 3131 | }, 3132 | ], 3133 | "@media macos and (orientation: landscape)": [ 3134 | { 3135 | expressions: [ 3136 | { 3137 | feature: "orientation", 3138 | modifier: undefined, 3139 | value: "landscape", 3140 | }, 3141 | ], 3142 | inverse: false, 3143 | type: "macos", 3144 | }, 3145 | ], 3146 | "@media web and (orientation: landscape)": [ 3147 | { 3148 | expressions: [ 3149 | { 3150 | feature: "orientation", 3151 | modifier: undefined, 3152 | value: "landscape", 3153 | }, 3154 | ], 3155 | inverse: false, 3156 | type: "web", 3157 | }, 3158 | ], 3159 | "@media windows and (orientation: landscape)": [ 3160 | { 3161 | expressions: [ 3162 | { 3163 | feature: "orientation", 3164 | modifier: undefined, 3165 | value: "landscape", 3166 | }, 3167 | ], 3168 | inverse: false, 3169 | type: "windows", 3170 | }, 3171 | ], 3172 | }, 3173 | }); 3174 | }); 3175 | 3176 | it("should support NOT operator", () => { 3177 | expect( 3178 | transform( 3179 | ` 3180 | .container { 3181 | background-color: #f00; 3182 | } 3183 | 3184 | @media not screen and (device-width: 768px) { 3185 | .container { 3186 | background-color: #00f; 3187 | } 3188 | } 3189 | `, 3190 | { 3191 | parseMediaQueries: true, 3192 | }, 3193 | ), 3194 | ).toEqual({ 3195 | __mediaQueries: { 3196 | "@media not screen and (device-width: 768px)": [ 3197 | { 3198 | expressions: [ 3199 | { 3200 | feature: "device-width", 3201 | modifier: undefined, 3202 | value: "768px", 3203 | }, 3204 | ], 3205 | inverse: true, 3206 | type: "screen", 3207 | }, 3208 | ], 3209 | }, 3210 | container: { 3211 | backgroundColor: "#f00", 3212 | }, 3213 | "@media not screen and (device-width: 768px)": { 3214 | container: { 3215 | backgroundColor: "#00f", 3216 | }, 3217 | }, 3218 | }); 3219 | }); 3220 | 3221 | it("should support OR queries", () => { 3222 | expect( 3223 | transform( 3224 | ` 3225 | .container { 3226 | background-color: #f00; 3227 | } 3228 | 3229 | @media (orientation: portrait), (orientation: landscape) { 3230 | .container { 3231 | background-color: #00f; 3232 | } 3233 | } 3234 | `, 3235 | { 3236 | parseMediaQueries: true, 3237 | }, 3238 | ), 3239 | ).toEqual({ 3240 | __mediaQueries: { 3241 | "@media (orientation: portrait), (orientation: landscape)": [ 3242 | { 3243 | expressions: [ 3244 | { 3245 | feature: "orientation", 3246 | modifier: undefined, 3247 | value: "portrait", 3248 | }, 3249 | ], 3250 | inverse: false, 3251 | type: "all", 3252 | }, 3253 | { 3254 | expressions: [ 3255 | { 3256 | feature: "orientation", 3257 | modifier: undefined, 3258 | value: "landscape", 3259 | }, 3260 | ], 3261 | inverse: false, 3262 | type: "all", 3263 | }, 3264 | ], 3265 | }, 3266 | container: { backgroundColor: "#f00" }, 3267 | "@media (orientation: portrait), (orientation: landscape)": { 3268 | container: { backgroundColor: "#00f" }, 3269 | }, 3270 | }); 3271 | }); 3272 | 3273 | it("should support media queries + ignoreRule option", () => { 3274 | expect( 3275 | transform( 3276 | ` 3277 | .container { 3278 | background-color: #f00; 3279 | } 3280 | 3281 | @media (orientation: landscape) { 3282 | .container { 3283 | background-color: #00f; 3284 | } 3285 | } 3286 | `, 3287 | { 3288 | ignoreRule: (selector) => { 3289 | if (selector === ".container") { 3290 | return true; 3291 | } 3292 | }, 3293 | parseMediaQueries: true, 3294 | }, 3295 | ), 3296 | ).toEqual({ 3297 | __mediaQueries: { 3298 | "@media (orientation: landscape)": [ 3299 | { 3300 | expressions: [ 3301 | { 3302 | feature: "orientation", 3303 | modifier: undefined, 3304 | value: "landscape", 3305 | }, 3306 | ], 3307 | inverse: false, 3308 | type: "all", 3309 | }, 3310 | ], 3311 | }, 3312 | }); 3313 | }); 3314 | 3315 | it("should throw for invalid types", () => { 3316 | expect(() => 3317 | transform( 3318 | ` 3319 | .foo { 3320 | color: blue; 3321 | } 3322 | 3323 | @media screens { 3324 | .foo { 3325 | color: red; 3326 | } 3327 | } 3328 | `, 3329 | { 3330 | parseMediaQueries: true, 3331 | }, 3332 | ), 3333 | ).toThrow('Failed to parse media query type "screens"'); 3334 | expect(() => 3335 | transform( 3336 | ` 3337 | .foo { 3338 | color: blue; 3339 | } 3340 | @media sdfgsdfg { 3341 | .foo { 3342 | color: red; 3343 | } 3344 | } 3345 | `, 3346 | { 3347 | parseMediaQueries: true, 3348 | }, 3349 | ), 3350 | ).toThrow('Failed to parse media query type "sdfgsdfg"'); 3351 | expect(() => 3352 | transform( 3353 | ` 3354 | .foo { 3355 | color: blue; 3356 | } 3357 | @media linux and (orientation: landscape) { 3358 | .foo { 3359 | color: red; 3360 | } 3361 | } 3362 | `, 3363 | { 3364 | parseMediaQueries: true, 3365 | }, 3366 | ), 3367 | ).toThrow('Failed to parse media query type "linux"'); 3368 | }); 3369 | 3370 | it("should throw for invalid features", () => { 3371 | expect(() => 3372 | transform( 3373 | ` 3374 | .foo { 3375 | color: blue; 3376 | } 3377 | @media (min-heigh: 50px) and (max-height: 150px) { 3378 | .foo { 3379 | color: red; 3380 | } 3381 | } 3382 | `, 3383 | { 3384 | parseMediaQueries: true, 3385 | }, 3386 | ), 3387 | ).toThrow('Failed to parse media query feature "min-heigh"'); 3388 | expect(() => 3389 | transform( 3390 | ` 3391 | .foo { 3392 | color: blue; 3393 | } 3394 | @media (orientations: landscape) { 3395 | .foo { 3396 | color: red; 3397 | } 3398 | } 3399 | `, 3400 | { 3401 | parseMediaQueries: true, 3402 | }, 3403 | ), 3404 | ).toThrow('Failed to parse media query feature "orientations"'); 3405 | }); 3406 | 3407 | it("should throw for values without units", () => { 3408 | expect(() => 3409 | transform( 3410 | ` 3411 | .foo { 3412 | color: blue; 3413 | } 3414 | @media (min-height: 50) and (max-height: 150px) { 3415 | .foo { 3416 | color: red; 3417 | } 3418 | } 3419 | `, 3420 | { 3421 | parseMediaQueries: true, 3422 | }, 3423 | ), 3424 | ).toThrow('Failed to parse media query expression "(min-height: 50)"'); 3425 | expect(() => 3426 | transform( 3427 | ` 3428 | .foo { 3429 | color: blue; 3430 | } 3431 | @media (min-height: 50px) and (max-height: 150) { 3432 | .foo { 3433 | color: red; 3434 | } 3435 | } 3436 | `, 3437 | { 3438 | parseMediaQueries: true, 3439 | }, 3440 | ), 3441 | ).toThrow('Failed to parse media query expression "(max-height: 150)"'); 3442 | expect(() => 3443 | transform( 3444 | ` 3445 | .foo { 3446 | color: blue; 3447 | } 3448 | @media (min-width) { 3449 | .foo { 3450 | color: red; 3451 | } 3452 | } 3453 | `, 3454 | { 3455 | parseMediaQueries: true, 3456 | }, 3457 | ), 3458 | ).toThrow('Failed to parse media query expression "(min-width)"'); 3459 | }); 3460 | }); 3461 | 3462 | describe("ICSS :export pseudo-selector", () => { 3463 | // https://github.com/css-modules/icss#export 3464 | 3465 | it("should parse ICSS :export pseudo-selectors", () => { 3466 | expect( 3467 | transform(` 3468 | :export { 3469 | whitecolor: #fcf5ed; 3470 | test: 1px; 3471 | } 3472 | `), 3473 | ).toEqual({ 3474 | whitecolor: "#fcf5ed", 3475 | test: "1px", 3476 | }); 3477 | }); 3478 | 3479 | it("if there is more than :export one in a file, the keys and values are combined and exported together", () => { 3480 | expect( 3481 | transform(` 3482 | 3483 | :export { 3484 | blackColor: #000; 3485 | } 3486 | 3487 | .bar { 3488 | color: blue; 3489 | } 3490 | 3491 | :export { 3492 | whitecolor: #fcf5ed; 3493 | test: 1px; 3494 | } 3495 | `), 3496 | ).toEqual({ 3497 | bar: { 3498 | color: "blue", 3499 | }, 3500 | blackColor: "#000", 3501 | whitecolor: "#fcf5ed", 3502 | test: "1px", 3503 | }); 3504 | }); 3505 | 3506 | it("should support exportedKey value with spaces", () => { 3507 | expect( 3508 | transform(` 3509 | :export { 3510 | blackColor: something is something; 3511 | } 3512 | 3513 | .bar { 3514 | color: blue; 3515 | } 3516 | `), 3517 | ).toEqual({ 3518 | bar: { 3519 | color: "blue", 3520 | }, 3521 | blackColor: "something is something", 3522 | }); 3523 | }); 3524 | 3525 | it("an exportedValue does not need to be quoted, it is already treated as a literal string", () => { 3526 | expect( 3527 | transform(` 3528 | :export { 3529 | foo: something; 3530 | boo: 0; 3531 | } 3532 | 3533 | .bar { 3534 | color: blue; 3535 | } 3536 | `), 3537 | ).toEqual({ 3538 | bar: { 3539 | color: "blue", 3540 | }, 3541 | foo: "something", 3542 | boo: "0", 3543 | }); 3544 | }); 3545 | 3546 | it("should parse :export and support the same exportedKey with different case", () => { 3547 | expect( 3548 | transform(` 3549 | :export { 3550 | whitecolor: #fcf5ed; 3551 | WhiteColor: #fff; 3552 | } 3553 | `), 3554 | ).toEqual({ 3555 | whitecolor: "#fcf5ed", 3556 | WhiteColor: "#fff", 3557 | }); 3558 | }); 3559 | 3560 | it("should parse a selector and :export", () => { 3561 | expect( 3562 | transform(` 3563 | .foo { 3564 | color: blue; 3565 | } 3566 | 3567 | :export { 3568 | whitecolor: #fcf5ed; 3569 | b: 0; 3570 | test: 1px; 3571 | } 3572 | `), 3573 | ).toEqual({ 3574 | foo: { 3575 | color: "blue", 3576 | }, 3577 | whitecolor: "#fcf5ed", 3578 | b: "0", 3579 | test: "1px", 3580 | }); 3581 | }); 3582 | 3583 | it("should do nothing with an empty :export block", () => { 3584 | expect( 3585 | transform(` 3586 | .foo { 3587 | color: blue; 3588 | } 3589 | 3590 | :export { 3591 | } 3592 | `), 3593 | ).toEqual({ 3594 | foo: { 3595 | color: "blue", 3596 | }, 3597 | }); 3598 | }); 3599 | 3600 | it("if a particular exportedKey is duplicated, the last (in source order) takes precedence.", () => { 3601 | expect( 3602 | transform(` 3603 | .foo { 3604 | color: blue; 3605 | } 3606 | 3607 | :export { 3608 | bar: 1; 3609 | bar: 2; 3610 | } 3611 | `), 3612 | ).toEqual({ 3613 | foo: { 3614 | color: "blue", 3615 | }, 3616 | bar: "2", 3617 | }); 3618 | expect( 3619 | transform(` 3620 | :export { 3621 | bar: 3; 3622 | } 3623 | 3624 | .foo { 3625 | color: blue; 3626 | } 3627 | 3628 | :export { 3629 | bar: 1; 3630 | bar: 2; 3631 | } 3632 | `), 3633 | ).toEqual({ 3634 | foo: { 3635 | color: "blue", 3636 | }, 3637 | bar: "2", 3638 | }); 3639 | expect( 3640 | transform(` 3641 | :export { 3642 | baz: 1; 3643 | bar: 3; 3644 | } 3645 | 3646 | .foo { 3647 | color: blue; 3648 | } 3649 | 3650 | :export { 3651 | bar: 1; 3652 | bar: 2; 3653 | } 3654 | `), 3655 | ).toEqual({ 3656 | foo: { 3657 | color: "blue", 3658 | }, 3659 | baz: "1", 3660 | bar: "2", 3661 | }); 3662 | }); 3663 | 3664 | it("should throw an error if exportedKey has the same name as a class and is defined twice", () => { 3665 | expect(() => 3666 | transform(` 3667 | :export { 3668 | bar: 1; 3669 | bar: 2; 3670 | } 3671 | 3672 | .bar { 3673 | color: blue; 3674 | } 3675 | `), 3676 | ).toThrow( 3677 | 'Failed to parse :export block because a CSS class in the same file is already using the name "bar"', 3678 | ); 3679 | }); 3680 | 3681 | it("should throw an error if exportedKey has the same name as a class", () => { 3682 | expect(() => 3683 | transform(` 3684 | .foo { 3685 | color: blue; 3686 | } 3687 | 3688 | :export { 3689 | foo: 1; 3690 | } 3691 | `), 3692 | ).toThrow( 3693 | 'Failed to parse :export block because a CSS class in the same file is already using the name "foo"', 3694 | ); 3695 | expect(() => 3696 | transform(` 3697 | :export { 3698 | foo: 1; 3699 | } 3700 | 3701 | .foo { 3702 | color: red; 3703 | } 3704 | `), 3705 | ).toThrow( 3706 | 'Failed to parse :export block because a CSS class in the same file is already using the name "foo"', 3707 | ); 3708 | expect(() => 3709 | transform(` 3710 | .foo { 3711 | color: blue; 3712 | } 3713 | 3714 | :export { 3715 | foo: 1; 3716 | } 3717 | 3718 | .foo { 3719 | color: red; 3720 | } 3721 | `), 3722 | ).toThrow( 3723 | 'Failed to parse :export block because a CSS class in the same file is already using the name "foo"', 3724 | ); 3725 | }); 3726 | 3727 | it("should throw for :export that is not top level", () => { 3728 | expect(() => 3729 | transform(` 3730 | .foo { 3731 | color: red; 3732 | :export { 3733 | bar: 1; 3734 | } 3735 | } 3736 | `), 3737 | ).toThrow(); 3738 | }); 3739 | }); 3740 | --------------------------------------------------------------------------------