├── .babelrc ├── .codeclimate.yml ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .jshintrc ├── .prettierrc ├── .travis.yml ├── .vscode └── settings.json ├── Changelog.md ├── LICENSE ├── README.md ├── modules ├── __tests__ │ ├── assignStyle-test.ts │ ├── camelCaseProperty-test.ts │ ├── cssifyDeclaration-test.ts │ ├── cssifyObject-test.ts │ ├── isPrefixedProperty-test.ts │ ├── isPrefixedValue-test.ts │ ├── isUnitlessProperty-test.ts │ ├── normalizeProperty-test.ts │ ├── resolveArrayValue-test.ts │ ├── unprefixProperty-test.ts │ └── unprefixValue-test.ts ├── assignStyle.ts ├── camelCaseProperty.ts ├── cssifyDeclaration.ts ├── cssifyObject.ts ├── hyphenate-style-name.d.ts ├── hyphenateProperty.ts ├── index.ts ├── isPrefixedProperty.ts ├── isPrefixedValue.ts ├── isUnitlessProperty.ts ├── normalizeProperty.ts ├── resolveArrayValue.ts ├── unprefixProperty.ts └── unprefixValue.ts ├── package.json ├── tsconfig.json └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "modules": false 7 | } 8 | ], 9 | "@babel/typescript" 10 | ], 11 | "env": { 12 | "commonjs": { 13 | "plugins": ["transform-es2015-modules-commonjs"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | languages: 2 | Ruby: true 3 | JavaScript: true 4 | PHP: true 5 | Python: true 6 | exclude_paths: 7 | - "dist/*" 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | modules/coverage/** -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": ["airbnb-base"], 4 | "plugins": ["@typescript-eslint"], 5 | "env": { 6 | "browser": true, 7 | "node": true, 8 | "jest": true 9 | }, 10 | "rules": { 11 | "semi": [2, "never"], 12 | "object-curly-newline": [0], 13 | "object-property-newline": [0], 14 | "comma-dangle": [0], 15 | "prefer-template": [0], 16 | "import/extensions": [0], // handled by TS 17 | "import/no-extraneous-dependencies": [0], 18 | "import/no-unresolved": [0], // handled by TS 19 | "react/prop-types": [0], 20 | "no-confusing-arrow": [0], 21 | "no-underscore-dangle": [0], 22 | "no-unused-vars": [0], // handled by TS 23 | "no-param-reassign": [0], 24 | "no-plusplus": [0], 25 | "guard-for-in": [0], 26 | "no-restricted-syntax": [0], 27 | "no-continue": [1], 28 | "no-prototype-builtins": [0], 29 | "max-len": [0, 80], 30 | "no-mixed-operators": [0], 31 | "no-lonely-if": [1], 32 | "no-bitwise": [0], 33 | "arrow-parens": [0], 34 | "operator-linebreak": [0] // handled by Prettier 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS or Editor files 2 | ._* 3 | .DS_Store 4 | Thumbs.db 5 | 6 | # Files that might appear on external disks 7 | .Spotlight-V100 8 | .Trashes 9 | 10 | # Always-ignore extensions 11 | *~ 12 | *.diff 13 | *.err 14 | *.log 15 | *.orig 16 | *.pyc 17 | *.rej 18 | *.sass-cache 19 | *.sw? 20 | *.vi 21 | 22 | 23 | node_modules 24 | es 25 | coverage 26 | lib 27 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "asi": true 4 | } 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "singleQuote": true, 4 | "bracketSpacing": true, 5 | "jsxBracketSameLine": true, 6 | "printWidth": 80, 7 | "tabWidth": 2, 8 | "useTabs": false, 9 | "semi": false 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" 4 | script: 5 | - npm run check 6 | addons: 7 | code_climate: 8 | repo_token: 354c5092a67b6e59c6e0a0081e306ab5934cf4e8860332c56d93538966809780 9 | after_script: 10 | - codeclimate-test-reporter < coverage/lcov.info 11 | notifications: 12 | email: false 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.exclude": { 3 | "**/._*": true, 4 | "**/.DS_Store": true, 5 | "**/Thumbs.db": true, 6 | "**/.Spotlight-V100": true, 7 | "**/.Trashes": true, 8 | "**/*~": true, 9 | "**/*.diff": true, 10 | "**/*.err": true, 11 | "**/*.log": true, 12 | "**/*.orig": true, 13 | "**/*.pyc": true, 14 | "**/*.rej": true, 15 | "**/*.sass-cache": true, 16 | "**/*.sw?": true, 17 | "**/*.vi": true, 18 | "**/node_modules": true, 19 | "**/es": true, 20 | "**/coverage": true, 21 | "**/lib": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ### 3.1.0 4 | 5 | - added a cache to improve performance for recuring properties 6 | - added TypeScript support 7 | 8 | ### 3.0.2 9 | 10 | - added new unitless properties 11 | 12 | ### 3.0.0 13 | 14 | - `assignStyle` now correctly merges array values without duplicates where the last occurence always wins in order 15 | 16 | ### 2.0.0 17 | 18 | - improve `assignStyle` to replace arrays 19 | 20 | ### 1.0.3 21 | 22 | - performance improvements 23 | 24 | ### 1.0.2 25 | 26 | - added `resolveArrayValue` and `assignStyle` 27 | 28 | ### 1.0.1 29 | 30 | - added `cssifyDeclaration` and `cssifyObject` 31 | 32 | ### 1.0.0 33 | 34 | Initial version 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Robin Frischmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSS-in-JS Utilities 2 | A library that provides useful utilities functions for CSS-in-JS solutions.
3 | They are intended to be used by CSS-in-JS library authors rather used directly. 4 |
5 | 6 | TravisCI Test Coverage npm downloads npm version gzipped size 7 | 8 | ## Installation 9 | ```sh 10 | yarn add css-in-js-utils 11 | ``` 12 | 13 | ## Why? 14 | By now I have authored and collaborated on many different libraries and found I would rewrite the very same utility functions every time. That's why this repository is hosting small utilities especially built for CSS-in-JS solutions and tools. Even if there are tons of different libraries already, they all basically use the same mechanisms and utilities. 15 | 16 | ## Utilities 17 | * [`assignStyle(base, ...extend)`](#assignstylebase-extend) 18 | * [`camelCaseProperty(property)`](#camelcasepropertyproperty) 19 | * [`cssifyDeclaration(property, value)`](#cssifydeclarationproperty-value) 20 | * [`cssifyObject(object)`](#cssifyobjectobject) 21 | * [`hyphenateProperty(property)`](#hyphenatepropertyproperty) 22 | * [`isPrefixedProperty(property)`](#isprefixedpropertyproperty) 23 | * [`isPrefixedValue(value)`](#isprefixedvaluevalue) 24 | * [`isUnitlessProperty(property)`](#isunitlesspropertyproperty) 25 | * [`normalizeProperty(property)`](#normalizepropertyproperty) 26 | * [`resolveArrayValue(property, value)`](#resolvearrayvalueproperty-value) 27 | * [`unprefixProperty(property)`](#unprefixpropertyproperty) 28 | * [`unprefixValue(value)`](#unprefixvaluevalue) 29 | 30 | ------ 31 | 32 | ### `assignStyle(base, ...extend)` 33 | Merges deep style objects similar to `Object.assign`.
34 | It also merges array values into a single array whithout creating duplicates. The last occurence of every item wins. 35 | 36 | ```javascript 37 | import { assignStyle } from 'css-in-js-utils' 38 | 39 | assignStyle( 40 | { color: 'red', backgroundColor: 'black' }, 41 | { color: 'blue' } 42 | ) 43 | // => { color: 'blue', backgroundColor: 'black' } 44 | 45 | assignStyle( 46 | { 47 | color: 'red', 48 | ':hover': { 49 | backgroundColor: 'black' 50 | } 51 | }, 52 | {  53 | ':hover': { 54 | backgroundColor: 'blue' 55 | } 56 | } 57 | ) 58 | // => { color: 'red', ':hover': { backgroundColor: 'blue' }} 59 | ``` 60 | 61 | ------ 62 | 63 | ### `camelCaseProperty(property)` 64 | Converts the `property` to camelCase. 65 | 66 | ```javascript 67 | import { camelCaseProperty } from 'css-in-js-utils' 68 | 69 | camelCaseProperty('padding-top') 70 | // => 'paddingTop' 71 | 72 | camelCaseProperty('-webkit-transition') 73 | // => 'WebkitTransition' 74 | ``` 75 | 76 | ------ 77 | 78 | ### `cssifyDeclaration(property, value)` 79 | Generates a CSS declaration (`property`:`value`) string. 80 | 81 | ```javascript 82 | import { cssifyDeclaration } from 'css-in-js-utils' 83 | 84 | cssifyDeclaration('paddingTop', '400px') 85 | // => 'padding-top:400px' 86 | 87 | cssifyDeclaration('WebkitFlex', 3) 88 | // => '-webkit-flex:3' 89 | ``` 90 | 91 | ------ 92 | 93 | ### `cssifyObject(object)` 94 | Generates a CSS string using all key-property pairs in `object`. 95 | It automatically removes declarations with value types other than `number` and `string`. 96 | 97 | ```javascript 98 | import { cssifyObject } from 'css-in-js-utils' 99 | 100 | cssifyObject({ 101 | paddingTop: '400px', 102 | paddingBottom: undefined, 103 | WebkitFlex: 3, 104 | _anyKey: [1, 2, 4] 105 | }) 106 | // => 'padding-top:400px;-webkit-flex:3' 107 | ``` 108 | 109 | ------ 110 | 111 | ### `hyphenateProperty(property)` 112 | Converts the `property` to hyphen-case. 113 | > Directly mirrors [hyphenate-style-name](https://github.com/rexxars/hyphenate-style-name). 114 | 115 | ```javascript 116 | import { hyphenateProperty } from 'css-in-js-utils' 117 | 118 | hyphenateProperty('paddingTop') 119 | // => 'padding-top' 120 | 121 | hyphenateProperty('WebkitTransition') 122 | // => '-webkit-transition' 123 | ``` 124 | 125 | ------ 126 | 127 | ### `isPrefixedProperty(property)` 128 | Checks if a `property` includes a vendor prefix. 129 | 130 | ```javascript 131 | import { isPrefixedProperty } from 'css-in-js-utils' 132 | 133 | isPrefixedProperty('paddingTop') 134 | // => false 135 | 136 | isPrefixedProperty('WebkitTransition') 137 | // => true 138 | ``` 139 | 140 | ------ 141 | ### `isPrefixedValue(value)` 142 | Checks if a `value` includes vendor prefixes. 143 | 144 | ```javascript 145 | import { isPrefixedValue } from 'css-in-js-utils' 146 | 147 | isPrefixedValue('200px') 148 | isPrefixedValue(200) 149 | // => false 150 | 151 | isPrefixedValue('-webkit-calc(100% - 50px)') 152 | // => true 153 | ``` 154 | 155 | ------ 156 | 157 | ### `isUnitlessProperty(property)` 158 | Checks if a `property` accepts unitless values. 159 | 160 | ```javascript 161 | import { isUnitlessProperty } from 'css-in-js-utils' 162 | 163 | isUnitlessProperty('width') 164 | // => false 165 | 166 | isUnitlessProperty('flexGrow') 167 | isUnitlessProperty('lineHeight') 168 | isUnitlessProperty('line-height') 169 | // => true 170 | ``` 171 | 172 | ------ 173 | 174 | ### `normalizeProperty(property)` 175 | Normalizes the `property` by unprefixing **and** camelCasing it. 176 | > Uses the [`camelCaseProperty`](#camelcasepropertyproperty) and [`unprefixProperty`](#unprefixpropertyproperty)-methods. 177 | 178 | ```javascript 179 | import { normalizeProperty } from 'css-in-js-utils' 180 | 181 | normalizeProperty('-webkit-transition-delay') 182 | // => 'transitionDelay' 183 | ``` 184 | 185 | ------ 186 | 187 | ### `resolveArrayValue(property, value)` 188 | Concatenates array values to single CSS value. 189 | > Uses the [`hyphenateProperty`](#hyphenatepropertyproperty)-method. 190 | 191 | 192 | ```javascript 193 | import { resolveArrayValue } from 'css-in-js-utils' 194 | 195 | resolveArrayValue('display', [ '-webkit-flex', 'flex' ]) 196 | // => '-webkit-flex;display:flex' 197 | 198 | resolveArrayValue('paddingTop', [ 'calc(100% - 50px)', '100px' ]) 199 | // => 'calc(100% - 50px);padding-top:100px' 200 | ``` 201 | 202 | ------ 203 | 204 | ### `unprefixProperty(property)` 205 | Removes the vendor prefix (if set) from the `property`. 206 | 207 | ```javascript 208 | import { unprefixProperty } from 'css-in-js-utils' 209 | 210 | unprefixProperty('WebkitTransition') 211 | // => 'transition' 212 | 213 | unprefixProperty('transitionDelay') 214 | // => 'transitionDelay' 215 | ``` 216 | 217 | ------ 218 | 219 | ### `unprefixValue(value)` 220 | Removes all vendor prefixes (if any) from the `value`. 221 | 222 | ```javascript 223 | import { unprefixValue } from 'css-in-js-utils' 224 | 225 | unprefixValue('-webkit-calc(-moz-calc(100% - 50px)/2)') 226 | // => 'calc(calc(100% - 50px)/2)' 227 | 228 | unprefixValue('100px') 229 | // => '100px' 230 | ``` 231 | 232 | ## Direct Import 233 | Every utility function may be imported directly to save bundle size. 234 | 235 | ```javascript 236 | import camelCaseProperty from 'css-in-js-utils/lib/camelCaseProperty' 237 | ``` 238 | 239 | ## License 240 | css-in-js-utils is licensed under the [MIT License](http://opensource.org/licenses/MIT).
241 | Documentation is licensed under [Creative Common License](http://creativecommons.org/licenses/by/4.0/).
242 | Created with ♥ by [@rofrischmann](http://rofrischmann.de). 243 | -------------------------------------------------------------------------------- /modules/__tests__/assignStyle-test.ts: -------------------------------------------------------------------------------- 1 | import assignStyle from '../assignStyle' 2 | 3 | describe('Assinging styles', () => { 4 | it('should merge properties', () => { 5 | expect( 6 | assignStyle({ color: 'red' }, { fontSize: 12 }, { lineHeight: 1 }) 7 | ).toEqual({ 8 | color: 'red', 9 | fontSize: 12, 10 | lineHeight: 1, 11 | }) 12 | }) 13 | 14 | it('should overwrite properties from right to left', () => { 15 | expect( 16 | assignStyle({ fontSize: 12 }, { fontSize: 16 }, { fontSize: 11 }) 17 | ).toEqual({ fontSize: 11 }) 18 | }) 19 | 20 | it('should merge nested objects', () => { 21 | expect( 22 | assignStyle( 23 | { 24 | fontSize: 12, 25 | ob2: { color: 'red' }, 26 | ob3: { color: 'red' }, 27 | }, 28 | { 29 | fontSize: 16, 30 | ob2: { fontSize: 12 }, 31 | }, 32 | { 33 | fontSize: 11, 34 | ob3: { color: 'blue' }, 35 | } 36 | ) 37 | ).toEqual({ 38 | fontSize: 11, 39 | ob2: { 40 | color: 'red', 41 | fontSize: 12, 42 | }, 43 | ob3: { color: 'blue' }, 44 | }) 45 | }) 46 | 47 | it('should not overwrite objects other than the first one', () => { 48 | const ob1 = { color: 'red' } 49 | const ob2 = { fontSize: 12 } 50 | 51 | const newOb = assignStyle({}, ob1, ob2) 52 | 53 | expect(newOb).toEqual({ 54 | color: 'red', 55 | fontSize: 12, 56 | }) 57 | 58 | newOb.foo = 'bar' 59 | expect(ob1).toEqual({ color: 'red' }) 60 | expect(ob2).toEqual({ fontSize: 12 }) 61 | }) 62 | 63 | it('should use the first object as base', () => { 64 | const ob1 = { color: 'red' } 65 | const ob2 = { fontSize: 12 } 66 | 67 | const newOb = assignStyle(ob1, ob2) 68 | 69 | expect(newOb).toEqual({ 70 | color: 'red', 71 | fontSize: 12, 72 | }) 73 | 74 | expect(ob1).toEqual(newOb) 75 | 76 | newOb.foo = 'bar' 77 | expect(ob1).toEqual({ 78 | color: 'red', 79 | fontSize: 12, 80 | foo: 'bar', 81 | }) 82 | }) 83 | 84 | it('should not recursively call assignStyle for null values', () => { 85 | const ob1 = { fontSize: 10 } 86 | const ob2 = { margin: null } 87 | 88 | const newOb = assignStyle({}, ob1, ob2) 89 | 90 | expect(newOb).toEqual({ 91 | fontSize: 10, 92 | margin: null, 93 | }) 94 | }) 95 | 96 | it('should merge array values (array-single)', () => { 97 | const ob1 = { fontSize: ['10px', '10rem'] } 98 | const ob2 = { fontSize: 20 } 99 | 100 | const newOb = assignStyle({}, ob1, ob2) 101 | 102 | expect(newOb).toEqual({ fontSize: ['10px', '10rem', 20] }) 103 | }) 104 | 105 | it('should merge array values (single-array)', () => { 106 | const ob1 = { fontSize: 10 } 107 | const ob2 = { fontSize: ['10px', '20vw'] } 108 | 109 | const newOb = assignStyle({}, ob1, ob2) 110 | 111 | expect(newOb).toEqual({ fontSize: [10, '10px', '20vw'] }) 112 | }) 113 | 114 | it('should merge array values (array-array)', () => { 115 | const ob1 = { fontSize: ['20pt', 10] } 116 | const ob2 = { fontSize: ['10px', '20vw'] } 117 | 118 | const newOb = assignStyle({}, ob1, ob2) 119 | 120 | expect(newOb).toEqual({ fontSize: ['20pt', 10, '10px', '20vw'] }) 121 | }) 122 | 123 | it('should merge array values without duplicates (array-single)', () => { 124 | const ob1 = { fontSize: ['10px', '10rem'] } 125 | const ob2 = { fontSize: '10px' } 126 | 127 | const newOb = assignStyle({}, ob1, ob2) 128 | 129 | expect(newOb).toEqual({ fontSize: ['10rem', '10px'] }) 130 | }) 131 | 132 | it('should merge array values without duplicates (array-array)', () => { 133 | const ob1 = { fontSize: ['20px', '10rem', '10px'] } 134 | const ob2 = { fontSize: ['10px', 5, '10rem'] } 135 | 136 | const newOb = assignStyle({}, ob1, ob2) 137 | 138 | expect(newOb).toEqual({ fontSize: ['20px', '10px', 5, '10rem'] }) 139 | }) 140 | 141 | it('should merge array values without duplicates (single-array)', () => { 142 | const ob1 = { fontSize: '10px' } 143 | const ob2 = { fontSize: ['10rem', '10px'] } 144 | 145 | const newOb = assignStyle({}, ob1, ob2) 146 | 147 | expect(newOb).toEqual({ fontSize: ['10rem', '10px'] }) 148 | }) 149 | 150 | it('should not recursively call assignStyle for null values', () => { 151 | const ob1 = { fontSize: 10 } 152 | const ob2 = { margin: null } 153 | 154 | const newOb = assignStyle({}, ob1, ob2) 155 | 156 | expect(newOb).toEqual({ 157 | fontSize: 10, 158 | margin: null, 159 | }) 160 | }) 161 | }) 162 | -------------------------------------------------------------------------------- /modules/__tests__/camelCaseProperty-test.ts: -------------------------------------------------------------------------------- 1 | import camelCaseProperty from '../camelCaseProperty' 2 | 3 | describe('Camel casing properties', () => { 4 | it('should camel case properties', () => { 5 | expect(camelCaseProperty('transition-delay')).toEqual('transitionDelay') 6 | expect(camelCaseProperty('-webkit-transition-delay')).toEqual( 7 | 'WebkitTransitionDelay' 8 | ) 9 | expect(camelCaseProperty('-ms-transition')).toEqual('msTransition') 10 | }) 11 | it('should return same output on same input', () => { 12 | expect(camelCaseProperty('border-color')).toEqual('borderColor') 13 | expect(camelCaseProperty('border-color')).toEqual('borderColor') 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /modules/__tests__/cssifyDeclaration-test.ts: -------------------------------------------------------------------------------- 1 | import cssifyDeclaration from '../cssifyDeclaration' 2 | 3 | describe('Cssifying declarations', () => { 4 | it('should return a valid css declaration', () => { 5 | expect(cssifyDeclaration('width', '300px')).toEqual('width:300px') 6 | expect(cssifyDeclaration('WebkitFlex', '1')).toEqual('-webkit-flex:1') 7 | expect(cssifyDeclaration('msTransitionDuration', '3s')).toEqual( 8 | '-ms-transition-duration:3s' 9 | ) 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /modules/__tests__/cssifyObject-test.ts: -------------------------------------------------------------------------------- 1 | import cssifyObject from '../cssifyObject' 2 | 3 | describe('Cssifying objects', () => { 4 | it('should generate a valid CSS string', () => { 5 | expect(cssifyObject({ color: 'red' })).toEqual('color:red') 6 | }) 7 | 8 | it('should convert properties to dash case', () => { 9 | expect(cssifyObject({ fontSize: '12px' })).toEqual('font-size:12px') 10 | }) 11 | 12 | it('should separate declarations with semicolons', () => { 13 | expect( 14 | cssifyObject({ 15 | fontSize: '12px', 16 | color: 'red', 17 | }) 18 | ).toEqual('font-size:12px;color:red') 19 | }) 20 | 21 | it('should convert vendor prefixes', () => { 22 | expect( 23 | cssifyObject({ 24 | WebkitJustifyContent: 'center', 25 | msFlexAlign: 'center', 26 | }) 27 | ).toEqual('-webkit-justify-content:center;-ms-flex-align:center') 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /modules/__tests__/isPrefixedProperty-test.ts: -------------------------------------------------------------------------------- 1 | import isPrefixedProperty from '../isPrefixedProperty' 2 | 3 | describe('Checking for prefixed properties', () => { 4 | it('should return true', () => { 5 | expect(isPrefixedProperty('WebkitTransition')).toEqual(true) 6 | expect(isPrefixedProperty('msTransitionDelay')).toEqual(true) 7 | }) 8 | 9 | it('should return false', () => { 10 | expect(isPrefixedProperty('transition')).toEqual(false) 11 | expect(isPrefixedProperty('transitionDelay')).toEqual(false) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /modules/__tests__/isPrefixedValue-test.ts: -------------------------------------------------------------------------------- 1 | import isPrefixedValue from '../isPrefixedValue' 2 | 3 | describe('Checking for prefixed values', () => { 4 | it('should return true', () => { 5 | expect(isPrefixedValue('-webkit-calc(100% - 20px)')).toEqual(true) 6 | expect(isPrefixedValue('-ms-transition')).toEqual(true) 7 | }) 8 | 9 | it('should return false', () => { 10 | expect(isPrefixedValue('200px')).toEqual(false) 11 | expect(isPrefixedValue('calc(100% - 20px)')).toEqual(false) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /modules/__tests__/isUnitlessProperty-test.ts: -------------------------------------------------------------------------------- 1 | import isUnitlessProperty from '../isUnitlessProperty' 2 | 3 | describe('Checking for unitless CSS properties', () => { 4 | it('should return true for unitless properties', () => { 5 | expect(isUnitlessProperty('fontWeight')).toEqual(true) 6 | expect(isUnitlessProperty('flex')).toEqual(true) 7 | expect(isUnitlessProperty('gridColumn')).toEqual(true) 8 | }) 9 | 10 | it('should return true for hypenated unitless properties', () => { 11 | expect(isUnitlessProperty('font-weight')).toEqual(true) 12 | expect(isUnitlessProperty('grid-column')).toEqual(true) 13 | }) 14 | 15 | it('should return true for prefixed unitless properties', () => { 16 | expect(isUnitlessProperty('WebkitFlex')).toEqual(true) 17 | expect(isUnitlessProperty('msFlex')).toEqual(true) 18 | expect(isUnitlessProperty('WebkitColumnCount')).toEqual(true) 19 | expect(isUnitlessProperty('msColumnCount')).toEqual(true) 20 | }) 21 | 22 | it('should return true for hypenated prefixed unitless properties', () => { 23 | expect(isUnitlessProperty('-webkit-flex')).toEqual(true) 24 | expect(isUnitlessProperty('-ms-flex')).toEqual(true) 25 | expect(isUnitlessProperty('-webkit-column-count')).toEqual(true) 26 | expect(isUnitlessProperty('-ms-column-count')).toEqual(true) 27 | }) 28 | 29 | it('should equal false for other properties', () => { 30 | expect(isUnitlessProperty('fontSize')).toEqual(false) 31 | expect(isUnitlessProperty('font-size')).toEqual(false) 32 | expect(isUnitlessProperty('-webkit-border-radius')).toEqual(false) 33 | expect(isUnitlessProperty('-ms-border-radius')).toEqual(false) 34 | expect(isUnitlessProperty('WebkitBorderRadius')).toEqual(false) 35 | expect(isUnitlessProperty('msBorderRadius')).toEqual(false) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /modules/__tests__/normalizeProperty-test.ts: -------------------------------------------------------------------------------- 1 | import normalizeProperty from '../normalizeProperty' 2 | 3 | describe('Normalizing properties', () => { 4 | it('should camel case hypenated properties', () => { 5 | expect(normalizeProperty('transition-delay')).toEqual('transitionDelay') 6 | }) 7 | 8 | it('should unprefix properties', () => { 9 | expect(normalizeProperty('WebkitTransitionDelay')).toEqual( 10 | 'transitionDelay' 11 | ) 12 | }) 13 | 14 | it('should unprefix and camel case properties', () => { 15 | expect(normalizeProperty('-webkit-transition-delay')).toEqual( 16 | 'transitionDelay' 17 | ) 18 | }) 19 | }) 20 | -------------------------------------------------------------------------------- /modules/__tests__/resolveArrayValue-test.ts: -------------------------------------------------------------------------------- 1 | import resolveArrayValue from '../resolveArrayValue' 2 | 3 | describe('Resolving array values', () => { 4 | it('should return a concatenated css value', () => { 5 | expect(resolveArrayValue('width', ['300px', '100px'])).toEqual( 6 | '300px;width:100px' 7 | ) 8 | }) 9 | 10 | it('should hyphenate property names', () => { 11 | expect(resolveArrayValue('WebkitFlex', [1, 2, 3])).toEqual( 12 | '1;-webkit-flex:2;-webkit-flex:3' 13 | ) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /modules/__tests__/unprefixProperty-test.ts: -------------------------------------------------------------------------------- 1 | import unprefixProperty from '../unprefixProperty' 2 | 3 | describe('Unprefixing properties', () => { 4 | it('should unprefix the property', () => { 5 | expect(unprefixProperty('msFlex')).toEqual('flex') 6 | expect(unprefixProperty('WebkitFlex')).toEqual('flex') 7 | }) 8 | 9 | it('should keep an unprefixed property', () => { 10 | expect(unprefixProperty('flex')).toEqual('flex') 11 | expect(unprefixProperty('padding')).toEqual('padding') 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /modules/__tests__/unprefixValue-test.ts: -------------------------------------------------------------------------------- 1 | import unprefixValue from '../unprefixValue' 2 | 3 | describe('Unprefixing values', () => { 4 | it('should unprefix the value', () => { 5 | expect(unprefixValue('-webkit-calc(100% - 20px)')).toEqual( 6 | 'calc(100% - 20px)' 7 | ) 8 | expect(unprefixValue('-ms-transition')).toEqual('transition') 9 | }) 10 | 11 | it('should keep an unprefixed value', () => { 12 | expect(unprefixValue('300px')).toEqual('300px') 13 | expect(unprefixValue(300)).toEqual(300) 14 | expect(unprefixValue('calc(100% - 20px)')).toEqual('calc(100% - 20px)') 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /modules/assignStyle.ts: -------------------------------------------------------------------------------- 1 | function filterUniqueArray(arr: any[]) { 2 | return arr.filter((val, index) => arr.lastIndexOf(val) === index) 3 | } 4 | 5 | type StyleObject = { 6 | [key: string]: 7 | | string 8 | | number 9 | | StyleObject 10 | | (string | number | StyleObject)[] 11 | } 12 | 13 | export default function assignStyle( 14 | base: StyleObject, 15 | ...extendingStyles: StyleObject[] 16 | ) { 17 | for (let i = 0, len = extendingStyles.length; i < len; ++i) { 18 | const style = extendingStyles[i] 19 | 20 | for (const property in style) { 21 | const value = style[property] 22 | const baseValue = base[property] 23 | 24 | if (baseValue && value) { 25 | if (Array.isArray(baseValue)) { 26 | base[property] = filterUniqueArray(baseValue.concat(value)) 27 | continue 28 | } 29 | 30 | if (Array.isArray(value)) { 31 | base[property] = filterUniqueArray([baseValue, ...value]) 32 | continue 33 | } 34 | 35 | if (typeof value === 'object') { 36 | base[property] = assignStyle({}, baseValue as StyleObject, value) 37 | continue 38 | } 39 | } 40 | 41 | base[property] = value 42 | } 43 | } 44 | 45 | return base 46 | } 47 | -------------------------------------------------------------------------------- /modules/camelCaseProperty.ts: -------------------------------------------------------------------------------- 1 | type CacheObject = { 2 | [key: string]: string 3 | } 4 | 5 | const CSS_VARIABLE = /^--/; 6 | const DASH = /-([a-z])/g 7 | const MS = /^Ms/g 8 | const cache: CacheObject = {} 9 | 10 | function toUpper(match: string) { 11 | return match[1].toUpperCase() 12 | } 13 | 14 | export default function camelCaseProperty(property: string) { 15 | if (cache.hasOwnProperty(property)) { 16 | return cache[property] 17 | } 18 | 19 | const camelProp = CSS_VARIABLE.test(property) ? property : property.replace(DASH, toUpper).replace(MS, 'ms') 20 | cache[property] = camelProp 21 | 22 | return camelProp 23 | } 24 | -------------------------------------------------------------------------------- /modules/cssifyDeclaration.ts: -------------------------------------------------------------------------------- 1 | import hyphenateProperty from './hyphenateProperty' 2 | 3 | export default function cssifyDeclaration( 4 | property: string, 5 | value: string | number 6 | ) { 7 | return hyphenateProperty(property) + ':' + value 8 | } 9 | -------------------------------------------------------------------------------- /modules/cssifyObject.ts: -------------------------------------------------------------------------------- 1 | import cssifyDeclaration from './cssifyDeclaration' 2 | 3 | export type StyleObject = { 4 | [key: string]: 5 | | string 6 | | number 7 | | StyleObject 8 | | (string | number | StyleObject)[] 9 | } 10 | 11 | export default function cssifyObject(style: StyleObject) { 12 | let css = '' 13 | 14 | for (const property in style) { 15 | const value = style[property] 16 | if (typeof value !== 'string' && typeof value !== 'number') { 17 | continue 18 | } 19 | 20 | // prevents the semicolon after 21 | // the last rule declaration 22 | if (css) { 23 | css += ';' 24 | } 25 | 26 | css += cssifyDeclaration(property, value) 27 | } 28 | 29 | return css 30 | } 31 | -------------------------------------------------------------------------------- /modules/hyphenate-style-name.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'hyphenate-style-name' { 2 | export default function hyphenateStyleName(property: string | number): string 3 | } 4 | -------------------------------------------------------------------------------- /modules/hyphenateProperty.ts: -------------------------------------------------------------------------------- 1 | import hyphenateStyleName from 'hyphenate-style-name' 2 | 3 | export default function hyphenateProperty(property: string | number) { 4 | return hyphenateStyleName(property) 5 | } 6 | -------------------------------------------------------------------------------- /modules/index.ts: -------------------------------------------------------------------------------- 1 | import assignStyle from './assignStyle' 2 | import camelCaseProperty from './camelCaseProperty' 3 | import cssifyDeclaration from './cssifyDeclaration' 4 | import cssifyObject from './cssifyObject' 5 | import hyphenateProperty from './hyphenateProperty' 6 | import isPrefixedProperty from './isPrefixedProperty' 7 | import isPrefixedValue from './isPrefixedValue' 8 | import isUnitlessProperty from './isUnitlessProperty' 9 | import normalizeProperty from './normalizeProperty' 10 | import resolveArrayValue from './resolveArrayValue' 11 | import unprefixProperty from './unprefixProperty' 12 | import unprefixValue from './unprefixValue' 13 | 14 | export { 15 | assignStyle, 16 | camelCaseProperty, 17 | cssifyDeclaration, 18 | cssifyObject, 19 | hyphenateProperty, 20 | isPrefixedProperty, 21 | isPrefixedValue, 22 | isUnitlessProperty, 23 | normalizeProperty, 24 | resolveArrayValue, 25 | unprefixProperty, 26 | unprefixValue, 27 | } 28 | -------------------------------------------------------------------------------- /modules/isPrefixedProperty.ts: -------------------------------------------------------------------------------- 1 | const RE = /^(Webkit|Moz|O|ms)/ 2 | 3 | export default function isPrefixedProperty(property: string) { 4 | return RE.test(property) 5 | } 6 | -------------------------------------------------------------------------------- /modules/isPrefixedValue.ts: -------------------------------------------------------------------------------- 1 | const RE = /-webkit-|-moz-|-ms-/ 2 | 3 | export default function isPrefixedValue(value: string) { 4 | return typeof value === 'string' && RE.test(value) 5 | } 6 | -------------------------------------------------------------------------------- /modules/isUnitlessProperty.ts: -------------------------------------------------------------------------------- 1 | import hyphenateProperty from './hyphenateProperty' 2 | 3 | const unitlessProperties: { [key: string]: boolean } = { 4 | borderImageOutset: true, 5 | borderImageSlice: true, 6 | borderImageWidth: true, 7 | fontWeight: true, 8 | lineHeight: true, 9 | opacity: true, 10 | orphans: true, 11 | tabSize: true, 12 | widows: true, 13 | zIndex: true, 14 | zoom: true, 15 | // SVG-related properties 16 | fillOpacity: true, 17 | floodOpacity: true, 18 | stopOpacity: true, 19 | strokeDasharray: true, 20 | strokeDashoffset: true, 21 | strokeMiterlimit: true, 22 | strokeOpacity: true, 23 | strokeWidth: true, 24 | } 25 | 26 | const prefixedUnitlessProperties = [ 27 | 'animationIterationCount', 28 | 'boxFlex', 29 | 'boxFlexGroup', 30 | 'boxOrdinalGroup', 31 | 'columnCount', 32 | 'flex', 33 | 'flexGrow', 34 | 'flexPositive', 35 | 'flexShrink', 36 | 'flexNegative', 37 | 'flexOrder', 38 | 'gridColumn', 39 | 'gridColumnEnd', 40 | 'gridColumnStart', 41 | 'gridRow', 42 | 'gridRowEnd', 43 | 'gridRowStart', 44 | 'lineClamp', 45 | 'order', 46 | ] 47 | 48 | const prefixes = ['Webkit', 'ms', 'Moz', 'O'] 49 | 50 | function getPrefixedProperty(prefix: string, property: string) { 51 | return prefix + property.charAt(0).toUpperCase() + property.slice(1) 52 | } 53 | 54 | // add all prefixed properties to the unitless properties 55 | for (let i = 0, len = prefixedUnitlessProperties.length; i < len; ++i) { 56 | const property = prefixedUnitlessProperties[i] 57 | unitlessProperties[property] = true 58 | 59 | for (let j = 0, jLen = prefixes.length; j < jLen; ++j) { 60 | unitlessProperties[getPrefixedProperty(prefixes[j], property)] = true 61 | } 62 | } 63 | 64 | // add all hypenated properties as well 65 | for (const property in unitlessProperties) { 66 | unitlessProperties[hyphenateProperty(property)] = true 67 | } 68 | 69 | export default function isUnitlessProperty(property: string) { 70 | return unitlessProperties.hasOwnProperty(property) 71 | } 72 | -------------------------------------------------------------------------------- /modules/normalizeProperty.ts: -------------------------------------------------------------------------------- 1 | import camelCaseProperty from './camelCaseProperty' 2 | import unprefixProperty from './unprefixProperty' 3 | 4 | export default function normalizeProperty(property: string) { 5 | return unprefixProperty(camelCaseProperty(property)) 6 | } 7 | -------------------------------------------------------------------------------- /modules/resolveArrayValue.ts: -------------------------------------------------------------------------------- 1 | import hyphenateProperty from './hyphenateProperty' 2 | 3 | export default function resolveArrayValue(property: string, value: string[]) { 4 | return value.join(';' + hyphenateProperty(property) + ':') 5 | } 6 | -------------------------------------------------------------------------------- /modules/unprefixProperty.ts: -------------------------------------------------------------------------------- 1 | const RE = /^(ms|Webkit|Moz|O)/ 2 | 3 | export default function unprefixProperty(property: string) { 4 | const propertyWithoutPrefix = property.replace(RE, '') 5 | return ( 6 | propertyWithoutPrefix.charAt(0).toLowerCase() + 7 | propertyWithoutPrefix.slice(1) 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /modules/unprefixValue.ts: -------------------------------------------------------------------------------- 1 | const RE = /(-ms-|-webkit-|-moz-|-o-)/g 2 | 3 | export default function unprefixValue(value: string | string[]) { 4 | if (typeof value === 'string') { 5 | return value.replace(RE, '') 6 | } 7 | 8 | return value 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-in-js-utils", 3 | "version": "3.1.0", 4 | "description": "Useful utility functions for CSS in JS solutions", 5 | "main": "lib/index.js", 6 | "module": "es/index.js", 7 | "types": "es/index.d.ts", 8 | "jsnext:main": "es/index.js", 9 | "sideEffects": false, 10 | "files": [ 11 | "lib/**", 12 | "es/**" 13 | ], 14 | "keywords": [ 15 | "css", 16 | "cssinjs", 17 | "utils", 18 | "small" 19 | ], 20 | "repository": "https://github.com/robinweser/css-in-js-utils.git", 21 | "author": "robinweser ", 22 | "license": "MIT", 23 | "scripts": { 24 | "build": "yarn run clean && yarn build:es && yarn build:lib", 25 | "build:lib": "cross-env BABEL_ENV=commonjs babel modules --extensions '.ts' --ignore 'modules/__tests__','**/*.d.ts' --out-dir lib", 26 | "build:es": "tsc && babel es --out-dir es", 27 | "clean": "rimraf es lib coverage", 28 | "check": "yarn lint && yarn test:coverage", 29 | "format": "prettier --write \"modules/**/*.{js,ts}\"", 30 | "lint": "eslint 'modules/**/*.{js,ts}'", 31 | "release": "git pull --rebase && yarn run check && yarn build && npm publish", 32 | "test": "cross-env BABEL_ENV=commonjs jest", 33 | "test:coverage": "cross-env BABEL_ENV=commonjs jest --coverage", 34 | "watch": "yarn test -- --watch" 35 | }, 36 | "jest": { 37 | "moduleDirectories": [ 38 | "node_modules", 39 | "modules" 40 | ] 41 | }, 42 | "dependencies": { 43 | "hyphenate-style-name": "^1.0.3" 44 | }, 45 | "devDependencies": { 46 | "@babel/cli": "^7.8.4", 47 | "@babel/core": "^7.9.0", 48 | "@babel/preset-env": "^7.9.5", 49 | "@babel/preset-typescript": "^7.9.0", 50 | "@types/jest": "^25.2.1", 51 | "@typescript-eslint/eslint-plugin": "^2.26.0", 52 | "@typescript-eslint/parser": "^2.26.0", 53 | "babel-jest": "^25.2.6", 54 | "babel-plugin-transform-es2015-modules-commonjs": "^6.26.2", 55 | "cross-env": "^7.0.2", 56 | "eslint": "^6.8.0", 57 | "eslint-config-airbnb-base": "^14.1.0", 58 | "eslint-plugin-import": "^2.20.2", 59 | "jest": "^25.2.6", 60 | "prettier": "^1.7.4", 61 | "rimraf": "^2.6.1", 62 | "typescript": "^3.8.3" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "declaration": true, 5 | "module": "ESNext", 6 | "moduleResolution": "node", 7 | "outDir": "es", 8 | "skipLibCheck": true, 9 | "strict": true, 10 | "target": "ESNext", 11 | "types": ["jest"] 12 | }, 13 | "exclude": ["modules/__tests__", "node_modules"], 14 | "include": ["modules"] 15 | } 16 | --------------------------------------------------------------------------------