├── .babelrc ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── LICENSE.md ├── README.md ├── cssapi-logo.png ├── docs └── images │ ├── rhythm-example.png │ ├── rhythm.png │ └── workflow.png ├── examples └── without-theme │ ├── .babelrc │ ├── index.html │ ├── package.json │ ├── src │ └── js │ │ ├── components │ │ └── App.js │ │ ├── index.js │ │ └── styles │ │ ├── api.js │ │ └── global.js │ ├── webpack.dev.config.js │ └── yarn.lock ├── jest.config.js ├── jsconfig.json ├── package.json ├── src ├── __tests__ │ ├── api │ │ ├── batching.js │ │ ├── configuration.js │ │ ├── declarations.js │ │ ├── expanders │ │ │ ├── axisExpander.js │ │ │ ├── directionsExpander.js │ │ │ └── minMaxExpander.js │ │ ├── extend.js │ │ ├── helpers.js │ │ ├── mq.js │ │ └── scope.js │ ├── breakpoints │ │ ├── breakpointProvider.js │ │ └── resolveBreakpoints.js │ ├── renderers │ │ └── renderDirectionProps.js │ ├── testHelpers │ │ ├── fixtures │ │ │ ├── breakpoints.js │ │ │ └── generic.js │ │ └── matchers │ │ │ └── customMatchers.js │ ├── themeHelpers │ │ ├── api.js │ │ ├── mixin.js │ │ └── mq.js │ ├── transformers │ │ ├── percentageStringToRatioTransformer.js │ │ └── ratioToPercentageStringTransformer.js │ └── utils │ │ ├── insertSubIntoProp.js │ │ ├── isValidModifiedMq.js │ │ ├── parseBreakpoint.js │ │ ├── replaceTokens.js │ │ └── transformAllPartsWith.js ├── breakpoints │ ├── breakpointProvider.js │ ├── descriptors │ │ ├── createAtQueryDecriptor.js │ │ ├── createGtQueryDescriptor.js │ │ ├── createLtQueryDescriptor.js │ │ ├── createRangedQueryDescriptor.js │ │ ├── createSingleQueryDescr.js │ │ └── createUnrangedQueryDescrptor.js │ └── resolveBreakpoints.js ├── build │ ├── createApi.js │ ├── declarations │ │ ├── createDeclarationProcessor.js │ │ ├── createDeclarationProcessors.js │ │ ├── processDeclarations.js │ │ └── renderers │ │ │ ├── renderBaseline.js │ │ │ ├── renderBatch.js │ │ │ ├── renderDeclarations.js │ │ │ ├── renderDirectionProps.js │ │ │ ├── renderDualFromOneProps.js │ │ │ ├── renderDualProps.js │ │ │ ├── renderQuery.js │ │ │ ├── renderQueryHeader.js │ │ │ └── renderSingleDeclaration.js │ ├── ensureBreakpointProvider.js │ ├── expansion │ │ ├── expandData.js │ │ ├── expandProperties.js │ │ ├── propertyExpanderMap.js │ │ └── propertyExpanders │ │ │ ├── axesExpander.js │ │ │ ├── cornerExpander.js │ │ │ ├── directionExpander.js │ │ │ ├── directionsExpander.js │ │ │ └── minMaxExpander.js │ └── mergeWithDefaultConfig.js ├── config │ └── defaultConfig.js ├── const │ ├── breakpointMapping.js │ ├── breakpoints.js │ ├── config.js │ ├── errors.js │ ├── expanders.js │ ├── queryDescriptor.js │ ├── rangeItem.js │ ├── regexp.js │ ├── scope.js │ ├── templates.js │ └── units.js ├── cssapi.js ├── errors.js ├── index.js ├── objects │ ├── breakpointDescriptor.js │ ├── breakpointMapping.js │ ├── config.js │ ├── queryDescriptor.js │ ├── rangeItem.js │ └── scope.js ├── resolvers │ ├── resolveKeyToObjectValue.js │ ├── resolveKeyToValue.js │ ├── resolveKeysToObjectValues.js │ └── resolveKeysToValues.js ├── themeHelpers │ ├── api.js │ ├── getApiFromProps.js │ ├── mixin.js │ └── mq.js ├── transformers │ ├── calcTransformer.js │ ├── composite │ │ ├── allPartsTransformer.js │ │ ├── baselineTransformer.js │ │ ├── partPositionTransformer.js │ │ └── transformPartsWith.js │ ├── dataLookupTransformer.js │ ├── factory │ │ └── dataLookupTransformers.js │ ├── gradientTransformer.js │ ├── lengthToEmsTransformer.js │ ├── lengthToRemsTransformer.js │ ├── lengthTransformers.js │ ├── percentageStringToRatioTransformer.js │ ├── ratioToPercentageStringTransformer.js │ ├── rhythmUnitsToEmsTransformer.js │ ├── rhythmUnitsToLengthTransformer.js │ ├── rhythmUnitsToRemsTransformer.js │ ├── rootPxToEmTransformer.js │ ├── transformTransformer.js │ ├── transformer.js │ ├── unitlessNumberToEmsTransformer.js │ ├── unitlessNumberToLengthTransformer.js │ ├── unitlessNumberToPxTransformer.js │ └── unitlessNumberToRemsTransformer.js └── utils │ ├── baseline.js │ ├── breakpointMap.js │ ├── breakpoints.js │ ├── converters.js │ ├── data.js │ ├── declarations.js │ ├── expanders.js │ ├── formatting.js │ ├── functions.js │ ├── list.js │ ├── logic.js │ ├── numbers.js │ ├── objects.js │ ├── parse.js │ ├── predicate.js │ ├── queryDescriptor.js │ ├── range.js │ ├── scope.js │ ├── templates.js │ └── transformers.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "transform-es2015-destructuring", 4 | "transform-object-rest-spread", 5 | "transform-es2017-object-entries" 6 | ], 7 | "env": { 8 | "test": { 9 | "presets": [["env"]] 10 | }, 11 | "development": { 12 | "presets": [["env"]] 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | lib 2 | dist -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [ 3 | 'eslint-config-airbnb-base', 4 | 'plugin:react/recommended', 5 | 'prettier', 6 | ], 7 | plugins: ['prettier'], 8 | env: { 9 | browser: true, 10 | jest: true, 11 | }, 12 | parserOptions: { 13 | sourceType: 'module', 14 | ecmaFeatures: { 15 | jsx: true, 16 | }, 17 | }, 18 | globals: { 19 | l: true, 20 | }, 21 | rules: { 22 | 'func-names': ['error', 'never'], 23 | 'no-param-reassign': 'off', 24 | 'import/no-extraneous-dependencies': 'off', 25 | 'no-confusing-arrow': 'off', 26 | quotes: [ 27 | 'error', 28 | 'backtick', 29 | { avoidEscape: true, allowTemplateLiterals: true }, 30 | ], 31 | 'jsx-quotes': ['error', 'prefer-double'], 32 | 'comma-dangle': ['error', 'always-multiline'], 33 | 'valid-jsdoc': ['error'], 34 | 'no-restricted-syntax': ['error', 'LabeledStatement', 'WithStatement'], 35 | 'import/extensions': ['off', 'never'], 36 | 'no-unused-expressions': ['error', { allowTaggedTemplates: true }], 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | coverage 4 | out 5 | dist 6 | lib -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | unsafe-perm = true -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /Users/pedr/Library/Application\ Support/Code/User/snippets/javascript.json -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true, 6 | "trailingComma": "es5", 7 | "overrides": [ 8 | { 9 | "files": ".prettierrc", 10 | "options": { "parser": "json" } 11 | }, 12 | { 13 | "files": ".babelrc", 14 | "options": { "parser": "json" } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: trusty 3 | node_js: 4 | - 6 5 | - 7 6 | - 8 7 | notifications: 8 | email: 9 | on_failure: change 10 | script: 11 | - node --version 12 | - yarn --version 13 | - yarn run lint 14 | - yarn run test:noWatch 15 | - codecov 16 | - yarn run build 17 | cache: 18 | yarn: true 19 | bundler: true 20 | directories: 21 | - node_modules -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Pedr Browne 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 | -------------------------------------------------------------------------------- /cssapi-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Undistraction/cssapi/8597637b134e3f9937ca35c724d7bc3f0e423e9c/cssapi-logo.png -------------------------------------------------------------------------------- /docs/images/rhythm-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Undistraction/cssapi/8597637b134e3f9937ca35c724d7bc3f0e423e9c/docs/images/rhythm-example.png -------------------------------------------------------------------------------- /docs/images/rhythm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Undistraction/cssapi/8597637b134e3f9937ca35c724d7bc3f0e423e9c/docs/images/rhythm.png -------------------------------------------------------------------------------- /docs/images/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Undistraction/cssapi/8597637b134e3f9937ca35c724d7bc3f0e423e9c/docs/images/workflow.png -------------------------------------------------------------------------------- /examples/without-theme/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["transform-es2015-destructuring", "transform-object-rest-spread"], 3 | "presets": ["env", "react"] 4 | } 5 | -------------------------------------------------------------------------------- /examples/without-theme/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | -------------------------------------------------------------------------------- /examples/without-theme/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cssapi-example", 3 | "version": "0.0.0", 4 | "main": "lib/index.js", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/Undistraction/webpack-template.git" 8 | }, 9 | "files": [ 10 | "dist", 11 | "src", 12 | "lib" 13 | ], 14 | "keywords": [], 15 | "author": "Pedr Browne", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/Undistraction/webpack-template/issues" 19 | }, 20 | "homepage": "https://github.com/Undistraction/webpack-template", 21 | "scripts": { 22 | "start": "webpack-dev-server --config ./webpack.dev.config.js" 23 | }, 24 | "devDependencies": { 25 | "babel-cli": "^6.26.0", 26 | "babel-core": "6.26.0", 27 | "babel-eslint": "^8.2.2", 28 | "babel-loader": "^7.1.4", 29 | "babel-plugin-external-helpers": "^6.22.0", 30 | "babel-plugin-transform-class-properties": "^6.24.1", 31 | "babel-plugin-transform-es2015-destructuring": "^6.23.0", 32 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 33 | "babel-preset-env": "^1.6.1", 34 | "babel-preset-react": "^6.24.1", 35 | "codecov": "^3.0.0", 36 | "css-loader": "^0.28.10", 37 | "cssbeautify": "^0.3.1", 38 | "eslint": "^4.11.0", 39 | "eslint-config-airbnb": "^16.1.0", 40 | "eslint-config-airbnb-base": "^12.1.0", 41 | "eslint-config-prettier": "^2.7.0", 42 | "eslint-plugin-class-property": "^1.1.0", 43 | "eslint-plugin-import": "^2.8.0", 44 | "eslint-plugin-jsx-a11y": "^6.0.2", 45 | "eslint-plugin-prettier": "^2.3.1", 46 | "eslint-plugin-react": "^7.7.0", 47 | "extract-text-webpack-plugin": "^4.0.0-beta.0", 48 | "html-webpack-plugin": "^3.0.6", 49 | "husky": "^0.15.0-rc.8", 50 | "prettier": "^1.8.2", 51 | "react-hot-loader": "^4.0.0", 52 | "style-loader": "^0.20.3", 53 | "stylelint": "^9.1.2", 54 | "stylelint-config-prettier": "^3.0.4", 55 | "stylelint-config-standard": "^18.2.0", 56 | "webpack": "^4.1.1", 57 | "webpack-cli": "^2.0.12", 58 | "webpack-dev-server": "^3.1.1" 59 | }, 60 | "dependencies": { 61 | "cssapi-baseline": "0.4.0", 62 | "cssapi-fonts": "0.3.1", 63 | "cssapi-mq": "2.1.0", 64 | "cssapi-scale": "0.0.3", 65 | "normalize.css": "^8.0.0", 66 | "react-dom": "^16.2.0", 67 | "styled-components": "^3.2.2", 68 | "styled-normalize": "^4.0.0" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/without-theme/src/js/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import styled from 'styled-components' 3 | 4 | import api from '../styles/api' 5 | 6 | const AppWrapper = styled.div` 7 | display: flex; 8 | flex-direction: column; 9 | height: 100%; 10 | ${api({ 11 | minWidth: 300, 12 | maxWidth: [`auto`, 1000, `initial`], 13 | padding: [0, `4ru`, 0], 14 | margin: [0, `0 auto`], 15 | })}; 16 | ` 17 | 18 | const AppHeader = styled.header` 19 | ${api({ 20 | backgroundImage: `headerFooter`, 21 | borderRadius: [0, `1ru`, 0], 22 | })}; 23 | ` 24 | 25 | const AppFooter = styled.footer` 26 | text-align: center; 27 | ${api({ 28 | padding: [`1ru`, `4ru`], 29 | backgroundImage: `headerFooter`, 30 | borderRadius: [0, `1ru`, 0], 31 | flexBasis: [`2ru`, `4ru`], 32 | })}; 33 | ` 34 | 35 | const AppTitle = styled.h1` 36 | ${api({ 37 | fontFamily: `title`, 38 | fontSize: [`large`, `large`], 39 | padding: [[`1ru`, `2ru`], [`2ru`, `4ru`]], 40 | })}; 41 | ` 42 | 43 | const AppBody = styled.div` 44 | ${api({ 45 | padding: [[`1ru`, `2ru`], [`2ru`, `4ru`]], 46 | display: `flex`, 47 | flexDirection: [`column`, `row`], 48 | flexGrow: 2, 49 | maxWidth: { 50 | large: 1200, 51 | }, 52 | margin: { 53 | large: `0 auto`, 54 | }, 55 | })}; 56 | ` 57 | 58 | const Primary = styled.main` 59 | ${api({ 60 | paddingRight: [0, `3ru`], 61 | })}; 62 | ` 63 | 64 | const Secondary = styled.aside` 65 | ${api({ 66 | flexBasis: [`auto`, `auto`, 200, 300], 67 | flexShrink: [`auto`, `auto`, 0], 68 | padding: `1ru`, 69 | backgroundImage: [`transparent`, `headerFooter`], 70 | })}; 71 | ` 72 | 73 | const PrimaryTitle = styled.h2` 74 | font-style: italic; 75 | ${api({ 76 | fontFamily: `title`, 77 | fontSize: [`large`, `large`], 78 | })}; 79 | ` 80 | 81 | const App = () => ( 82 | 83 | 84 | Example: Without Theme 85 | 86 | 87 | 88 | PrimaryTitle 89 |

90 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus 91 | quis erat malesuada, lobortis leo eu, faucibus ante. Sed tempor 92 | tincidunt lectus, ac scelerisque justo tincidunt lobortis. Phasellus 93 | metus elit, porta vel sodales eu, euismod a libero. Duis sagittis, 94 | massa et dignissim eleifend, elit lorem luctus ligula, ut mattis ex 95 | dui non lacus. Nulla facilisi. Etiam et eros consequat, malesuada 96 | mauris at, mollis purus. Donec venenatis, enim vitae commodo 97 | consequat, arcu erat mollis leo, eget dapibus ligula lacus ut augue. 98 |

99 |

100 | Morbi in fringilla ligula, eget congue diam. Ut gravida quam sit amet 101 | diam malesuada malesuada. Donec nibh odio, convallis at velit eu, 102 | lobortis rutrum massa. Fusce cursus, turpis nec mattis lacinia, nibh 103 | ligula mollis turpis, a aliquam mauris tellus eget arcu. Mauris ut 104 | quam ac neque luctus eleifend quis eu arcu. Nunc hendrerit eget leo ut 105 | faucibus. Aenean et quam vel arcu vulputate rutrum at ac turpis. 106 | Interdum et malesuada fames ac ante ipsum primis in faucibus. 107 |

108 |

109 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus 110 | quis erat malesuada, lobortis leo eu, faucibus ante. Sed tempor 111 | tincidunt lectus, ac scelerisque justo tincidunt lobortis. Phasellus 112 | metus elit, porta vel sodales eu, euismod a libero. Duis sagittis, 113 | massa et dignissim eleifend, elit lorem luctus ligula, ut mattis ex 114 | dui non lacus. Nulla facilisi. Etiam et eros consequat, malesuada 115 | mauris at, mollis purus. Donec venenatis, enim vitae commodo 116 | consequat, arcu erat mollis leo, eget dapibus ligula lacus ut augue. 117 |

118 |

119 | Morbi in fringilla ligula, eget congue diam. Ut gravida quam sit amet 120 | diam malesuada malesuada. Donec nibh odio, convallis at velit eu, 121 | lobortis rutrum massa. Fusce cursus, turpis nec mattis lacinia, nibh 122 | ligula mollis turpis, a aliquam mauris tellus eget arcu. Mauris ut 123 | quam ac neque luctus eleifend quis eu arcu. Nunc hendrerit eget leo ut 124 | faucibus. Aenean et quam vel arcu vulputate rutrum at ac turpis. 125 | Interdum et malesuada fames ac ante ipsum primis in faucibus. 126 |

127 |
128 | 129 |
    130 |
  • Alpha
  • 131 |
  • Bravo
  • 132 |
  • Charlie
  • 133 |
  • Delta
  • 134 |
  • Echo
  • 135 |
  • Foxtrot
  • 136 |
  • Gamma
  • 137 |
  • Hotel
  • 138 |
139 |
140 |
141 | CSSAPI 142 |
143 | ) 144 | 145 | export default App 146 | -------------------------------------------------------------------------------- /examples/without-theme/src/js/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom' 3 | import App from './components/App' 4 | import global from './styles/global' 5 | 6 | const MOUNT_NODE = document.getElementById(`root`) 7 | 8 | ReactDOM.render(, MOUNT_NODE) 9 | -------------------------------------------------------------------------------- /examples/without-theme/src/js/styles/api.js: -------------------------------------------------------------------------------- 1 | import configureCSSAPI from '../../../../../src/index' 2 | 3 | const config = { 4 | breakpoints: [[`medium`, 500], [`large`, 1100], [`xLarge`, 1300]], 5 | data: { 6 | color: { 7 | lightGrey: `#DDD`, 8 | black: `#0D0D0D`, 9 | midGrey: `#777`, 10 | background: `#{lightGrey}`, 11 | text: `#{black}`, 12 | headerFooter: `linear-gradient(45deg, #{midGrey} 0%, #{lightGrey} 100%)`, 13 | }, 14 | scale: { 15 | small: 10, 16 | medium: 14, 17 | large: 24, 18 | }, 19 | font: { 20 | default: `Montserrat, Helvetica, Sans-Serif`, 21 | title: `Raleway, Helvetica, Sans-Serif`, 22 | }, 23 | rhythm: 15, 24 | scopes: [ 25 | { 26 | resolve: [`medium`, `large`, `xLarge`], 27 | data: { 28 | rhythm: 20, 29 | scale: { 30 | small: 12, 31 | medium: 16, 32 | large: 32, 33 | }, 34 | }, 35 | }, 36 | ], 37 | }, 38 | } 39 | 40 | const cssAPI = configureCSSAPI(config) 41 | 42 | export default cssAPI 43 | -------------------------------------------------------------------------------- /examples/without-theme/src/js/styles/global.js: -------------------------------------------------------------------------------- 1 | import { injectGlobal } from 'styled-components' 2 | import styledNormalize from 'styled-normalize' 3 | import api from './api' 4 | 5 | injectGlobal` 6 | ${styledNormalize} 7 | 8 | html { 9 | box-sizing: border-box; 10 | margin: 0; 11 | padding: 0; 12 | } 13 | 14 | *, *:before, *:after { 15 | box-sizing: inherit; 16 | } 17 | 18 | body { 19 | margin: 0; 20 | padding: 0; 21 | font-family: Helvetica, Arial, Sans-Serif; 22 | ${api({ 23 | backgroundColor: [`background`], 24 | fontFamily: `default`, 25 | lineHeight: [`2ru`, `2ru`], 26 | fontSize: [`medium`, `medium`], 27 | })} 28 | } 29 | 30 | h1, 31 | h2, 32 | h3 { 33 | font-weight: normal; 34 | margin: 0; 35 | padding: 0; 36 | } 37 | 38 | p { 39 | ${api({ 40 | margin: `2ru 0`, 41 | })} 42 | } 43 | 44 | ul, 45 | ol { 46 | padding: 0, 47 | margin: 0; 48 | } 49 | html, 50 | body, 51 | #root { 52 | min-height: 100%; 53 | } 54 | ` 55 | -------------------------------------------------------------------------------- /examples/without-theme/webpack.dev.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require(`webpack`); 2 | const HtmlWebpackPlugin = require(`html-webpack-plugin`); 3 | 4 | module.exports = { 5 | mode: `development`, 6 | entry: [`react-hot-loader/patch`, `./src/js/index.js`], 7 | module: { 8 | rules: [ 9 | { 10 | test: /\.(js|jsx)$/, 11 | exclude: /node_modules/, 12 | use: [`babel-loader`], 13 | }, 14 | { 15 | test: /\.css$/, 16 | use: [{ loader: `style-loader` }, { loader: `css-loader` }], 17 | }, 18 | ], 19 | }, 20 | resolve: { 21 | extensions: [`*`, `.js`, `.jsx`], 22 | }, 23 | output: { 24 | path: `${__dirname}/dist`, 25 | publicPath: `/`, 26 | filename: `bundle.js`, 27 | }, 28 | plugins: [ 29 | new webpack.HotModuleReplacementPlugin(), // Adds HMR 30 | new webpack.NamedModulesPlugin(), // Display module path in HMR updates 31 | new HtmlWebpackPlugin({ 32 | title: `CSSAPI: WithoutTheme`, 33 | template: `index.html`, 34 | }), // HTML template generation 35 | ], 36 | devServer: { 37 | contentBase: `./dev`, // Serve from dir 38 | hot: true, // Enable HMR 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | bail: true, 3 | verbose: true, 4 | collectCoverage: true, 5 | collectCoverageFrom: [`src/**/*.js`], 6 | coveragePathIgnorePatterns: [`src/index.js`], 7 | coverageReporters: [`html`, `lcov`], 8 | setupTestFrameworkScriptFile: `/src/__tests__/testHelpers/matchers/customMatchers.js`, 9 | setupFiles: [], 10 | modulePathIgnorePatterns: [`testHelpers/`], 11 | } 12 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6" 4 | }, 5 | "include": ["src/**/*"], 6 | "resolve": { 7 | "modulesDirectories": ["src"] 8 | }, 9 | "exclude": ["node_modules/**/*", "dist/**/*", "coverage/**/*", "lib/**/*"] 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cssapi", 3 | "version": "0.9.7", 4 | "main": "lib/index.js", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/Undistraction/cssapi.git" 8 | }, 9 | "files": [ 10 | "dist", 11 | "src", 12 | "lib" 13 | ], 14 | "keywords": [ 15 | "styled-components", 16 | "css", 17 | "css-in-js" 18 | ], 19 | "author": "Pedr Browne", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/Undistraction/cssapi/issues" 23 | }, 24 | "homepage": "https://github.com/Undistraction/cssapi", 25 | "scripts": { 26 | "build": "npm run build:lib", 27 | "prebuild:lib": "rimraf lib/*", 28 | "prebuild": "npm run lint", 29 | "build:lib": "babel --out-dir lib --ignore \"__tests__\" src", 30 | "test": "jest --watch", 31 | "test:noWatch": "jest", 32 | "prepublishOnly": "npm run build", 33 | "publish:patch": "npm version patch && sudo npm publish", 34 | "publish:minor": "npm version minor && sudo npm publish", 35 | "publish:major": "npm version major && sudo npm publish", 36 | "lint": "eslint src", 37 | "audit:packages": "yarn outdated || true" 38 | }, 39 | "devDependencies": { 40 | "babel-cli": "^6.26.0", 41 | "babel-core": "6.26.0", 42 | "babel-plugin-external-helpers": "^6.22.0", 43 | "babel-plugin-transform-es2015-destructuring": "^6.23.0", 44 | "babel-plugin-transform-es2017-object-entries": "^0.0.4", 45 | "babel-plugin-transform-object-rest-spread": "^6.26.0", 46 | "babel-preset-env": "^1.6.1", 47 | "babel-preset-react": "^6.24.1", 48 | "codecov": "^3.0.0", 49 | "cssbeautify": "^0.3.1", 50 | "eslint": "^4.11.0", 51 | "eslint-config-airbnb": "^16.1.0", 52 | "eslint-config-airbnb-base": "^12.1.0", 53 | "eslint-config-prettier": "^2.7.0", 54 | "eslint-plugin-import": "^2.8.0", 55 | "eslint-plugin-jsx-a11y": "^6.0.2", 56 | "eslint-plugin-prettier": "^2.3.1", 57 | "eslint-plugin-react": "^7.5.1", 58 | "jasmine-expect": "^3.8.3", 59 | "jasmine-multiline-matchers": "^0.2.2", 60 | "jest": "^22.4.3", 61 | "prettier": "^1.8.2", 62 | "rollup": "^0.51.3", 63 | "rollup-plugin-babel": "^3.0.2", 64 | "rollup-plugin-commonjs": "^8.2.6", 65 | "rollup-plugin-node-resolve": "^3.0.0", 66 | "sinon": "^4.4.9" 67 | }, 68 | "dependencies": { 69 | "dasherize": "^2.0.0", 70 | "ramda": "^0.25.0", 71 | "ramda-adjunct": "^2.9.0", 72 | "ramda-log": "^0.0.3" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/__tests__/api/batching.js: -------------------------------------------------------------------------------- 1 | import cssapi from '../../index' 2 | import { scope } from '../../utils/scope' 3 | import { 4 | breakpoint1, 5 | breakpoint2, 6 | breakpoint3, 7 | key1, 8 | } from '../testHelpers/fixtures/generic' 9 | 10 | describe(`api()`, () => { 11 | const breakpointMap = [ 12 | [breakpoint1, `25em`], 13 | [breakpoint2, `50em`], 14 | [breakpoint3, `75em`], 15 | ] 16 | 17 | const cssApi = cssapi({ 18 | breakpoints: breakpointMap, 19 | data: { 20 | color: { 21 | red: `#FA0000`, 22 | green: `#00FA00`, 23 | blue: `#0000FA`, 24 | }, 25 | rhythm: 20, 26 | scopes: [ 27 | { 28 | resolve: [breakpoint1, breakpoint2], 29 | data: { 30 | rhythm: 24, 31 | }, 32 | }, 33 | { 34 | resolve: [breakpoint3], 35 | data: { 36 | rhythm: 28, 37 | }, 38 | }, 39 | ], 40 | }, 41 | }) 42 | 43 | it(`throws if prop name is not valid`, () => { 44 | expect(() => cssApi({ [key1]: `` })).toThrow( 45 | `[cssapi] api() API doesn't support a property named 'key1'` 46 | ) 47 | }) 48 | 49 | it(`batches mulitple api functions into media queries`, () => { 50 | const result = cssApi({ 51 | padding: `1rem`, 52 | margin: [`1ru`, `2ru`, `3ru`], 53 | borderWidth: [scope`1ru`], 54 | color: [`c:red`, `c:green`, `c:blue`], 55 | }) 56 | 57 | const expected = ` 58 | padding: 1rem; 59 | 60 | @media (max-width: 24.99em) { 61 | margin: 1.25rem; 62 | border-width: 1.25rem; 63 | color: #FA0000; 64 | } 65 | 66 | @media (min-width: 25em) and (max-width: 49.99em) { 67 | margin: 3rem; 68 | color: #00FA00; 69 | } 70 | 71 | @media (min-width: 50em) { 72 | margin: 4.5rem; 73 | color: #0000FA; 74 | } 75 | 76 | @media (min-width: 25em) and (max-width: 74.99em) { 77 | border-width: 1.5rem; 78 | } 79 | 80 | @media (min-width: 75em) { 81 | border-width: 1.75rem; 82 | }` 83 | 84 | expect(result).toEqualMultiline(expected) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /src/__tests__/api/configuration.js: -------------------------------------------------------------------------------- 1 | import cssapi from '../../index' 2 | import { 3 | breakpoint1, 4 | breakpoint2, 5 | breakpoint3, 6 | } from '../testHelpers/fixtures/generic' 7 | 8 | describe(`configuration`, () => { 9 | it(`supports length values for breakpoint map`, () => { 10 | const breakpoints = [ 11 | [breakpoint1, `25em`], 12 | [breakpoint2, `50em`], 13 | [breakpoint3, `75em`], 14 | ] 15 | 16 | expect(() => cssapi({ breakpoints })).not.toThrow() 17 | }) 18 | 19 | it(`supports unitless values for breakpoint map`, () => { 20 | const breakpoints = [ 21 | [breakpoint1, 400], 22 | [breakpoint2, 800], 23 | [breakpoint3, 1200], 24 | ] 25 | 26 | expect(() => cssapi({ breakpoints })).not.toThrow() 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /src/__tests__/api/expanders/axisExpander.js: -------------------------------------------------------------------------------- 1 | import directionExpander from '../../../build/expansion/propertyExpanders/directionExpander' 2 | import { key1, value1, value2 } from '../../testHelpers/fixtures/generic' 3 | 4 | describe(`directionExpander`, () => { 5 | it(`expands one property to five`, () => { 6 | const transformers = [value1, value2] 7 | const mainWrapper = () => {} 8 | const renderer = () => {} 9 | 10 | const style = { 11 | transformers, 12 | renderer, 13 | } 14 | 15 | const expected = { 16 | [key1]: { 17 | transformers: mainWrapper(transformers), 18 | renderer, 19 | }, 20 | [`${key1}Top`]: { transformers, renderer }, 21 | [`${key1}Right`]: { transformers, renderer }, 22 | [`${key1}Bottom`]: { transformers, renderer }, 23 | [`${key1}Left`]: { transformers, renderer }, 24 | } 25 | const result = directionExpander({ mainWrapper })(key1, style) 26 | 27 | expect(result).toEqual(expected) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /src/__tests__/api/expanders/directionsExpander.js: -------------------------------------------------------------------------------- 1 | import directionsExpander from '../../../build/expansion/propertyExpanders/directionsExpander' 2 | import { key1, value1, value2 } from '../../testHelpers/fixtures/generic' 3 | 4 | describe(`directionsExpander`, () => { 5 | it(`expands to directions`, () => { 6 | const transformers = [value1, value2] 7 | const mainWrapper = () => {} 8 | const renderer = () => {} 9 | 10 | const style = { 11 | transformers, 12 | renderer, 13 | } 14 | 15 | const expected = { 16 | [`top`]: { transformers, renderer }, 17 | [`right`]: { transformers, renderer }, 18 | [`bottom`]: { transformers, renderer }, 19 | [`left`]: { transformers, renderer }, 20 | } 21 | const result = directionsExpander({ mainWrapper })(key1, style) 22 | 23 | expect(result).toEqual(expected) 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /src/__tests__/api/expanders/minMaxExpander.js: -------------------------------------------------------------------------------- 1 | import minMaxExpander from '../../../build/expansion/propertyExpanders/minMaxExpander' 2 | import { key1, value1, value2 } from '../../testHelpers/fixtures/generic' 3 | 4 | describe(`minMaxExpander`, () => { 5 | it(`expands one property to three`, () => { 6 | const transformers = () => [value1, value2] 7 | const renderer = () => {} 8 | 9 | const style = { 10 | transformers, 11 | renderer, 12 | } 13 | 14 | const expected = { 15 | [key1]: { 16 | transformers, 17 | renderer, 18 | }, 19 | [`minKey1`]: { transformers, renderer }, 20 | [`maxKey1`]: { transformers, renderer }, 21 | } 22 | const result = minMaxExpander()(key1, style) 23 | 24 | expect(result).toEqual(expected) 25 | }) 26 | }) 27 | -------------------------------------------------------------------------------- /src/__tests__/api/extend.js: -------------------------------------------------------------------------------- 1 | import cssapi from '../../index' 2 | import { 3 | breakpoint1, 4 | breakpoint2, 5 | breakpoint3, 6 | } from '../testHelpers/fixtures/generic' 7 | 8 | describe(`extend()`, () => { 9 | it(`creates a new api based on old`, () => { 10 | const firstBreakpointMap = [ 11 | [breakpoint1, `25em`], 12 | [breakpoint2, `50em`], 13 | [breakpoint3, `75em`], 14 | ] 15 | 16 | const extendedBreakpointMap = [ 17 | [breakpoint1, `12.5em`], 18 | [breakpoint2, `25em`], 19 | [breakpoint3, `37.5em`], 20 | ] 21 | 22 | const cssApi = cssapi({ 23 | breakpoints: firstBreakpointMap, 24 | }) 25 | 26 | const extendedCssApi = cssApi.extend({ 27 | breakpoints: extendedBreakpointMap, 28 | data: { 29 | baseFontSize: 10, 30 | }, 31 | }) 32 | 33 | expect( 34 | cssApi.mq(breakpoint1)({ 35 | margin: `1ru`, 36 | padding: `2ru`, 37 | }) 38 | ).toEqualMultiline(` 39 | @media (min-width: 25em) { 40 | margin: 1.25rem; 41 | padding: 2.5rem; 42 | } 43 | `) 44 | 45 | expect( 46 | extendedCssApi.mq(breakpoint1)({ 47 | margin: `1ru`, 48 | padding: `2ru`, 49 | }) 50 | ).toEqualMultiline(` 51 | @media (min-width: 12.5em) { 52 | margin: 2rem; 53 | padding: 4rem; 54 | } 55 | `) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /src/__tests__/api/helpers.js: -------------------------------------------------------------------------------- 1 | import cssapi from '../..' 2 | import { key1, key2, key3, value1 } from '../testHelpers/fixtures/generic' 3 | 4 | describe(`helpers`, () => { 5 | const breakpointMap = [[key1, `25em`], [key2, `50em`], [key3, `75em`]] 6 | const scaleData = { 7 | scale: { 8 | small: 12, 9 | medium: 16, 10 | large: 22, 11 | }, 12 | } 13 | const colorData = { 14 | color: { 15 | [key1]: value1, 16 | }, 17 | } 18 | 19 | describe(`box helpers`, () => { 20 | const api = cssapi({ 21 | breakpoints: breakpointMap, 22 | data: { 23 | ...colorData, 24 | }, 25 | }) 26 | 27 | describe(`padding-h`, () => { 28 | describe(`with one arguments`, () => { 29 | it(`returns left and right padding`, () => { 30 | expect(api({ paddingH: [`10px`, `15px`, `20px`] })).toEqualMultiline(` 31 | @media (max-width: 24.99em) { 32 | padding-left: 10px; 33 | padding-right: 10px; 34 | } 35 | 36 | @media (min-width: 25em) and (max-width: 49.99em) { 37 | padding-left: 15px; 38 | padding-right: 15px; 39 | } 40 | 41 | @media (min-width: 50em) { 42 | padding-left: 20px; 43 | padding-right: 20px; 44 | } 45 | `) 46 | }) 47 | }) 48 | 49 | describe(`with two arguments`, () => { 50 | it(`returns left and right padding`, () => { 51 | expect(api({ paddingH: [`10px 20px`, `15px 30px`, `20px 40px`] })) 52 | .toEqualMultiline(` 53 | @media (max-width: 24.99em) { 54 | padding-left: 10px; 55 | padding-right: 20px; 56 | } 57 | 58 | @media (min-width: 25em) and (max-width: 49.99em) { 59 | padding-left: 15px; 60 | padding-right: 30px; 61 | } 62 | 63 | @media (min-width: 50em) { 64 | padding-left: 20px; 65 | padding-right: 40px; 66 | }`) 67 | }) 68 | }) 69 | }) 70 | 71 | describe(`padding-v`, () => { 72 | describe(`with one arguments`, () => { 73 | it(`returns top and bottom padding`, () => { 74 | expect(api({ paddingV: [`10px`, `15px`, `20px`] })).toEqualMultiline(` 75 | @media (max-width: 24.99em) { 76 | padding-top: 10px; 77 | padding-bottom: 10px; 78 | } 79 | 80 | @media (min-width: 25em) and (max-width: 49.99em) { 81 | padding-top: 15px; 82 | padding-bottom: 15px; 83 | } 84 | 85 | @media (min-width: 50em) { 86 | padding-top: 20px; 87 | padding-bottom: 20px; 88 | } 89 | `) 90 | }) 91 | }) 92 | 93 | describe(`with two arguments`, () => { 94 | it(`returns top and bottom padding`, () => { 95 | expect(api({ paddingV: [`10px 20px`, `15px 30px`, `20px 40px`] })) 96 | .toEqualMultiline(` 97 | @media (max-width: 24.99em) { 98 | padding-top: 10px; 99 | padding-bottom: 20px; 100 | } 101 | 102 | @media (min-width: 25em) and (max-width: 49.99em) { 103 | padding-top: 15px; 104 | padding-bottom: 30px; 105 | } 106 | 107 | @media (min-width: 50em) { 108 | padding-top: 20px; 109 | padding-bottom: 40px; 110 | }`) 111 | }) 112 | }) 113 | }) 114 | 115 | describe(`border-v`, () => { 116 | it(`returns top and bottom borders`, () => { 117 | expect( 118 | api({ 119 | borderV: [`1ru solid c:key1`, `2ru solid black`, `3ru solid black`], 120 | }) 121 | ).toEqualMultiline(` 122 | @media (max-width: 24.99em) { 123 | border-top: 1.25rem solid value1; 124 | border-bottom: 1.25rem solid value1; 125 | } 126 | 127 | @media (min-width: 25em) and (max-width: 49.99em) { 128 | border-top: 2.5rem solid black; 129 | border-bottom: 2.5rem solid black; 130 | } 131 | 132 | @media (min-width: 50em) { 133 | border-top: 3.75rem solid black; 134 | border-bottom: 3.75rem solid black; 135 | } 136 | `) 137 | }) 138 | }) 139 | 140 | describe(`border-h`, () => { 141 | it(`returns left and right borders`, () => { 142 | expect( 143 | api({ 144 | borderH: [`1ru solid c:key1`, `2ru solid black`, `3ru solid black`], 145 | }) 146 | ).toEqualMultiline(` 147 | @media (max-width: 24.99em) { 148 | border-left: 1.25rem solid value1; 149 | border-right: 1.25rem solid value1; 150 | } 151 | 152 | @media (min-width: 25em) and (max-width: 49.99em) { 153 | border-left: 2.5rem solid black; 154 | border-right: 2.5rem solid black; 155 | } 156 | 157 | @media (min-width: 50em) { 158 | border-left: 3.75rem solid black; 159 | border-right: 3.75rem solid black; 160 | } 161 | `) 162 | }) 163 | }) 164 | }) 165 | 166 | describe(`offset`, () => { 167 | const api = cssapi({ breakpoints: breakpointMap }) 168 | 169 | it(`renders a single value`, () => { 170 | const result = api({ offset: `10px` }) 171 | expect(result).toEqualMultiline(` 172 | top: 10px; 173 | right: 10px; 174 | bottom: 10px; 175 | left: 10px; 176 | `) 177 | }) 178 | 179 | it(`renders two values`, () => { 180 | const result = api({ offset: `10px 20px` }) 181 | expect(result).toEqualMultiline(` 182 | top: 10px; 183 | right: 20px; 184 | bottom: 10px; 185 | left: 20px; 186 | `) 187 | }) 188 | 189 | it(`renders three values`, () => { 190 | const result = api({ 191 | offset: `10px 20px 5px`, 192 | }) 193 | expect(result).toEqualMultiline(` 194 | top: 10px; 195 | right: 20px; 196 | bottom: 5px; 197 | left: 20px; 198 | `) 199 | }) 200 | 201 | it(`renders four values`, () => { 202 | const result = api({ 203 | offset: `10px 20px 5px 2px`, 204 | }) 205 | expect(result).toEqualMultiline(` 206 | top: 10px; 207 | right: 20px; 208 | bottom: 5px; 209 | left: 2px; 210 | `) 211 | }) 212 | }) 213 | 214 | describe(`offsetV`, () => { 215 | const api = cssapi({ breakpoints: breakpointMap }) 216 | 217 | it(`renders a single value`, () => { 218 | const result = api({ 219 | offsetV: `10px`, 220 | }) 221 | expect(result).toEqualMultiline(` 222 | top: 10px; 223 | bottom: 10px; 224 | `) 225 | }) 226 | 227 | it(`renders two values`, () => { 228 | const result = api({ offsetV: `10px 20px` }) 229 | expect(result).toEqualMultiline(` 230 | top: 10px; 231 | bottom: 20px; 232 | `) 233 | }) 234 | }) 235 | 236 | describe(`offsetH`, () => { 237 | const api = cssapi({ breakpoints: breakpointMap }) 238 | 239 | it(`renders a single value`, () => { 240 | const result = api({ offsetH: `10px` }) 241 | expect(result).toEqualMultiline(` 242 | left: 10px; 243 | right: 10px; 244 | `) 245 | }) 246 | 247 | it(`renders two values`, () => { 248 | const result = api({ offsetH: `10px 20px` }) 249 | expect(result).toEqualMultiline(` 250 | left: 10px; 251 | right: 20px; 252 | `) 253 | }) 254 | }) 255 | 256 | describe(`baseline`, () => { 257 | describe(`half lines`, () => { 258 | const api = cssapi({ 259 | breakpoints: breakpointMap, 260 | data: { 261 | ...scaleData, 262 | }, 263 | }) 264 | 265 | describe(`with explicit font-size`, () => { 266 | describe(`with explicit lines`, () => { 267 | const result = api({ baseline: `16px 1` }) 268 | expect(result).toEqualMultiline(` 269 | font-size: 16px; 270 | line-height: 1.25rem; 271 | `) 272 | }) 273 | 274 | describe(`with auto lines`, () => { 275 | expect(api({ baseline: `16px` })).toEqualMultiline(` 276 | font-size: 16px; 277 | line-height: 1.25rem; 278 | `) // 1 lines 279 | 280 | expect(api({ baseline: `20px` })).toEqualMultiline(` 281 | font-size: 20px; 282 | line-height: 1.875rem; 283 | `) // 1.5 lines 284 | 285 | expect(api({ baseline: `21px` })).toEqualMultiline(` 286 | font-size: 21px; 287 | line-height: 1.875rem; 288 | `) // 1.5 lines 289 | }) 290 | }) 291 | 292 | describe(`with unitless font-size`, () => { 293 | describe(`with explicit lines`, () => { 294 | const result = api({ baseline: `16 1` }) 295 | expect(result).toEqualMultiline(` 296 | font-size: 1rem; 297 | line-height: 1.25rem; 298 | `) 299 | }) 300 | }) 301 | 302 | describe(`with rhythm unit font-size`, () => { 303 | describe(`with explicit lines`, () => { 304 | const result = api({ baseline: `1ru` }) 305 | expect(result).toEqualMultiline(` 306 | font-size: 1.25rem; 307 | line-height: 1.875rem; 308 | `) 309 | }) 310 | }) 311 | 312 | describe(`with font name`, () => { 313 | describe(`with explicit lines`, () => { 314 | const result = api({ baseline: `s:large 1` }) 315 | expect(result).toEqualMultiline(` 316 | font-size: 1.375rem; 317 | line-height: 1.25rem; 318 | `) 319 | }) 320 | }) 321 | }) 322 | 323 | describe(`whole lines`, () => { 324 | const api = cssapi({ 325 | breakpoints: breakpointMap, 326 | data: { 327 | ...scaleData, 328 | baseline: { 329 | allowHalfLines: false, 330 | }, 331 | }, 332 | }) 333 | 334 | expect(api({ baseline: `20px` })).toEqualMultiline(` 335 | font-size: 20px; 336 | line-height: 2.5rem; 337 | `) // 2 lines 338 | }) 339 | 340 | describe(`leading`, () => { 341 | const api = cssapi({ 342 | breakpoints: breakpointMap, 343 | data: { 344 | ...scaleData, 345 | baseline: { 346 | minLeading: 4, 347 | }, 348 | }, 349 | }) 350 | 351 | expect(api({ baseline: `17px` })).toEqualMultiline(` 352 | font-size: 17px; 353 | line-height: 1.875rem; 354 | `) // 1.5 lines 355 | 356 | expect(api({ baseline: `16px` })).toEqualMultiline(` 357 | font-size: 16px; 358 | line-height: 1.25rem; 359 | `) // 1 line 360 | }) 361 | 362 | describe(`lineHeight`, () => { 363 | const api = cssapi({ 364 | breakpoints: breakpointMap, 365 | data: { 366 | ...scaleData, 367 | baseline: { 368 | lineHeight: 24, 369 | }, 370 | }, 371 | }) 372 | 373 | expect(api({ baseline: `22px` })).toEqualMultiline(` 374 | font-size: 22px; 375 | line-height: 1.5rem; 376 | `) // 1 lines 377 | 378 | expect(api({ baseline: `23px` })).toEqualMultiline(` 379 | font-size: 23px; 380 | line-height: 2.25rem; 381 | `) // 1.5 lines 382 | }) 383 | }) 384 | }) 385 | -------------------------------------------------------------------------------- /src/__tests__/api/mq.js: -------------------------------------------------------------------------------- 1 | import cssapi from '../../index' 2 | import { 3 | breakpoint1, 4 | breakpoint2, 5 | breakpoint3, 6 | } from '../testHelpers/fixtures/generic' 7 | 8 | describe(`mq()`, () => { 9 | const breakpointMap = [ 10 | [breakpoint1, `25em`], 11 | [breakpoint2, `50em`], 12 | [breakpoint3, `75em`], 13 | ] 14 | 15 | const api = cssapi({ 16 | breakpoints: breakpointMap, 17 | }) 18 | 19 | it(`doesn't wrap default breakpoint`, () => { 20 | expect( 21 | api.mq(`default`)({ 22 | margin: `1ru`, 23 | padding: `2ru`, 24 | }) 25 | ).toEqualMultiline(` 26 | margin: 1.25rem; 27 | padding: 2.5rem; 28 | `) 29 | }) 30 | 31 | it(`wraps middle breakpoint`, () => { 32 | expect( 33 | api.mq(breakpoint1)({ 34 | margin: `1ru`, 35 | padding: `2ru`, 36 | }) 37 | ).toEqualMultiline(` 38 | @media (min-width: 25em) { 39 | margin: 1.25rem; 40 | padding: 2.5rem; 41 | } 42 | `) 43 | }) 44 | 45 | describe(`with modifiers and offsets`, () => { 46 | describe(`with <`, () => { 47 | it(`renders`, () => { 48 | expect( 49 | api.mq(``, () => { 62 | it(`renders`, () => { 63 | expect( 64 | api.mq(`>breakpoint1`)({ 65 | margin: `1ru`, 66 | padding: `2ru`, 67 | }) 68 | ).toEqualMultiline(` 69 | @media (min-width: 25em) { 70 | margin: 1.25rem; 71 | padding: 2.5rem; 72 | } 73 | `) 74 | }) 75 | }) 76 | 77 | describe(`with @`, () => { 78 | it(`renders`, () => { 79 | expect( 80 | api.mq(`@breakpoint1`)({ 81 | margin: `1ru`, 82 | padding: `2ru`, 83 | }) 84 | ).toEqualMultiline(` 85 | @media (min-width: 25em) and (max-width: 49.99em) { 86 | margin: 1.25rem; 87 | padding: 2.5rem; 88 | } 89 | `) 90 | }) 91 | }) 92 | 93 | describe(`with range and offsets`, () => { 94 | it(`renders`, () => { 95 | expect( 96 | api.mq(`>breakpoint1+50 { 111 | expect(() => 112 | api.mq(breakpoint2)({ 113 | margin: [10, 20], 114 | }) 115 | ).toThrow( 116 | `[cssapi] api.mq() When using the mq() helper you must supply only a single decaration value but you supplied: [10,20]` 117 | ) 118 | }) 119 | 120 | it(`throws if an object is supplied for a declaration`, () => { 121 | expect(() => 122 | api.mq(breakpoint2)({ 123 | margin: { 124 | [breakpoint1]: 10, 125 | }, 126 | }) 127 | ).toThrow( 128 | `[cssapi] api.mq() When using the mq() helper you must supply only a single decaration value but you supplied: {"breakpoint1":10}` 129 | ) 130 | }) 131 | }) 132 | -------------------------------------------------------------------------------- /src/__tests__/api/scope.js: -------------------------------------------------------------------------------- 1 | import cssapi, { scope } from '../../index' 2 | import { 3 | breakpoint1, 4 | breakpoint2, 5 | breakpoint3, 6 | value1, 7 | value2, 8 | } from '../testHelpers/fixtures/generic' 9 | 10 | describe(`scope`, () => { 11 | const breakpointMap = [ 12 | [breakpoint1, 400], 13 | [breakpoint2, 800], 14 | [breakpoint3, 1200], 15 | ] 16 | 17 | const scopedRhythm = { 18 | rhythm: 20, 19 | scopes: [ 20 | { 21 | resolve: [breakpoint1, breakpoint2], 22 | data: { 23 | rhythm: 24, 24 | }, 25 | }, 26 | { 27 | resolve: [breakpoint3], 28 | data: { 29 | rhythm: 28, 30 | }, 31 | }, 32 | ], 33 | } 34 | 35 | const scopedBaseline = { 36 | scopes: [ 37 | { 38 | resolve: [breakpoint1, breakpoint2], 39 | data: { 40 | baseline: { 41 | lineHeight: 24, 42 | }, 43 | }, 44 | }, 45 | { 46 | resolve: [breakpoint3], 47 | data: { 48 | baseline: { 49 | lineHeight: 28, 50 | }, 51 | }, 52 | }, 53 | ], 54 | } 55 | 56 | const scopedScale = { 57 | scale: { 58 | '1': 16, 59 | small: 12, 60 | medium: `s:1`, 61 | large: 22, 62 | }, 63 | scopes: [ 64 | { 65 | resolve: [breakpoint1, breakpoint2], 66 | data: { 67 | scale: { 68 | '1': 22, 69 | small: 16, 70 | medium: `s:1`, 71 | large: 28, 72 | }, 73 | }, 74 | }, 75 | { 76 | resolve: [breakpoint3], 77 | data: { 78 | scale: { 79 | '1': 26, 80 | small: 18, 81 | medium: `s:1`, 82 | large: 32, 83 | }, 84 | }, 85 | }, 86 | ], 87 | } 88 | 89 | describe(`using separate values for breakpoints`, () => { 90 | describe(`with scoped value`, () => { 91 | const cssApi = cssapi({ 92 | breakpoints: breakpointMap, 93 | data: { 94 | ...scopedRhythm, 95 | }, 96 | }) 97 | 98 | describe(`default breakpoint`, () => { 99 | it(`resolves the default value`, () => { 100 | expect( 101 | cssApi({ 102 | padding: `2ru`, 103 | }) 104 | ).toEqual(`padding: 2.5rem;`) 105 | }) 106 | }) 107 | 108 | describe(`default and first breakpoints `, () => { 109 | it(`resolves to the scoped value`, () => { 110 | expect( 111 | cssApi({ 112 | padding: [`2ru`, `2ru`], 113 | }) 114 | ).toEqualMultiline(` 115 | @media (max-width: 24.99em) { 116 | padding: 2.5rem; 117 | } 118 | 119 | @media (min-width: 25em) { 120 | padding: 3rem; 121 | }`) 122 | }) 123 | }) 124 | 125 | describe(`default, first and second breakpoints`, () => { 126 | it(`resolves to the scoped value`, () => { 127 | expect(cssApi({ padding: [`2ru`, `2ru`, `2ru`] })).toEqualMultiline(` 128 | @media (max-width: 24.99em) { 129 | padding: 2.5rem; 130 | } 131 | 132 | @media (min-width: 25em) { 133 | padding: 3rem; 134 | }`) 135 | }) 136 | }) 137 | 138 | describe(`default, second, third and fourth breakpoints`, () => { 139 | it(`resolves to the scoped value`, () => { 140 | expect(cssApi({ padding: [`2ru`, `2ru`, `2ru`, `2ru`] })) 141 | .toEqualMultiline(` 142 | @media (max-width: 24.99em) { 143 | padding: 2.5rem; 144 | } 145 | 146 | @media (min-width: 25em) and (max-width: 74.99em) { 147 | padding: 3rem; 148 | } 149 | 150 | @media (min-width: 75em) { 151 | padding: 3.5rem; 152 | }`) 153 | }) 154 | }) 155 | }) 156 | 157 | describe(`with scoped object`, () => { 158 | const cssApi = cssapi({ 159 | breakpoints: breakpointMap, 160 | data: { 161 | ...scopedScale, 162 | }, 163 | }) 164 | 165 | describe(`default breakpoint`, () => { 166 | it(`resolves the default value`, () => { 167 | expect(cssApi({ fontSize: `s:medium` })).toEqual(`font-size: 1rem;`) 168 | }) 169 | }) 170 | 171 | describe(`default and second breakpoints`, () => { 172 | it(`resolves to the scoped value`, () => { 173 | expect(cssApi({ fontSize: [`s:medium`, `s:medium`] })) 174 | .toEqualMultiline(` 175 | @media (max-width: 24.99em) { 176 | font-size: 1rem; 177 | } 178 | 179 | @media (min-width: 25em) { 180 | font-size: 1.375rem; 181 | }`) 182 | }) 183 | }) 184 | 185 | describe(`default, second and third breakpoints`, () => { 186 | it(`resolves to the scoped value`, () => { 187 | expect(cssApi({ fontSize: [`s:medium`, `s:medium`, `s:medium`] })) 188 | .toEqualMultiline(` 189 | @media (max-width: 24.99em) { 190 | font-size: 1rem; 191 | } 192 | 193 | @media (min-width: 25em) { 194 | font-size: 1.375rem; 195 | }`) 196 | }) 197 | }) 198 | 199 | describe(`default, second, third and fourth breakpoints`, () => { 200 | it(`resolves to the scoped value`, () => { 201 | expect( 202 | cssApi({ 203 | fontSize: [`s:medium`, `s:medium`, `s:medium`, `s:medium`], 204 | }) 205 | ).toEqualMultiline(` 206 | @media (max-width: 24.99em) { 207 | font-size: 1rem; 208 | } 209 | 210 | @media (min-width: 25em) and (max-width: 74.99em) { 211 | font-size: 1.375rem; 212 | } 213 | 214 | @media (min-width: 75em) { 215 | font-size: 1.625rem; 216 | }`) 217 | }) 218 | }) 219 | }) 220 | }) 221 | 222 | describe(`scoped baseline`, () => { 223 | const cssApi = cssapi({ 224 | breakpoints: breakpointMap, 225 | data: { 226 | ...scopedBaseline, 227 | }, 228 | }) 229 | 230 | describe(`default breakpoint`, () => { 231 | it(`resolves the all values`, () => { 232 | expect(cssApi({ baseline: [`16`, `16`, `16`, `16`] })) 233 | .toEqualMultiline(` 234 | @media (max-width: 24.99em) { 235 | font-size: 1rem; 236 | line-height: 1.25rem; 237 | } 238 | 239 | @media (min-width: 25em) and (max-width: 74.99em) { 240 | font-size: 1rem; 241 | line-height: 1.5rem; 242 | } 243 | 244 | @media (min-width: 75em) { 245 | font-size: 1rem; 246 | line-height: 1.75rem; 247 | } 248 | `) 249 | }) 250 | }) 251 | }) 252 | 253 | describe(`scope helper`, () => { 254 | describe(`with scoped value`, () => { 255 | const cssApi = cssapi({ 256 | breakpoints: breakpointMap, 257 | data: { 258 | ...scopedRhythm, 259 | }, 260 | }) 261 | 262 | describe(`default, second, third and fourth breakpoints`, () => { 263 | it(`renders values for every breakpoint`, () => { 264 | expect(cssApi({ padding: scope`2ru` })).toEqualMultiline(` 265 | @media (max-width: 24.99em) { 266 | padding: 2.5rem; 267 | } 268 | 269 | @media (min-width: 25em) and (max-width: 74.99em) { 270 | padding: 3rem; 271 | } 272 | 273 | @media (min-width: 75em) { 274 | padding: 3.5rem; 275 | }`) 276 | }) 277 | }) 278 | }) 279 | 280 | describe(`with scoped object`, () => { 281 | const cssApi = cssapi({ 282 | breakpoints: breakpointMap, 283 | data: { 284 | ...scopedScale, 285 | }, 286 | }) 287 | 288 | describe(`default, second, third and fourth breakpoints`, () => { 289 | it(`resolves to the scoped value`, () => { 290 | expect(cssApi({ fontSize: scope`s:medium` })).toEqualMultiline(` 291 | @media (max-width: 24.99em) { 292 | font-size: 1rem; 293 | } 294 | 295 | @media (min-width: 25em) and (max-width: 74.99em) { 296 | font-size: 1.375rem; 297 | } 298 | 299 | @media (min-width: 75em) { 300 | font-size: 1.625rem; 301 | }`) 302 | }) 303 | }) 304 | }) 305 | 306 | it(`handles interpolated values`, () => { 307 | expect(scope`${value1}`).toEqual({ 308 | scope: `value1`, 309 | }) 310 | 311 | expect(scope` ${value1}`).toEqual({ 312 | scope: ` value1`, 313 | }) 314 | 315 | expect(scope`${value1} ${value2} value3`).toEqual({ 316 | scope: `value1 value2 value3`, 317 | }) 318 | }) 319 | }) 320 | }) 321 | -------------------------------------------------------------------------------- /src/__tests__/breakpoints/breakpointProvider.js: -------------------------------------------------------------------------------- 1 | import breakpointProvider from '../../breakpoints/breakpointProvider' 2 | import { 3 | key1, 4 | key2, 5 | key3, 6 | value1, 7 | value2, 8 | value3, 9 | } from '../testHelpers/fixtures/generic' 10 | 11 | describe(`breakpointProvider`, () => { 12 | describe(`with length values`, () => { 13 | const breakpointMap = [[key1, `25em`], [key2, `50em`], [key3, `75em`]] 14 | const configuredProvider = breakpointProvider(breakpointMap) 15 | describe(`byIndex()`, () => { 16 | it(`returns an array of name, value pairs`, () => { 17 | const expected = [ 18 | { 19 | name: key1, 20 | query: { from: `25em`, to: `50em` }, 21 | value: value1, 22 | }, 23 | { 24 | name: key2, 25 | query: { from: `50em`, to: `75em` }, 26 | value: value2, 27 | }, 28 | { name: key3, query: { from: `75em` }, value: value3 }, 29 | ] 30 | 31 | expect(configuredProvider.byIndex([value1, value2, value3])).toEqual( 32 | expected 33 | ) 34 | }) 35 | }) 36 | 37 | describe(`byName()`, () => { 38 | it(`returns an array of name, value pairs`, () => { 39 | const expected = [ 40 | { name: key1, query: { from: `25em` }, value: value1 }, 41 | { name: key2, query: { from: `50em` }, value: value2 }, 42 | { name: key3, query: { from: `75em` }, value: value3 }, 43 | ] 44 | expect( 45 | configuredProvider.byName({ 46 | [key1]: value1, 47 | [key2]: value2, 48 | [key3]: value3, 49 | }) 50 | ).toEqual(expected) 51 | }) 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /src/__tests__/breakpoints/resolveBreakpoints.js: -------------------------------------------------------------------------------- 1 | import breakpointProvider from '../../breakpoints/breakpointProvider' 2 | import resolveBreakpoints from '../../breakpoints/resolveBreakpoints' 3 | import { key1, key2, key3, key4 } from '../testHelpers/fixtures/generic' 4 | 5 | const value1 = `500em` 6 | const value2 = `900em` 7 | const value3 = `1200em` 8 | 9 | describe(`resolveBreakpoints()`, () => { 10 | const breakpointMap = [[key1, value1], [key2, value2], [key3, value3]] 11 | const provider = breakpointProvider(breakpointMap) 12 | const resolver = resolveBreakpoints(provider) 13 | 14 | describe(`missing breakpoints`, () => { 15 | it(`throws when no breakpoint exists for index`, () => { 16 | expect(() => resolver(`a`, `b`, `c`, `d`)).toThrow( 17 | `[cssapi] (config.breakpoints) Couldn't resolve breakpoint at index 3 with args: ["a","b","c","d"]` 18 | ) 19 | }) 20 | 21 | it(`throws when no breakpoint exists for key`, () => { 22 | expect(() => 23 | resolver({ 24 | [key1]: `a`, 25 | [key2]: `b`, 26 | [key3]: `c`, 27 | [key4]: `d`, 28 | }) 29 | ).toThrow( 30 | `[cssapi] (config.breakpoints) Couldn't resolve breakpoint with name 'key4' with args: [{"key1":"a","key2":"b","key3":"c","key4":"d"}]` 31 | ) 32 | }) 33 | }) 34 | 35 | describe(`with separate args`, () => { 36 | it(`returns an array of breakpointName and value`, () => { 37 | expect(resolver(`a`, `b`, `c`)).toEqual([ 38 | { 39 | name: key1, 40 | query: { from: `500em`, to: `900em` }, 41 | value: `a`, 42 | }, 43 | { 44 | name: key2, 45 | query: { from: `900em`, to: `1200em` }, 46 | value: `b`, 47 | }, 48 | { 49 | name: key3, 50 | query: { from: `1200em` }, 51 | value: `c`, 52 | }, 53 | ]) 54 | }) 55 | }) 56 | 57 | describe(`with a map`, () => { 58 | it(`returns an array of breakpointName and value`, () => { 59 | expect( 60 | resolver({ 61 | [key1]: `a`, 62 | [key2]: `b`, 63 | [key3]: `c`, 64 | }) 65 | ).toEqual([ 66 | { 67 | name: key1, 68 | query: { from: `500em` }, 69 | value: `a`, 70 | }, 71 | { 72 | name: key2, 73 | query: { from: `900em` }, 74 | value: `b`, 75 | }, 76 | { 77 | name: key3, 78 | query: { from: `1200em` }, 79 | value: `c`, 80 | }, 81 | ]) 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /src/__tests__/renderers/renderDirectionProps.js: -------------------------------------------------------------------------------- 1 | import renderDirectionProps from '../../build/declarations/renderers/renderDirectionProps' 2 | 3 | describe(`renderDirectionProps()`, () => { 4 | const renderer = renderDirectionProps 5 | it(`renders a single`, () => { 6 | const result = renderer(null, [`10px`]) 7 | expect(result).toEqualMultiline(` 8 | top: 10px; 9 | right: 10px; 10 | bottom: 10px; 11 | left: 10px; 12 | `) 13 | }) 14 | 15 | it(`renders two values`, () => { 16 | const result = renderer(null, [`10px`, `20px`]) 17 | expect(result).toEqualMultiline(` 18 | top: 10px; 19 | right: 20px; 20 | bottom: 10px; 21 | left: 20px; 22 | `) 23 | }) 24 | 25 | it(`renders three values`, () => { 26 | const result = renderer(null, [`10px`, `20px`, `5px`]) 27 | expect(result).toEqualMultiline(` 28 | top: 10px; 29 | right: 20px; 30 | bottom: 5px; 31 | left: 20px; 32 | `) 33 | }) 34 | 35 | it(`renders four values`, () => { 36 | const result = renderer(null, [`10px`, `20px`, `5px`, `2px`]) 37 | expect(result).toEqualMultiline(` 38 | top: 10px; 39 | right: 20px; 40 | bottom: 5px; 41 | left: 2px; 42 | `) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /src/__tests__/testHelpers/fixtures/breakpoints.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Undistraction/cssapi/8597637b134e3f9937ca35c724d7bc3f0e423e9c/src/__tests__/testHelpers/fixtures/breakpoints.js -------------------------------------------------------------------------------- /src/__tests__/testHelpers/fixtures/generic.js: -------------------------------------------------------------------------------- 1 | export const value1 = `value1` 2 | export const value2 = `value2` 3 | export const value3 = `value3` 4 | export const value4 = `value4` 5 | export const invalidKeyValue = `invalidKeyValue` 6 | export const invalidKeyName = `invalidKeyName` 7 | export const key1 = `key1` 8 | export const key2 = `key2` 9 | export const key3 = `key3` 10 | export const key4 = `key4` 11 | export const key5 = `key5` 12 | export const key6 = `key6` 13 | export const breakpoint1 = `breakpoint1` 14 | export const breakpoint2 = `breakpoint2` 15 | export const breakpoint3 = `breakpoint3` 16 | export const breakpoint4 = `breakpoint4` 17 | export const func1 = () => {} 18 | -------------------------------------------------------------------------------- /src/__tests__/testHelpers/matchers/customMatchers.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-unused-vars 2 | import JasmineExpect from 'jasmine-expect' 3 | import { toEqualMultiline, toThrowMultiline } from 'jasmine-multiline-matchers' 4 | 5 | expect.extend({ 6 | toEqualMultiline, 7 | toThrowMultiline, 8 | }) 9 | -------------------------------------------------------------------------------- /src/__tests__/themeHelpers/api.js: -------------------------------------------------------------------------------- 1 | import configureCssApi, { api } from '../../index' 2 | 3 | describe(`theme helpers`, () => { 4 | describe(`api()`, () => { 5 | const cssApi = configureCssApi() 6 | 7 | describe(`with no theme`, () => { 8 | it(`throws`, () => { 9 | expect(() => api({})({})).toThrow( 10 | `[cssapi] api() There was no theme object available on the props object` 11 | ) 12 | }) 13 | }) 14 | 15 | describe(`with no api function`, () => { 16 | it(`throws`, () => { 17 | expect(() => api({})({ theme: {} })).toThrow( 18 | `[cssapi] api() There was no api function defined on the theme object. Value was: undefined` 19 | ) 20 | }) 21 | }) 22 | 23 | describe(`with api defined on theme`, () => { 24 | it(`resolves declaration object`, () => { 25 | expect( 26 | api({ 27 | padding: 16, 28 | })({ 29 | theme: { 30 | api: cssApi, 31 | }, 32 | }) 33 | ).toEqual(`padding: 1rem;`) 34 | }) 35 | }) 36 | }) 37 | }) 38 | -------------------------------------------------------------------------------- /src/__tests__/themeHelpers/mixin.js: -------------------------------------------------------------------------------- 1 | import configureCssApi, { mixin } from '../../index' 2 | import { value1, value2, value3 } from '../testHelpers/fixtures/generic' 3 | 4 | describe(`mixin`, () => { 5 | const cssApi = configureCssApi() 6 | 7 | const defaultMixinFunc = () => () => {} 8 | 9 | describe(`with no theme`, () => { 10 | it(`throws`, () => { 11 | const props = {} 12 | 13 | expect(() => mixin(defaultMixinFunc)()(props)).toThrow( 14 | `[cssapi] mixin() There was no theme object available on the props object` 15 | ) 16 | }) 17 | }) 18 | 19 | describe(`with no api function`, () => { 20 | it(`throws`, () => { 21 | const props = { theme: {} } 22 | expect(() => mixin(defaultMixinFunc)()(props)).toThrow( 23 | `[cssapi] mixin() There was no api function defined on the theme object. Value was: undefined` 24 | ) 25 | }) 26 | }) 27 | 28 | describe(`with mixin defined on theme`, () => { 29 | describe(`when mixin accepts args`, () => { 30 | const mock = jest.fn() 31 | 32 | const mixinFunc = (api, props) => (...args) => { 33 | mock(api, props, ...args) 34 | } 35 | 36 | const props = { 37 | theme: { 38 | api: cssApi, 39 | }, 40 | } 41 | 42 | it(`resolves declaration object`, () => { 43 | mixin(mixinFunc)(value1, value2, value3)(props) 44 | 45 | expect(mock).toBeCalledWith(cssApi, props, value1, value2, value3) 46 | }) 47 | }) 48 | 49 | describe(`when mixin doesn't accept args`, () => { 50 | const mock = jest.fn() 51 | 52 | const mixinFunc = (api, props) => () => { 53 | mock(api, props) 54 | } 55 | 56 | const props = { 57 | theme: { 58 | api: cssApi, 59 | }, 60 | } 61 | 62 | it(`resolves declaration object`, () => { 63 | mixin(mixinFunc)()(props) 64 | 65 | expect(mock).toBeCalledWith(cssApi, props) 66 | }) 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /src/__tests__/themeHelpers/mq.js: -------------------------------------------------------------------------------- 1 | import configureCssApi, { mq } from '../../index' 2 | import { breakpoint1 } from '../testHelpers/fixtures/generic' 3 | 4 | describe(`theme helpers`, () => { 5 | describe(`mq()`, () => { 6 | const cssApi = configureCssApi() 7 | 8 | describe(`with no theme`, () => { 9 | it(`throws`, () => { 10 | expect(() => mq(breakpoint1, {})({})).toThrow( 11 | `[cssapi] api() There was no theme object available on the props object` 12 | ) 13 | }) 14 | }) 15 | 16 | describe(`with no api function`, () => { 17 | it(`throws`, () => { 18 | expect(() => mq(breakpoint1, {})({ theme: {} })).toThrow( 19 | `[cssapi] api() There was no api function defined on the theme object. Value was: undefined` 20 | ) 21 | }) 22 | }) 23 | 24 | describe(`with api defined on theme`, () => { 25 | it(`resolves declaration object`, () => { 26 | expect( 27 | mq(`default`, { 28 | padding: 16, 29 | })({ 30 | theme: { 31 | api: cssApi, 32 | }, 33 | }) 34 | ).toEqual(`padding: 1rem;`) 35 | }) 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /src/__tests__/transformers/percentageStringToRatioTransformer.js: -------------------------------------------------------------------------------- 1 | import { map } from 'ramda' 2 | import { mapIndexed } from 'ramda-adjunct' 3 | import percentageStringToRatioTransformer from '../../transformers/percentageStringToRatioTransformer' 4 | 5 | describe(`percentageStringToRatioTransformer`, () => { 6 | it(`returns non-percentage values untouched`, () => { 7 | const values = [10, -10, 0, `10`, `10rem`, `auto`] 8 | map(value => { 9 | const result = percentageStringToRatioTransformer(value) 10 | expect(result).toEqual(value) 11 | })(values) 12 | }) 13 | 14 | it(`transforms percentage value`, () => { 15 | const values = [`50%`, `3%`, `400%`] 16 | const expectedValues = [0.5, 0.03, 4] 17 | mapIndexed((value, idx) => { 18 | const result = percentageStringToRatioTransformer(value) 19 | expect(result).toEqual(expectedValues[idx]) 20 | })(values) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/__tests__/transformers/ratioToPercentageStringTransformer.js: -------------------------------------------------------------------------------- 1 | import { map } from 'ramda' 2 | import { mapIndexed } from 'ramda-adjunct' 3 | import ratioToPercentageStringTransformer from '../../transformers/ratioToPercentageStringTransformer' 4 | 5 | describe(`ratioToPercentageStringTransformer`, () => { 6 | it(`returns non-fraction values untouched`, () => { 7 | const values = [0, `10`, `10rem`, `10%`, `auto`] 8 | map(value => { 9 | const result = ratioToPercentageStringTransformer(value) 10 | expect(result).toEqual(value) 11 | })(values) 12 | }) 13 | 14 | it(`transforms fraction value below 4`, () => { 15 | const values = [0.5, 1 / 4, 4] 16 | const expectedValues = [`50%`, `25%`, `400%`] 17 | mapIndexed((value, idx) => { 18 | const result = ratioToPercentageStringTransformer(value) 19 | expect(result).toEqual(expectedValues[idx]) 20 | })(values) 21 | }) 22 | }) 23 | -------------------------------------------------------------------------------- /src/__tests__/utils/insertSubIntoProp.js: -------------------------------------------------------------------------------- 1 | import { insertSubIntoProp } from '../../utils/formatting' 2 | import { value1 } from '../testHelpers/fixtures/generic' 3 | 4 | describe(`insertSubIntoProp`, () => { 5 | it(`inserts the sub into the prop`, () => { 6 | const prop = `alphaBravo` 7 | const expected = `alphaValue1Bravo` 8 | expect(insertSubIntoProp([prop, value1])).toEqual(expected) 9 | }) 10 | }) 11 | -------------------------------------------------------------------------------- /src/__tests__/utils/isValidModifiedMq.js: -------------------------------------------------------------------------------- 1 | import { map, test } from 'ramda' 2 | import { RE_MODIFIED_MQ } from '../../const/regexp' 3 | 4 | describe(`isValidModifiedMq`, () => { 5 | it(`returns true for valid values`, () => { 6 | const validValues = [ 7 | `alpha`, 8 | `alphaBravo`, 9 | `alphaBravoCharlie`, 10 | `>alpha`, 11 | `alpha+10`, 14 | `alpha+1.5`, 16 | `alpha-10`, 18 | `>alpha-10`, 19 | `>alpha-1.5`, 20 | `>alpha-1.5`, 21 | `>alpha+10em`, 22 | `alpha+1.5em`, 24 | `alpha-1.5em`, 26 | `>alpha-1.5em`, 27 | `alpha-50emalpha-50em { 32 | expect(test(RE_MODIFIED_MQ, value)).toBeTrue() 33 | }, validValues) 34 | }) 35 | 36 | it(`returns false for invalid values`, () => { 37 | const invalidValues = [ 38 | `<alpha`, 40 | `>>alpha`, 41 | `>bravo`, 45 | `alpha+10>bravo`, 46 | `alpha-50em { 56 | expect(test(RE_MODIFIED_MQ, value)).toBeFalse() 57 | }, invalidValues) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /src/__tests__/utils/parseBreakpoint.js: -------------------------------------------------------------------------------- 1 | import { map } from 'ramda' 2 | import { GT_MODIFIER, LT_MODIFIER, MODIFIERS } from '../../const/breakpoints' 3 | import { parseBreakpoint } from '../../utils/breakpoints' 4 | 5 | describe(`parseBreakpoint`, () => { 6 | const name1 = `alpha` 7 | const name2 = `bravo` 8 | 9 | it(`throws when syntax is invalid`, () => { 10 | const value = ` parseBreakpoint(value)).toThrow( 13 | `The syntax you used to describe your breakpoint range was invalid for ' { 18 | it(`parses a name`, () => { 19 | const value = name1 20 | expect(parseBreakpoint(value)).toEqual({ 21 | name: value, 22 | range: [{ name: value }], 23 | }) 24 | }) 25 | 26 | map(modifier => { 27 | it(`parses a name with ${modifier} modifier`, () => { 28 | const value = `${modifier}${name1}` 29 | expect(parseBreakpoint(value)).toEqual({ 30 | name: value, 31 | range: [{ name: name1, modifier }], 32 | }) 33 | }) 34 | }, MODIFIERS) 35 | 36 | describe(`with offset modifiers`, () => { 37 | describe(`positive`, () => { 38 | describe(`with unitless value`, () => { 39 | it(`parses with an offset`, () => { 40 | const value = `${name1}+10` 41 | expect(parseBreakpoint(value)).toEqual({ 42 | name: value, 43 | range: [{ name: name1, offset: `10` }], 44 | }) 45 | }) 46 | }) 47 | 48 | describe(`with em value`, () => { 49 | it(`parses with an offset`, () => { 50 | const value = `${name1}+10em` 51 | expect(parseBreakpoint(value)).toEqual({ 52 | name: value, 53 | range: [{ name: name1, offset: `10em` }], 54 | }) 55 | }) 56 | }) 57 | }) 58 | 59 | describe(`negative`, () => { 60 | describe(`with unitless value`, () => { 61 | it(`parses with an offset`, () => { 62 | const value = `${name1}-10` 63 | expect(parseBreakpoint(value)).toEqual({ 64 | name: value, 65 | range: [{ name: name1, offset: `-10` }], 66 | }) 67 | }) 68 | }) 69 | 70 | describe(`with em value`, () => { 71 | it(`parses with an offset`, () => { 72 | const value = `${name1}-10em` 73 | expect(parseBreakpoint(value)).toEqual({ 74 | name: value, 75 | range: [{ name: name1, offset: `-10em` }], 76 | }) 77 | }) 78 | }) 79 | }) 80 | }) 81 | 82 | describe(`with modifier and offset modifier`, () => { 83 | describe(`with em value`, () => { 84 | it(`parses with an offset`, () => { 85 | const value = `${LT_MODIFIER}${name1}+10em` 86 | expect(parseBreakpoint(value)).toEqual({ 87 | name: value, 88 | range: [{ name: name1, modifier: LT_MODIFIER, offset: `10em` }], 89 | }) 90 | }) 91 | }) 92 | }) 93 | }) 94 | 95 | describe(`multiple values`, () => { 96 | it(`parses two names`, () => { 97 | const value = `${name1}${LT_MODIFIER}${name2}` 98 | expect(parseBreakpoint(value)).toEqual({ 99 | name: value, 100 | range: [{ name: name1 }, { name: name2 }], 101 | }) 102 | }) 103 | 104 | it(`with modified first name`, () => { 105 | const value = `>${name1}${LT_MODIFIER}${name2}` 106 | expect(parseBreakpoint(value)).toEqual({ 107 | name: value, 108 | range: [{ name: name1, modifier: GT_MODIFIER }, { name: name2 }], 109 | }) 110 | }) 111 | 112 | it(`with modified first name and both offset`, () => { 113 | const value = `>${name1}+15em${LT_MODIFIER}${name2}-40` 114 | expect(parseBreakpoint(value)).toEqual({ 115 | name: value, 116 | range: [ 117 | { name: name1, modifier: GT_MODIFIER, offset: `15em` }, 118 | { name: name2, offset: `-40` }, 119 | ], 120 | }) 121 | }) 122 | }) 123 | }) 124 | -------------------------------------------------------------------------------- /src/__tests__/utils/replaceTokens.js: -------------------------------------------------------------------------------- 1 | import { replaceTokens, replaceToken } from '../../utils/formatting' 2 | 3 | describe(`replaceToken`, () => { 4 | it(`replaces a single token`, () => { 5 | const template = `alpha #{bravo}` 6 | expect(replaceToken(template, `bravo`, `charlie`)).toEqual(`alpha charlie`) 7 | }) 8 | 9 | it(`replaces a multiple identical tokens`, () => { 10 | const template = `alpha #{bravo} #{bravo}` 11 | expect(replaceToken(template, `bravo`, `charlie`)).toEqual( 12 | `alpha charlie charlie` 13 | ) 14 | }) 15 | }) 16 | 17 | describe(`replaceTokens`, () => { 18 | describe(`with object`, () => { 19 | it(`replaces a single token`, () => { 20 | const template = `alpha #{bravo}` 21 | expect(replaceTokens(template, { bravo: `charlie` })).toEqual( 22 | `alpha charlie` 23 | ) 24 | }) 25 | 26 | it(`replaces a multiple identical tokens`, () => { 27 | const template = `alpha #{bravo} #{bravo}` 28 | expect(replaceTokens(template, { bravo: `charlie` })).toEqual( 29 | `alpha charlie charlie` 30 | ) 31 | }) 32 | 33 | it(`replaces a multiple different tokens`, () => { 34 | const template = `alpha #{bravo} #{delta}` 35 | expect( 36 | replaceTokens(template, { bravo: `charlie`, delta: `echo` }) 37 | ).toEqual(`alpha charlie echo`) 38 | }) 39 | 40 | it(`handles additional values`, () => { 41 | const template = `alpha #{bravo} #{delta}` 42 | expect( 43 | replaceTokens(template, { 44 | bravo: `charlie`, 45 | delta: `echo`, 46 | foxtrot: `gamma`, 47 | }) 48 | ).toEqual(`alpha charlie echo`) 49 | }) 50 | }) 51 | 52 | describe(`with array`, () => { 53 | it(`replaces all tokens`, () => { 54 | const template = `alpha #{1} #{2}` 55 | expect(replaceTokens(template, [`bravo`, `charlie`])).toEqual( 56 | `alpha bravo charlie` 57 | ) 58 | }) 59 | 60 | it(`handles addtional values`, () => { 61 | const template = `alpha #{1} #{2}` 62 | expect(replaceTokens(template, [`bravo`, `charlie`, `delta`])).toEqual( 63 | `alpha bravo charlie` 64 | ) 65 | }) 66 | }) 67 | }) 68 | -------------------------------------------------------------------------------- /src/__tests__/utils/transformAllPartsWith.js: -------------------------------------------------------------------------------- 1 | import allPartsTransformer from '../../transformers/composite/allPartsTransformer' 2 | 3 | describe(`allPartsTransformer()`, () => { 4 | describe(`with a single part`, () => { 5 | it(`applies the transfomer to that part`, () => { 6 | const value = `a` 7 | const transformer = jest.fn(() => `transformedValue`) 8 | const f = allPartsTransformer(transformer) 9 | const result = f(value) 10 | expect(result).toEqual([`transformedValue`]) 11 | expect(transformer).toHaveBeenCalledWith(value, undefined, undefined) 12 | }) 13 | }) 14 | 15 | describe(`with multiple parts`, () => { 16 | it(`applies the transfomer to all parts`, () => { 17 | const value = `a b c` 18 | const transformer = jest.fn(() => `transformedValue`) 19 | const f = allPartsTransformer(transformer) 20 | const result = f(value) 21 | expect(result).toEqual([ 22 | `transformedValue`, 23 | `transformedValue`, 24 | `transformedValue`, 25 | ]) 26 | expect(transformer.mock.calls).toEqual([ 27 | [`a`, undefined, undefined], 28 | [`b`, undefined, undefined], 29 | [`c`, undefined, undefined], 30 | ]) 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /src/breakpoints/breakpointProvider.js: -------------------------------------------------------------------------------- 1 | import { 2 | apply, 3 | converge, 4 | identity, 5 | insert, 6 | map, 7 | once, 8 | pipe, 9 | reduce, 10 | __, 11 | } from 'ramda' 12 | import { appendFlipped, mapIndexed, reduceIndexed } from 'ramda-adjunct' 13 | import { 14 | assocQuery, 15 | assocValue, 16 | createBreakpointMapping, 17 | propName, 18 | } from '../objects/breakpointMapping' 19 | import { 20 | findBreakpointByIndex, 21 | findBreakpointByName, 22 | } from '../utils/breakpointMap' 23 | import { breakpointValuesToEms, parseBreakpoint } from '../utils/breakpoints' 24 | import { defaultToObj } from '../utils/functions' 25 | import { numKeys } from '../utils/list' 26 | import { reduceObjIndexed } from '../utils/objects' 27 | import { 28 | calculateQueryDescriptor, 29 | calculateQueryDescriptorForRange, 30 | } from '../utils/queryDescriptor' 31 | 32 | // ----------------------------------------------------------------------------- 33 | // Process Range 34 | // ----------------------------------------------------------------------------- 35 | 36 | const addBreakpointValueToRange = breakpointMap => 37 | converge(assocValue, [ 38 | pipe(propName, findBreakpointByName(breakpointMap)), 39 | identity, 40 | ]) 41 | 42 | const addBreakpointValuesToRange = breakpointMap => 43 | map(addBreakpointValueToRange(breakpointMap)) 44 | 45 | const processRange = breakpointMap => 46 | pipe( 47 | addBreakpointValuesToRange(breakpointMap), 48 | calculateQueryDescriptorForRange(breakpointMap) 49 | ) 50 | 51 | const parseBreakpointToMap = (breakpoints, [name, value]) => 52 | pipe(parseBreakpoint, assocValue(value), appendFlipped(breakpoints))(name) 53 | 54 | const parseBreakpointsToMap = reduceObjIndexed(parseBreakpointToMap, []) 55 | 56 | const createMappingByName = breakpointMap => ( 57 | mappings, 58 | { name, range, value } 59 | ) => 60 | pipe( 61 | processRange(breakpointMap), 62 | createBreakpointMapping(name, value), 63 | appendFlipped(mappings) 64 | )(range) 65 | 66 | const createMappingsByName = breakpointMap => 67 | reduce(createMappingByName(breakpointMap), []) 68 | 69 | const createMappingByIndex = breakpointMap => (mappings, value, idx) => 70 | pipe( 71 | findBreakpointByIndex(breakpointMap), 72 | insert(1, value), 73 | apply(createBreakpointMapping), 74 | appendFlipped(mappings) 75 | )(idx) 76 | 77 | const createMappingsByIndex = breakpointMap => values => 78 | reduceIndexed(createMappingByIndex(breakpointMap), [], values) 79 | 80 | const addQueryDescriptor = mappings => (value, idx) => 81 | pipe(calculateQueryDescriptor, assocQuery(__, value))(idx, mappings) 82 | 83 | const addQueryDescriptors = mappings => 84 | mapIndexed(addQueryDescriptor(mappings), mappings) 85 | 86 | // ----------------------------------------------------------------------------- 87 | // API 88 | // ----------------------------------------------------------------------------- 89 | 90 | const createApi = breakpointMap => { 91 | // Resolve breakpoints for values declared using an object syntax 92 | const byName = pipe( 93 | parseBreakpointsToMap, 94 | createMappingsByName(breakpointMap) 95 | ) 96 | 97 | // Resolve breakpoints for values declared using an array syntax 98 | const byIndex = pipe( 99 | createMappingsByIndex(breakpointMap), 100 | addQueryDescriptors 101 | ) 102 | 103 | const count = once(() => numKeys(breakpointMap)) 104 | 105 | return { 106 | byIndex, 107 | byName, 108 | count, 109 | } 110 | } 111 | 112 | // ----------------------------------------------------------------------------- 113 | // Exports 114 | // ----------------------------------------------------------------------------- 115 | 116 | const breakpointProvider = pipe(defaultToObj, breakpointValuesToEms, createApi) 117 | 118 | export default breakpointProvider 119 | -------------------------------------------------------------------------------- /src/breakpoints/descriptors/createAtQueryDecriptor.js: -------------------------------------------------------------------------------- 1 | import { nth, pipe } from 'ramda' 2 | import createQueryDescriptor from '../../objects/queryDescriptor' 3 | import { 4 | findBreakpointIndex, 5 | findNextBreakpointByIndex, 6 | isNotLastBreakpoint, 7 | } from '../../utils/breakpointMap' 8 | 9 | const nextBreakpointByIndex = breakpointMap => 10 | pipe(findNextBreakpointByIndex(breakpointMap), nth(1)) 11 | 12 | const createAtQueryDescriptor = (breakpointMap, rangeItem, rangeItemValue) => { 13 | // We need to limit ourselves using a max of the next query if it exists. 14 | // Use our own index to check if there is a breakpoint after us 15 | const idx = findBreakpointIndex(breakpointMap, rangeItem.name) 16 | 17 | let nextBreakpointValue 18 | if (isNotLastBreakpoint(breakpointMap, idx)) { 19 | nextBreakpointValue = nextBreakpointByIndex(breakpointMap)(idx) 20 | } 21 | 22 | // Otherwise we are the last breakpoint so we don't need a max 23 | return createQueryDescriptor({ 24 | from: rangeItemValue, 25 | to: nextBreakpointValue, 26 | }) 27 | } 28 | 29 | export default createAtQueryDescriptor 30 | -------------------------------------------------------------------------------- /src/breakpoints/descriptors/createGtQueryDescriptor.js: -------------------------------------------------------------------------------- 1 | import createQueryDescriptor from '../../objects/queryDescriptor' 2 | 3 | const createGtQueryDescriptor = (breakpointMap, rangeItem, rangeItemValue) => 4 | createQueryDescriptor({ from: rangeItemValue }) 5 | 6 | export default createGtQueryDescriptor 7 | -------------------------------------------------------------------------------- /src/breakpoints/descriptors/createLtQueryDescriptor.js: -------------------------------------------------------------------------------- 1 | import createQueryDescriptor from '../../objects/queryDescriptor' 2 | 3 | const createLtQueryDescriptor = (breakpointMap, rangeItem, rangeItemValue) => 4 | createQueryDescriptor({ 5 | to: rangeItemValue, 6 | }) 7 | 8 | export default createLtQueryDescriptor 9 | -------------------------------------------------------------------------------- /src/breakpoints/descriptors/createRangedQueryDescriptor.js: -------------------------------------------------------------------------------- 1 | import { nth, pipe } from 'ramda' 2 | import createQueryDescriptor from '../../objects/queryDescriptor' 3 | import { applyOffsetToBreakpointValue } from '../../utils/range' 4 | 5 | const createRangedQueryDescriptor = ( 6 | _, 7 | firstRangeItem, 8 | firstItemValue, 9 | range 10 | ) => { 11 | const lastItemValue = pipe(nth(1), applyOffsetToBreakpointValue)(range) 12 | return createQueryDescriptor({ from: firstItemValue, to: lastItemValue }) 13 | } 14 | 15 | export default createRangedQueryDescriptor 16 | -------------------------------------------------------------------------------- /src/breakpoints/descriptors/createSingleQueryDescr.js: -------------------------------------------------------------------------------- 1 | import { cond, either, T } from 'ramda' 2 | import { hasNoModifier } from '../../objects/rangeItem' 3 | import { 4 | modifierIsGtModifier, 5 | modifierIsLtModifier, 6 | } from '../../utils/predicate' 7 | import createAtQueryDescriptor from './createAtQueryDecriptor' 8 | import renderGtQuery from './createGtQueryDescriptor' 9 | import renderLtQuery from './createLtQueryDescriptor' 10 | 11 | const createSingleQueryDescriptor = cond([ 12 | [either(hasNoModifier, modifierIsGtModifier), () => renderGtQuery], 13 | [modifierIsLtModifier, () => renderLtQuery], 14 | [T, () => createAtQueryDescriptor], 15 | ]) 16 | 17 | export default createSingleQueryDescriptor 18 | -------------------------------------------------------------------------------- /src/breakpoints/descriptors/createUnrangedQueryDescrptor.js: -------------------------------------------------------------------------------- 1 | import { cond, either, T } from 'ramda' 2 | import { hasNoModifier } from '../../objects/rangeItem' 3 | import { 4 | modifierIsGtModifier, 5 | modifierIsLtModifier, 6 | } from '../../utils/predicate' 7 | import createAtQueryDescriptor from './createAtQueryDecriptor' 8 | import renderGtQuery from './createGtQueryDescriptor' 9 | import renderLtQuery from './createLtQueryDescriptor' 10 | 11 | const createUnrangedQueryDescriptor = cond([ 12 | [either(hasNoModifier, modifierIsGtModifier), () => renderGtQuery], 13 | [modifierIsLtModifier, () => renderLtQuery], 14 | [T, () => createAtQueryDescriptor], 15 | ]) 16 | 17 | export default createUnrangedQueryDescriptor 18 | -------------------------------------------------------------------------------- /src/breakpoints/resolveBreakpoints.js: -------------------------------------------------------------------------------- 1 | import { always, compose, cond, head, pipe, T, times, tryCatch } from 'ramda' 2 | import { isPlainObject } from 'ramda-adjunct' 3 | import { invalidBreakpointError, throwBreakpointError } from '../errors' 4 | import { hasScope, propScope } from '../objects/scope' 5 | 6 | const argIsScopeObj = compose(hasScope, head) 7 | 8 | const argIsObj = compose(isPlainObject, head) 9 | 10 | const expandScope = provider => value => times(always(value), provider.count()) 11 | 12 | const resolveBreakpointsImpl = provider => breakpoint => 13 | cond([ 14 | [ 15 | argIsScopeObj, 16 | pipe( 17 | head, 18 | propScope, 19 | expandScope(provider), 20 | resolveBreakpointsImpl(provider) 21 | // run through and remove values that don't change. 22 | ), 23 | ], 24 | [argIsObj, compose(provider.byName, head)], 25 | [T, provider.byIndex], 26 | ])(breakpoint) 27 | 28 | const resolveBreakpoints = provider => (...args) => 29 | tryCatch(resolveBreakpointsImpl(provider), message => 30 | throwBreakpointError(invalidBreakpointError(message, args)) 31 | )(args) 32 | 33 | export default resolveBreakpoints 34 | -------------------------------------------------------------------------------- /src/build/createApi.js: -------------------------------------------------------------------------------- 1 | import { curry, mergeDeepRight, pipe } from 'ramda' 2 | import cssapi from '../index' 3 | import { 4 | addBreakpointToDeclarations, 5 | batchDeclarations, 6 | optimiseDeclarations, 7 | } from '../utils/declarations' 8 | import processDeclarations from './declarations/processDeclarations' 9 | import renderBatch from './declarations/renderers/renderBatch' 10 | 11 | const buildApiFunc = processors => 12 | pipe( 13 | processDeclarations(processors), 14 | optimiseDeclarations, 15 | batchDeclarations, 16 | renderBatch 17 | ) 18 | 19 | const buildMqFunc = api => { 20 | api.mq = curry((breakpoint, declarations) => 21 | pipe(addBreakpointToDeclarations, api)(breakpoint, declarations) 22 | ) 23 | return api 24 | } 25 | 26 | const buildExtendFunc = baseConfig => api => { 27 | api.extend = pipe(mergeDeepRight(baseConfig), cssapi) 28 | return api 29 | } 30 | 31 | const createApi = config => 32 | pipe(buildApiFunc, buildMqFunc, buildExtendFunc(config)) 33 | 34 | export default createApi 35 | -------------------------------------------------------------------------------- /src/build/declarations/createDeclarationProcessor.js: -------------------------------------------------------------------------------- 1 | import { identity, partial, pipe, reduce, __ } from 'ramda' 2 | import { appendFlipped, ensureArray, list } from 'ramda-adjunct' 3 | import { createBreakpointMapping } from '../../objects/breakpointMapping' 4 | import { transformDeclarationValue } from '../../utils/transformers' 5 | import renderSingleDeclaration from './renderers/renderSingleDeclaration' 6 | 7 | const renderDeclaration = (propName, renderer) => 8 | pipe(ensureArray, partial(renderer, [propName]), list) 9 | 10 | const processDeclaration = ( 11 | propName, 12 | data, 13 | { transformers = identity, renderer = renderSingleDeclaration }, 14 | { name, query, value } 15 | ) => 16 | pipe( 17 | transformDeclarationValue(transformers, name, data), 18 | renderDeclaration(propName, renderer), 19 | createBreakpointMapping(name, __, query) 20 | )(value) 21 | 22 | // Create a map of curried 'processDeclaration' functions for each property, 23 | // ready to render styles when they receive a value 24 | const reducer = (propName, data, style) => (declarations, breakpointMapping) => 25 | pipe(processDeclaration, appendFlipped(declarations))( 26 | propName, 27 | data, 28 | style, 29 | breakpointMapping 30 | ) 31 | 32 | const createDeclarationProcessor = (propName, data, style) => 33 | reduce(reducer(propName, data, style), []) 34 | 35 | export default createDeclarationProcessor 36 | -------------------------------------------------------------------------------- /src/build/declarations/createDeclarationProcessors.js: -------------------------------------------------------------------------------- 1 | import { assoc, pipe, __ } from 'ramda' 2 | import resolveBreakpoints from '../../breakpoints/resolveBreakpoints' 3 | import { reduceObjIndexed } from '../../utils/objects' 4 | import createDeclarationProcessor from './createDeclarationProcessor' 5 | 6 | const create = (breakpoints, name, data, style) => 7 | pipe( 8 | resolveBreakpoints(breakpoints), 9 | createDeclarationProcessor(name, data, style) 10 | ) 11 | 12 | const createDeclarationProcessorReducer = (breakpoints, data) => ( 13 | acc, 14 | [name, style] 15 | ) => pipe(create, assoc(name, __, acc))(breakpoints, name, data, style) 16 | 17 | const createDeclarationProcessors = ({ breakpoints, data, properties }) => 18 | reduceObjIndexed( 19 | createDeclarationProcessorReducer(breakpoints, data), 20 | {}, 21 | properties 22 | ) 23 | 24 | export default createDeclarationProcessors 25 | -------------------------------------------------------------------------------- /src/build/declarations/processDeclarations.js: -------------------------------------------------------------------------------- 1 | import { apply, isNil, pipe, prop, unnest, when } from 'ramda' 2 | import { appendFlipped, ensureArray } from 'ramda-adjunct' 3 | import { invalidPropertyError, throwAPIError } from '../../errors' 4 | import { reduceObjIndexed } from '../../utils/objects' 5 | 6 | const processDeclaration = processors => (acc, [name, args]) => { 7 | const processor = prop(name, processors) 8 | when(isNil, () => throwAPIError(invalidPropertyError(name)))(processor) 9 | return pipe(ensureArray, apply(processor), appendFlipped(acc))(args) 10 | } 11 | 12 | const processDeclarations = processors => 13 | pipe(reduceObjIndexed(processDeclaration(processors), []), unnest) 14 | 15 | export default processDeclarations 16 | -------------------------------------------------------------------------------- /src/build/declarations/renderers/renderBaseline.js: -------------------------------------------------------------------------------- 1 | import { pipe, zip } from 'ramda' 2 | import { joinWithNewline } from '../../../utils/formatting' 3 | import renderDeclarations from './renderDeclarations' 4 | 5 | const PROPS = [`fontSize`, `lineHeight`] 6 | 7 | const renderBaseline = (_, value) => 8 | pipe(zip(PROPS), renderDeclarations, joinWithNewline)(value) 9 | 10 | export default renderBaseline 11 | -------------------------------------------------------------------------------- /src/build/declarations/renderers/renderBatch.js: -------------------------------------------------------------------------------- 1 | import { compose, isEmpty, pipe, reduce, unless } from 'ramda' 2 | import { appendFlipped, compact } from 'ramda-adjunct' 3 | import { 4 | joinWithDoubleNewlines, 5 | joinWithNewline, 6 | } from '../../../utils/formatting' 7 | import renderQuery from './renderQuery' 8 | 9 | const writeToString = outputString => 10 | compose(joinWithDoubleNewlines, compact, appendFlipped([outputString])) 11 | 12 | const renderStyle = (outputString, { query, value }) => 13 | pipe( 14 | joinWithNewline, 15 | unless(() => isEmpty(query), renderQuery(query)), 16 | writeToString(outputString) 17 | )(value) 18 | 19 | const renderBatch = reduce(renderStyle, ``) 20 | 21 | export default renderBatch 22 | -------------------------------------------------------------------------------- /src/build/declarations/renderers/renderDeclarations.js: -------------------------------------------------------------------------------- 1 | import { apply, map } from 'ramda' 2 | import renderSingleDeclaration from './renderSingleDeclaration' 3 | 4 | const renderDeclarations = map(apply(renderSingleDeclaration)) 5 | 6 | export default renderDeclarations 7 | -------------------------------------------------------------------------------- /src/build/declarations/renderers/renderDirectionProps.js: -------------------------------------------------------------------------------- 1 | import { pipe, zip } from 'ramda' 2 | import { DIRECTIONS_LIST } from '../../../const/expanders' 3 | import { joinWithNewline } from '../../../utils/formatting' 4 | import renderDeclarations from './renderDeclarations' 5 | 6 | const renderDirectionProps = (name, value) => { 7 | const right = value[1] || value[0] 8 | const bottom = value[2] || value[0] 9 | const left = value[3] || value[1] || value[0] 10 | const directionValues = [value[0], right, bottom, left] 11 | 12 | return pipe(zip(DIRECTIONS_LIST), renderDeclarations, joinWithNewline)( 13 | directionValues 14 | ) 15 | } 16 | export default renderDirectionProps 17 | -------------------------------------------------------------------------------- /src/build/declarations/renderers/renderDualFromOneProps.js: -------------------------------------------------------------------------------- 1 | import { pipe, zip } from 'ramda' 2 | import { joinWithNewline } from '../../../utils/formatting' 3 | import renderDeclarations from './renderDeclarations' 4 | 5 | const renderDualProps = propNames => (_, value) => 6 | pipe(zip(propNames), renderDeclarations, joinWithNewline)([value, value]) 7 | 8 | export default renderDualProps 9 | -------------------------------------------------------------------------------- /src/build/declarations/renderers/renderDualProps.js: -------------------------------------------------------------------------------- 1 | import { pipe, zip } from 'ramda' 2 | import { joinWithNewline } from '../../../utils/formatting' 3 | import renderDeclarations from './renderDeclarations' 4 | 5 | const renderDualProps = propNames => (_, value) => { 6 | const firstProp = value[0] 7 | const lastProp = value[1] || firstProp 8 | const directionValues = [firstProp, lastProp] 9 | 10 | return pipe(zip(propNames), renderDeclarations, joinWithNewline)( 11 | directionValues 12 | ) 13 | } 14 | export default renderDualProps 15 | -------------------------------------------------------------------------------- /src/build/declarations/renderers/renderQuery.js: -------------------------------------------------------------------------------- 1 | import { indentLines } from '../../../utils/formatting' 2 | import { createQueryFromTemplate } from '../../../utils/templates' 3 | import renderQueryHeader from './renderQueryHeader' 4 | 5 | const renderQuery = query => value => 6 | createQueryFromTemplate({ 7 | query: renderQueryHeader(query), 8 | value: indentLines(value), 9 | }) 10 | 11 | export default renderQuery 12 | -------------------------------------------------------------------------------- /src/build/declarations/renderers/renderQueryHeader.js: -------------------------------------------------------------------------------- 1 | import { 2 | createQueryMaxHeaderFromTemplate, 3 | createQueryMinHeaderFromTemplate, 4 | createQueryMinMaxHeaderFromTemplate, 5 | } from '../../../utils/templates' 6 | 7 | const renderQueryHeader = ({ to, from }) => { 8 | if (from && to) { 9 | return createQueryMinMaxHeaderFromTemplate(to, from) 10 | } 11 | 12 | if (from) { 13 | return createQueryMinHeaderFromTemplate(from) 14 | } 15 | 16 | return createQueryMaxHeaderFromTemplate(to) 17 | } 18 | 19 | export default renderQueryHeader 20 | -------------------------------------------------------------------------------- /src/build/declarations/renderers/renderSingleDeclaration.js: -------------------------------------------------------------------------------- 1 | import { compose, evolve, pipe } from 'ramda' 2 | import { ensureArray } from 'ramda-adjunct' 3 | import { joinWithSpace, toKebabCase } from '../../../utils/formatting' 4 | import { createDeclarationFromTemplate } from '../../../utils/templates' 5 | 6 | const renderSingleDeclaration = (name, value) => 7 | pipe( 8 | evolve({ 9 | name: toKebabCase, 10 | value: compose(joinWithSpace, ensureArray), 11 | }), 12 | createDeclarationFromTemplate 13 | )({ name, value }) 14 | 15 | export default renderSingleDeclaration 16 | -------------------------------------------------------------------------------- /src/build/ensureBreakpointProvider.js: -------------------------------------------------------------------------------- 1 | import { compose, over } from 'ramda' 2 | import breakpointProvider from '../breakpoints/breakpointProvider' 3 | import { lBreakpoints } from '../objects/config' 4 | import { addDefaultBreakpoint } from '../utils/breakpoints' 5 | 6 | const configureBreakpointProvider = compose( 7 | breakpointProvider, 8 | addDefaultBreakpoint 9 | ) 10 | 11 | const ensureBreakpointProvider = over(lBreakpoints, configureBreakpointProvider) 12 | 13 | export default ensureBreakpointProvider 14 | -------------------------------------------------------------------------------- /src/build/expansion/expandData.js: -------------------------------------------------------------------------------- 1 | import { 2 | assoc, 3 | cond, 4 | identity, 5 | keys, 6 | lensProp, 7 | map, 8 | mergeDeepLeft, 9 | over, 10 | pipe, 11 | prop, 12 | T, 13 | when, 14 | without, 15 | __, 16 | } from 'ramda' 17 | import { concatRight, isNotUndefined, isString } from 'ramda-adjunct' 18 | import CONFIG_FIELD_NAMES from '../../const/config' 19 | import { 20 | missingDataItemKeyError, 21 | missingDataNodeError, 22 | throwDataError, 23 | unrecognisedDataPrefixError, 24 | } from '../../errors' 25 | import { lData, propScopes } from '../../objects/config' 26 | import { splitOnColon } from '../../utils/formatting' 27 | import { reduceWithKeys } from '../../utils/list' 28 | import { whenIsUndefined } from '../../utils/logic' 29 | import { 30 | hasUnnestedWhitespace, 31 | isCSSFunction, 32 | isToken, 33 | } from '../../utils/predicate' 34 | import { 35 | transformFunctionElements, 36 | transformGroup, 37 | } from '../../utils/transformers' 38 | 39 | const { SCOPES, ALIASES } = CONFIG_FIELD_NAMES 40 | 41 | const validPrefixes = (config, data) => 42 | pipe( 43 | keys, 44 | without([SCOPES, ALIASES]), 45 | concatRight(keys(config.data[ALIASES])) 46 | )(data) 47 | 48 | const expandData = config => { 49 | // Build Function to resolve aliases 50 | const resolveDataAlias = (name, data) => 51 | pipe( 52 | prop, 53 | whenIsUndefined(() => prop(name, config.data.aliases)), 54 | whenIsUndefined(() => { 55 | throwDataError( 56 | unrecognisedDataPrefixError(name, validPrefixes(config, data)) 57 | ) 58 | }) 59 | )(name, data) 60 | 61 | // --------------------------------------------------------------------------- 62 | // Value Expansion 63 | // --------------------------------------------------------------------------- 64 | 65 | const resolveName = (sourceData, name, key) => 66 | pipe( 67 | prop(name), 68 | whenIsUndefined(() => throwDataError(missingDataNodeError(name))), 69 | prop(key), 70 | whenIsUndefined(() => throwDataError(missingDataItemKeyError(name, key))) 71 | )(sourceData) 72 | 73 | const expandToken = sourceData => token => { 74 | const [prefix, key] = splitOnColon(token) 75 | const name = resolveDataAlias(prefix, sourceData) 76 | const value = resolveName(sourceData, name, key) 77 | // eslint-disable-next-line no-use-before-define 78 | return expandValue(sourceData)(value) 79 | } 80 | 81 | const expandValues = sourceData => 82 | // eslint-disable-next-line no-use-before-define 83 | map(expandValue(sourceData)) 84 | 85 | const expandValue = sourceData => v => 86 | cond([ 87 | [isToken, expandToken(sourceData)], 88 | [isCSSFunction, transformFunctionElements(expandValues(sourceData))], 89 | [hasUnnestedWhitespace, transformGroup(expandValue(sourceData))], 90 | [T, identity], 91 | ])(v) 92 | 93 | // --------------------------------------------------------------------------- 94 | // Node Expansion 95 | // --------------------------------------------------------------------------- 96 | 97 | const expandNode = (name, expandedRootData) => 98 | reduceWithKeys((node, key) => { 99 | const sourceData = over( 100 | lensProp(name), 101 | mergeDeepLeft(node), 102 | expandedRootData 103 | ) 104 | return over(lensProp(key), when(isString, expandValue(sourceData)))(node) 105 | }) 106 | 107 | const expandNodes = expandedRootData => 108 | reduceWithKeys((acc, name) => 109 | pipe(expandNode, over(lensProp(name), __, acc))(name, expandedRootData) 110 | ) 111 | 112 | // --------------------------------------------------------------------------- 113 | // Scopes 114 | // --------------------------------------------------------------------------- 115 | 116 | const expandScopes = expandedRootData => 117 | map(over(lData, expandNodes(expandedRootData))) 118 | 119 | // --------------------------------------------------------------------------- 120 | // Data 121 | // --------------------------------------------------------------------------- 122 | 123 | const expand = data => { 124 | // Expand the root node using itself as a data source 125 | const expandedRootData = expandNodes(data)(data) 126 | 127 | // Expand scoped nodes 128 | return pipe( 129 | propScopes, 130 | when(isNotUndefined, expandScopes(expandedRootData)), 131 | assoc(SCOPES, __, expandedRootData) 132 | )(data) 133 | } 134 | 135 | return over(lData, expand, config) 136 | } 137 | 138 | export default expandData 139 | -------------------------------------------------------------------------------- /src/build/expansion/expandProperties.js: -------------------------------------------------------------------------------- 1 | import { compose, mergeDeepRight, over, prop, __ } from 'ramda' 2 | import { lProperties } from '../../objects/config' 3 | import { reduceObjIndexed } from '../../utils/objects' 4 | import EXPANDER_MAP from './propertyExpanderMap' 5 | 6 | const expand = reduceObjIndexed( 7 | (properties, [propName, expander]) => 8 | compose(mergeDeepRight(properties), expander)( 9 | propName, 10 | prop(propName, properties) 11 | ), 12 | __, 13 | EXPANDER_MAP 14 | ) 15 | 16 | const expandProperties = over(lProperties, expand) 17 | 18 | export default expandProperties 19 | -------------------------------------------------------------------------------- /src/build/expansion/propertyExpanderMap.js: -------------------------------------------------------------------------------- 1 | import partPositionTransformer from '../../transformers/composite/partPositionTransformer' 2 | import transformPartsWith from '../../transformers/composite/transformPartsWith' 3 | import { wrapWithTransform } from '../../utils/expanders' 4 | import { insertSubIntoProp } from '../../utils/formatting' 5 | import axesExpander from './propertyExpanders/axesExpander' 6 | import cornerExpander from './propertyExpanders/cornerExpander' 7 | import directionExpander from './propertyExpanders/directionExpander' 8 | import directionsExpander from './propertyExpanders/directionsExpander' 9 | import minMaxExpander from './propertyExpanders/minMaxExpander' 10 | 11 | // Expanders take an item described in the config and expand it to multiple 12 | // separate properties. For example 'padding' is expanded to 'padding-top', 13 | // 'padding-right', 'padding-bottom' and 'padding-left', or 'directions' is 14 | // expanded to 'top', 'right', 'bottom' and 'left'. 15 | const EXPANDER_MAP = Object.freeze({ 16 | padding: directionExpander(), 17 | margin: directionExpander(), 18 | border: directionExpander({ 19 | mainWrapper: transformPartsWith, 20 | subWrapper: transformPartsWith, 21 | }), 22 | borderWidth: directionExpander({ 23 | createPropNameStrategy: insertSubIntoProp, 24 | }), 25 | borderStyle: directionExpander({ 26 | createPropNameStrategy: insertSubIntoProp, 27 | }), 28 | borderColor: directionExpander({ 29 | createPropNameStrategy: insertSubIntoProp, 30 | }), 31 | borderRadius: cornerExpander({ 32 | createPropNameStrategy: insertSubIntoProp, 33 | }), 34 | width: minMaxExpander(), 35 | height: minMaxExpander(), 36 | directions: directionsExpander(), 37 | overflow: axesExpander(), 38 | outlineColor: wrapWithTransform(transformPartsWith), 39 | outline: wrapWithTransform(transformPartsWith), 40 | flex: wrapWithTransform(partPositionTransformer(2)), 41 | background: wrapWithTransform(transformPartsWith), 42 | backgroundImage: wrapWithTransform(transformPartsWith), 43 | backgroundPosition: axesExpander({ 44 | mainWrapper: transformPartsWith, 45 | subWrapper: transformPartsWith, 46 | }), 47 | boxShadow: wrapWithTransform(transformPartsWith), 48 | transformOrigin: wrapWithTransform(transformPartsWith), 49 | offset: wrapWithTransform(transformPartsWith), 50 | offsetV: wrapWithTransform(transformPartsWith), 51 | offsetH: wrapWithTransform(transformPartsWith), 52 | paddingH: wrapWithTransform(transformPartsWith), 53 | paddingV: wrapWithTransform(transformPartsWith), 54 | marginH: wrapWithTransform(transformPartsWith), 55 | marginV: wrapWithTransform(transformPartsWith), 56 | borderH: wrapWithTransform(transformPartsWith), 57 | borderV: wrapWithTransform(transformPartsWith), 58 | }) 59 | 60 | export default EXPANDER_MAP 61 | -------------------------------------------------------------------------------- /src/build/expansion/propertyExpanders/axesExpander.js: -------------------------------------------------------------------------------- 1 | import { converge, identity, pipe } from 'ramda' 2 | import { AXES_LIST } from '../../../const/expanders' 3 | import { expandMainProp, expandSubProps } from '../../../utils/expanders' 4 | import { appendSubToProp } from '../../../utils/formatting' 5 | 6 | const axesExpander = ({ 7 | mainWrapper = identity, 8 | subWrapper = identity, 9 | } = {}) => (propName, style) => 10 | converge(pipe, [ 11 | expandMainProp, 12 | expandSubProps(appendSubToProp, AXES_LIST, subWrapper), 13 | ])(propName, style, mainWrapper)({}) 14 | 15 | export default axesExpander 16 | -------------------------------------------------------------------------------- /src/build/expansion/propertyExpanders/cornerExpander.js: -------------------------------------------------------------------------------- 1 | import { converge, identity, pipe } from 'ramda' 2 | import { CORNERS_LIST } from '../../../const/expanders' 3 | import allPartsTransformer from '../../../transformers/composite/allPartsTransformer' 4 | import { expandMainProp, expandSubProps } from '../../../utils/expanders' 5 | import { appendSubToProp } from '../../../utils/formatting' 6 | 7 | const cornerExpander = ({ 8 | mainWrapper = allPartsTransformer, 9 | subWrapper = identity, 10 | createPropNameStrategy = appendSubToProp, 11 | } = {}) => (propName, style) => 12 | converge(pipe, [ 13 | expandMainProp, 14 | expandSubProps(createPropNameStrategy, CORNERS_LIST, subWrapper), 15 | ])(propName, style, mainWrapper)({}) 16 | 17 | export default cornerExpander 18 | -------------------------------------------------------------------------------- /src/build/expansion/propertyExpanders/directionExpander.js: -------------------------------------------------------------------------------- 1 | import { converge, identity, pipe } from 'ramda' 2 | import { DIRECTIONS_LIST } from '../../../const/expanders' 3 | import allPartsTransformer from '../../../transformers/composite/allPartsTransformer' 4 | import { expandMainProp, expandSubProps } from '../../../utils/expanders' 5 | import { appendSubToProp } from '../../../utils/formatting' 6 | 7 | const directionExpander = ({ 8 | mainWrapper = allPartsTransformer, 9 | subWrapper = identity, 10 | createPropNameStrategy = appendSubToProp, 11 | } = {}) => (propName, style) => 12 | converge(pipe, [ 13 | expandMainProp, 14 | expandSubProps(createPropNameStrategy, DIRECTIONS_LIST, subWrapper), 15 | ])(propName, style, mainWrapper)({}) 16 | 17 | export default directionExpander 18 | -------------------------------------------------------------------------------- /src/build/expansion/propertyExpanders/directionsExpander.js: -------------------------------------------------------------------------------- 1 | import { DIRECTIONS_LIST } from '../../../const/expanders' 2 | import { expandSubProps } from '../../../utils/expanders' 3 | import { appendSubToProp } from '../../../utils/formatting' 4 | 5 | const directionsExpander = () => (_, style) => 6 | expandSubProps(appendSubToProp, DIRECTIONS_LIST)(``, style)({}) 7 | 8 | export default directionsExpander 9 | -------------------------------------------------------------------------------- /src/build/expansion/propertyExpanders/minMaxExpander.js: -------------------------------------------------------------------------------- 1 | import { converge, pipe } from 'ramda' 2 | import { MIN_MAX_LIST } from '../../../const/expanders' 3 | import { expandMainProp, expandSubProps } from '../../../utils/expanders' 4 | import { prependSubToProp } from '../../../utils/formatting' 5 | 6 | const minMaxExpander = () => (propName, style) => 7 | converge(pipe, [ 8 | expandMainProp, 9 | expandSubProps(prependSubToProp, MIN_MAX_LIST), 10 | ])(propName, style)({}) 11 | 12 | export default minMaxExpander 13 | -------------------------------------------------------------------------------- /src/build/mergeWithDefaultConfig.js: -------------------------------------------------------------------------------- 1 | import { mergeDeepRight, pipe } from 'ramda' 2 | import defaultConfig from '../config/defaultConfig' 3 | import { defaultToObj } from '../utils/functions' 4 | 5 | const mergeWithDefaultConfig = pipe(defaultToObj, mergeDeepRight(defaultConfig)) 6 | 7 | export default mergeWithDefaultConfig 8 | -------------------------------------------------------------------------------- /src/config/defaultConfig.js: -------------------------------------------------------------------------------- 1 | import renderBaseline from '../build/declarations/renderers/renderBaseline' 2 | import renderDirectionProps from '../build/declarations/renderers/renderDirectionProps' 3 | import renderDualFromOneProps from '../build/declarations/renderers/renderDualFromOneProps' 4 | import renderDualProps from '../build/declarations/renderers/renderDualProps' 5 | import CONFIG_FIELD_NAMES from '../const/config' 6 | import { 7 | DIRECTIONS_LIST_HORIZONTAL, 8 | DIRECTIONS_LIST_VERTICAL, 9 | } from '../const/expanders' 10 | import { LENGTH_UNITS } from '../const/units' 11 | import baselineTransformer from '../transformers/composite/baselineTransformer' 12 | import { 13 | borderLookupTransformer, 14 | boxShadowLookupTransformer, 15 | colorLookupTransformer, 16 | fontLookupTransformer, 17 | gradientLookupTransformer, 18 | imageLookupTransformer, 19 | scaleLookupTransformer, 20 | } from '../transformers/factory/dataLookupTransformers' 21 | import gradientTransformer from '../transformers/gradientTransformer' 22 | import lengthTransformers from '../transformers/lengthTransformers' 23 | import percentageStringToRatioTransformer from '../transformers/percentageStringToRatioTransformer' 24 | import transformTransformer from '../transformers/transformTransformer' 25 | 26 | const { BREAKPOINTS, DATA, ALIASES, SCOPES, PROPERTIES } = CONFIG_FIELD_NAMES 27 | 28 | // ----------------------------------------------------------------------------- 29 | // Define API 30 | // ----------------------------------------------------------------------------- 31 | 32 | const defaultConfig = { 33 | [BREAKPOINTS]: [], 34 | [DATA]: { 35 | [ALIASES]: { 36 | c: `color`, 37 | g: `gradient`, 38 | s: `scale`, 39 | d: `boxShadow`, 40 | b: `border`, 41 | i: `image`, 42 | f: `font`, 43 | }, 44 | lengthUnit: LENGTH_UNITS.REM, // | 'rem' | 'em' | 'px' 45 | baseFontSize: 16, // Font size of your page's root element 46 | rhythm: 20, // Unit of rhythm for use in layout 47 | baseline: { 48 | lineHeight: 20, // Baseline height 49 | minLeading: 2, // Minimum remaining leading before line or half-line added 50 | allowHalfLines: true, // Allow half-lines to be used in baseline calc 51 | }, 52 | color: {}, 53 | gradient: {}, 54 | scale: {}, 55 | boxShadow: {}, 56 | border: {}, 57 | image: {}, 58 | font: {}, 59 | [SCOPES]: [], 60 | }, 61 | [PROPERTIES]: { 62 | // ------------------------------------------------------------------------- 63 | // Box Model 64 | // ------------------------------------------------------------------------- 65 | 66 | padding: { 67 | transformers: lengthTransformers, 68 | }, 69 | margin: { 70 | transformers: lengthTransformers, 71 | }, 72 | border: { 73 | transformers: [ 74 | borderLookupTransformer, 75 | lengthTransformers, 76 | colorLookupTransformer, 77 | ], 78 | }, 79 | borderWidth: { 80 | transformers: lengthTransformers, 81 | }, 82 | borderColor: { 83 | transformers: colorLookupTransformer, 84 | }, 85 | borderStyle: {}, 86 | borderSpacing: { 87 | transformers: lengthTransformers, 88 | }, 89 | borderRadius: { 90 | transformers: lengthTransformers, 91 | }, 92 | borderImageSource: { 93 | transformers: [ 94 | imageLookupTransformer, 95 | gradientLookupTransformer, 96 | gradientTransformer([colorLookupTransformer, ...lengthTransformers]), 97 | ], 98 | }, 99 | 100 | // ------------------------------------------------------------------------- 101 | // Outline 102 | // ------------------------------------------------------------------------- 103 | 104 | outline: { 105 | transformers: [ 106 | borderLookupTransformer, 107 | lengthTransformers, 108 | colorLookupTransformer, 109 | ], 110 | }, 111 | outlineColor: { 112 | transformers: colorLookupTransformer, 113 | }, 114 | outlineOffset: { 115 | transformers: lengthTransformers, 116 | }, 117 | outlineStyle: {}, 118 | outlineWidth: { 119 | transformers: lengthTransformers, 120 | }, 121 | 122 | // ------------------------------------------------------------------------- 123 | // Text 124 | // ------------------------------------------------------------------------- 125 | 126 | fontFamily: { 127 | transformers: fontLookupTransformer, 128 | }, 129 | fontSize: { 130 | transformers: [scaleLookupTransformer, lengthTransformers], 131 | }, 132 | fontWeight: {}, 133 | fontVarient: {}, 134 | fontStretch: {}, 135 | fontStyle: {}, 136 | lineHeight: { 137 | transformers: lengthTransformers, 138 | }, 139 | textAlign: {}, 140 | letterSpacing: { 141 | transformers: lengthTransformers, 142 | }, 143 | wordWrap: {}, 144 | wordSpacing: {}, 145 | textDecoration: {}, 146 | whiteSpace: {}, 147 | 148 | // ------------------------------------------------------------------------- 149 | // List 150 | // ------------------------------------------------------------------------- 151 | 152 | listStyle: { 153 | transformers: imageLookupTransformer, 154 | }, 155 | listStyleImage: { 156 | transformers: imageLookupTransformer, 157 | }, 158 | listStylePosition: {}, 159 | listStyleType: {}, 160 | 161 | // ------------------------------------------------------------------------- 162 | // Background 163 | // ------------------------------------------------------------------------- 164 | 165 | background: { 166 | transformers: [ 167 | colorLookupTransformer, 168 | imageLookupTransformer, 169 | gradientLookupTransformer, 170 | imageLookupTransformer, 171 | gradientTransformer([colorLookupTransformer, ...lengthTransformers]), 172 | ], 173 | }, 174 | 175 | backgroundAttachment: {}, 176 | 177 | backgroundClip: {}, 178 | 179 | backgroundColor: { 180 | transformers: colorLookupTransformer, 181 | }, 182 | 183 | backgroundImage: { 184 | transformers: [ 185 | gradientLookupTransformer, 186 | imageLookupTransformer, 187 | imageLookupTransformer, 188 | gradientTransformer([colorLookupTransformer, ...lengthTransformers]), 189 | ], 190 | }, 191 | 192 | backgroundOrigin: {}, 193 | 194 | backgroundPosition: { 195 | transformers: lengthTransformers, 196 | }, 197 | 198 | backgroundRepeat: {}, 199 | 200 | backgroundSize: { 201 | transformers: lengthTransformers, 202 | }, 203 | 204 | // ------------------------------------------------------------------------- 205 | // Color / Visibility 206 | // ------------------------------------------------------------------------- 207 | 208 | opacity: { 209 | transformers: percentageStringToRatioTransformer, 210 | }, 211 | color: { 212 | transformers: colorLookupTransformer, 213 | }, 214 | visibility: {}, 215 | 216 | // ------------------------------------------------------------------------- 217 | // Layout 218 | // ------------------------------------------------------------------------- 219 | 220 | display: {}, 221 | float: {}, 222 | clear: {}, 223 | position: {}, 224 | directions: { 225 | transformers: lengthTransformers, 226 | }, 227 | width: { 228 | transformers: lengthTransformers, 229 | }, 230 | height: { 231 | transformers: lengthTransformers, 232 | }, 233 | 234 | // ------------------------------------------------------------------------- 235 | // Flexbox 236 | // ------------------------------------------------------------------------- 237 | 238 | flex: { 239 | transformers: lengthTransformers, 240 | }, 241 | flexDirection: {}, 242 | justifyContent: {}, 243 | alignItems: {}, 244 | alignContent: {}, 245 | alignSelf: {}, 246 | flexBasis: { 247 | transformers: lengthTransformers, 248 | }, 249 | flexShrink: {}, // Doesn't support values 250 | flexGrow: {}, // Doesn't support values 251 | flexWrap: {}, 252 | order: {}, 253 | 254 | // ------------------------------------------------------------------------- 255 | // Columns 256 | // ------------------------------------------------------------------------- 257 | 258 | columnCount: {}, 259 | 260 | columnWidth: { 261 | transformers: lengthTransformers, 262 | }, 263 | 264 | columnGap: { 265 | transformers: lengthTransformers, 266 | }, 267 | 268 | columnRuleWidth: { 269 | transformers: lengthTransformers, 270 | }, 271 | 272 | // ------------------------------------------------------------------------- 273 | // Tables 274 | // ------------------------------------------------------------------------- 275 | 276 | verticalAlign: {}, 277 | 278 | // ------------------------------------------------------------------------- 279 | // Misc 280 | // ------------------------------------------------------------------------- 281 | 282 | zIndex: {}, 283 | zoom: {}, 284 | overflow: {}, 285 | boxShadow: { 286 | transformers: [ 287 | boxShadowLookupTransformer, 288 | colorLookupTransformer, 289 | lengthTransformers, 290 | ], 291 | }, 292 | cursor: {}, 293 | hyphens: {}, 294 | 295 | // ------------------------------------------------------------------------- 296 | // Transforms 297 | // ------------------------------------------------------------------------- 298 | 299 | transform: { transformers: transformTransformer(lengthTransformers) }, 300 | transformBox: {}, 301 | transformOrigin: { 302 | transformers: lengthTransformers, 303 | }, 304 | 305 | // ------------------------------------------------------------------------- 306 | // SVG 307 | // ------------------------------------------------------------------------- 308 | 309 | fill: { 310 | transformers: [ 311 | colorLookupTransformer, 312 | gradientLookupTransformer, 313 | gradientTransformer([colorLookupTransformer, ...lengthTransformers]), 314 | ], 315 | }, 316 | 317 | stroke: { 318 | transformers: [ 319 | colorLookupTransformer, 320 | gradientLookupTransformer, 321 | gradientTransformer([colorLookupTransformer, ...lengthTransformers]), 322 | ], 323 | }, 324 | 325 | stopColor: { 326 | transformers: [colorLookupTransformer], 327 | }, 328 | 329 | // ------------------------------------------------------------------------- 330 | // Helpers 331 | // ------------------------------------------------------------------------- 332 | 333 | paddingH: { 334 | transformers: lengthTransformers, 335 | renderer: renderDualProps([`paddingLeft`, `paddingRight`]), 336 | }, 337 | 338 | paddingV: { 339 | transformers: lengthTransformers, 340 | renderer: renderDualProps([`paddingTop`, `paddingBottom`]), 341 | }, 342 | 343 | marginH: { 344 | transformers: lengthTransformers, 345 | renderer: renderDualProps([`marginRight`, `marginLeft`]), 346 | }, 347 | 348 | marginV: { 349 | transformers: lengthTransformers, 350 | renderer: renderDualProps([`marginTop`, `marginBottom`]), 351 | }, 352 | 353 | borderH: { 354 | transformers: [ 355 | borderLookupTransformer, 356 | lengthTransformers, 357 | colorLookupTransformer, 358 | ], 359 | renderer: renderDualFromOneProps([`borderLeft`, `borderRight`]), 360 | }, 361 | 362 | borderV: { 363 | transformers: [ 364 | borderLookupTransformer, 365 | lengthTransformers, 366 | colorLookupTransformer, 367 | ], 368 | renderer: renderDualFromOneProps([`borderTop`, `borderBottom`]), 369 | }, 370 | 371 | offset: { 372 | transformers: lengthTransformers, 373 | renderer: renderDirectionProps, 374 | }, 375 | 376 | offsetV: { 377 | transformers: lengthTransformers, 378 | renderer: renderDualProps(DIRECTIONS_LIST_VERTICAL), 379 | }, 380 | 381 | offsetH: { 382 | transformers: lengthTransformers, 383 | renderer: renderDualProps(DIRECTIONS_LIST_HORIZONTAL), 384 | }, 385 | 386 | borderTopRadius: { 387 | transformers: lengthTransformers, 388 | renderer: renderDualProps([ 389 | `borderTopLeftRadius`, 390 | `borderTopRightRadius`, 391 | ]), 392 | }, 393 | 394 | borderRightRadius: { 395 | transformers: lengthTransformers, 396 | renderer: renderDualProps([ 397 | `borderTopRightRadius`, 398 | `borderBottomRightRadius`, 399 | ]), 400 | }, 401 | 402 | borderBottomRadius: { 403 | transformers: lengthTransformers, 404 | renderer: renderDualProps([ 405 | `borderBottomRightRadius`, 406 | `borderBottomLeftRadius`, 407 | ]), 408 | }, 409 | 410 | borderLeftRadius: { 411 | transformers: lengthTransformers, 412 | renderer: renderDualProps([ 413 | `borderBottomLeftRadius`, 414 | `borderTopLeftRadius`, 415 | ]), 416 | }, 417 | 418 | baseline: { 419 | transformers: baselineTransformer([ 420 | scaleLookupTransformer, 421 | lengthTransformers, 422 | ]), 423 | renderer: renderBaseline, 424 | }, 425 | }, 426 | } 427 | 428 | export default defaultConfig 429 | -------------------------------------------------------------------------------- /src/const/breakpointMapping.js: -------------------------------------------------------------------------------- 1 | const BREAKPOINT_MAPPING_FIELDS = Object.freeze({ 2 | NAME: `name`, 3 | VALUE: `value`, 4 | QUERY: `query`, 5 | }) 6 | 7 | export default BREAKPOINT_MAPPING_FIELDS 8 | -------------------------------------------------------------------------------- /src/const/breakpoints.js: -------------------------------------------------------------------------------- 1 | export const DEFAULT_BREAKPOINT_NAME = `default` 2 | 3 | export const LT_MODIFIER = `<` 4 | 5 | export const GT_MODIFIER = `>` 6 | 7 | export const AT_MODIFIER = `@` 8 | 9 | export const MODIFIERS = [LT_MODIFIER, GT_MODIFIER, AT_MODIFIER] 10 | 11 | export const POSITIVE_OFFSET = `+` 12 | 13 | export const NEGATIVE_OFFSET = `-` 14 | 15 | export const OFFSETS = [POSITIVE_OFFSET, NEGATIVE_OFFSET] 16 | -------------------------------------------------------------------------------- /src/const/config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next- import/prefer-default-export 2 | export default Object.freeze({ 3 | DATA: `data`, 4 | SCOPES: `scopes`, 5 | RESOLVE: `resolve`, 6 | BREAKPOINTS: `breakpoints`, 7 | PROPERTIES: `properties`, 8 | TRANSFORMERS: `transformers`, 9 | ALIASES: `aliases`, 10 | }) 11 | -------------------------------------------------------------------------------- /src/const/errors.js: -------------------------------------------------------------------------------- 1 | export const ERROR_PREFIX = `[cssapi]` 2 | export const DATA_PREFIX = `(config.data)` 3 | export const BREAKPOINTS_PREFIX = `(config.breakpoints)` 4 | export const MQ_PREFIX = `api.mq()` 5 | export const API_PREFIX = `api()` 6 | export const MIXIN_PREFIX = `mixin()` 7 | export const PARSE_PREFIX = `(parsing)` 8 | -------------------------------------------------------------------------------- /src/const/expanders.js: -------------------------------------------------------------------------------- 1 | import { values } from 'ramda' 2 | 3 | export const DIRECTIONS = Object.freeze({ 4 | TOP: `top`, 5 | RIGHT: `right`, 6 | BOTTOM: `bottom`, 7 | LEFT: `left`, 8 | }) 9 | 10 | export const CORNERS = Object.freeze({ 11 | TOP_RIGHT: `topRight`, 12 | BOTTOM_RIGHT: `bottomRight`, 13 | BOTTOM_LEFT: `bottomLeft`, 14 | TOP_LEFT: `topLeft`, 15 | }) 16 | 17 | export const DIRECTIONS_LIST = values(DIRECTIONS) 18 | 19 | export const CORNERS_LIST = values(CORNERS) 20 | 21 | export const DIRECTIONS_LIST_HORIZONTAL = [DIRECTIONS.LEFT, DIRECTIONS.RIGHT] 22 | 23 | export const DIRECTIONS_LIST_VERTICAL = [DIRECTIONS.TOP, DIRECTIONS.BOTTOM] 24 | 25 | export const MIN_MAX = { 26 | MIN: `min`, 27 | MAX: `max`, 28 | } 29 | 30 | export const MIN_MAX_LIST = values(MIN_MAX) 31 | 32 | export const AXES = { 33 | X: `x`, 34 | Y: `y`, 35 | } 36 | 37 | export const AXES_LIST = values(AXES) 38 | -------------------------------------------------------------------------------- /src/const/queryDescriptor.js: -------------------------------------------------------------------------------- 1 | const QUERY_DESCRIPTOR_FIELDS = Object.freeze({ 2 | FROM: `from`, 3 | TO: `to`, 4 | }) 5 | 6 | export default QUERY_DESCRIPTOR_FIELDS 7 | -------------------------------------------------------------------------------- /src/const/rangeItem.js: -------------------------------------------------------------------------------- 1 | const RANGE_FIELDS = Object.freeze({ 2 | OFFSET: `offset`, 3 | MODIFIER: `modifier`, 4 | }) 5 | 6 | export default RANGE_FIELDS 7 | -------------------------------------------------------------------------------- /src/const/regexp.js: -------------------------------------------------------------------------------- 1 | export const RE_COLOR = /^(#?([0-9a-f]{3}|[0-9a-f]{6})|(rgb|hsl)a?\((-?\d+%?[,\s]+){2,3}\s*[\d.]+%?\))$/i 2 | 3 | export const RE_RHYTHM_UNITS = /^-?\d*\.?\d*ru$/ 4 | 5 | export const RE_START_OF_LINE = /^/gm 6 | 7 | export const RE_WHITESPACE = /\s+/ 8 | 9 | export const RE_PERCENT_NUMBER = /^\d+%$/ 10 | 11 | export const RE_CAPITAL_LETTERS = /([A-Z])/g 12 | 13 | export const RE_MEDIA_QUERY_STRING = /^@media / 14 | 15 | export const RE_RADIAL_GRADIENT = /^radial-gradient\(.+\)$/ 16 | 17 | export const RE_LINEAR_GRADIENT = /^linear-gradient\(.+\)$/ 18 | 19 | export const RE_URL = /^url\(.+\)$/ 20 | 21 | export const RE_UNNESTED_COMMA = /,(?![^()]*(?:\([^()]*\))?\))/g 22 | 23 | export const RE_ARGUMENTS_OF_CSS_FUNCTION = /^(?:[a-zA-Z\-_\d]*)\((.*)\)$/g 24 | 25 | export const RE_CSS_FUNCTION_NAME = /^([a-zA-Z-]*)\(.*\)$/ 26 | 27 | export const RE_UNNESTED_WHITESPACE = /\s(?![^()]*\))/g 28 | 29 | export const RE_COLOR_NAME = /^c:(.*)$/ 30 | 31 | // Note we exclude values starting `data:` as they might be images 32 | export const RE_TOKEN = /^(?!(?:data|http|https):)([a-zA-Z\-_\d]*):(.*)$/ 33 | 34 | export const RE_CSS_FUNCTION = /^[a-zA-Z-]*\(.*\)$/ 35 | 36 | export const RE_TRANSFORM_TRANSLATE_FUNCTION = /^translate(?:x|y|z|3d)?\(.*\)$/ 37 | 38 | export const RE_CALC_FUNCTION = /^calc\(.*\)$/ 39 | 40 | export const RE_CALC_VALUES = /(-*[0-9a-z.%]+)/g 41 | 42 | export const RE_MODIFIED_MQ = /^(?![<@][^<\s]*<)(?!^]?[a-zA-Z\d]+(?:(?:[+-])\d*\.?\d*(?:em)?)?)(?:<[a-zA-Z\d]+(?:(?:\+|-)\d*\.?\d*(?:em)?)?)?$/ 43 | -------------------------------------------------------------------------------- /src/const/scope.js: -------------------------------------------------------------------------------- 1 | const SCOPE_FIELDS = Object.freeze({ 2 | SCOPE: `scope`, 3 | }) 4 | 5 | export default SCOPE_FIELDS 6 | -------------------------------------------------------------------------------- /src/const/templates.js: -------------------------------------------------------------------------------- 1 | export const QUERY_MIN_TEMPLATE = `@media (min-width: #{minWidth})` 2 | 3 | export const QUERY_MAX_TEMPLATE = `@media (max-width: #{maxWidth})` 4 | 5 | export const QUERY_MIN_MAX_TEMPLATE = `@media (min-width: #{minWidth}) and (max-width: #{maxWidth})` 6 | 7 | export const QUERY_TEMPLATE = `#{query} { 8 | #{value} 9 | }` 10 | 11 | export const DECLARATION_TEMPLATE = `#{name}: #{value};` 12 | 13 | export const CSS_FUNCTION_TEMPLATE = `#{typeOfFunction}(#{value})` 14 | -------------------------------------------------------------------------------- /src/const/units.js: -------------------------------------------------------------------------------- 1 | export const LENGTH_UNITS = Object.freeze({ 2 | REM: `rem`, 3 | PX: `px`, 4 | EM: `em`, 5 | }) 6 | 7 | export const ANGLE_UNITS = Object.freeze({ 8 | DEG: `deg`, 9 | RAD: `rad`, 10 | GRAD: `grad`, 11 | TURN: `turn`, 12 | }) 13 | 14 | export const PERCENT_UNIT = `%` 15 | -------------------------------------------------------------------------------- /src/cssapi.js: -------------------------------------------------------------------------------- 1 | import { pipe } from 'ramda' 2 | import createApi from './build/createApi' 3 | import createDeclarationProcessors from './build/declarations/createDeclarationProcessors' 4 | import ensureBreakpointProvider from './build/ensureBreakpointProvider' 5 | import expandData from './build/expansion/expandData' 6 | import expandProperties from './build/expansion/expandProperties' 7 | import mergeWithDefaultConfig from './build/mergeWithDefaultConfig' 8 | 9 | // ----------------------------------------------------------------------------- 10 | // Exports 11 | // ----------------------------------------------------------------------------- 12 | 13 | const api = config => 14 | pipe( 15 | mergeWithDefaultConfig, 16 | expandData, 17 | expandProperties, 18 | ensureBreakpointProvider, 19 | createDeclarationProcessors, 20 | createApi(config) 21 | )(config) 22 | 23 | export default api 24 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | import { compose } from 'ramda' 2 | import { appendFlipped } from 'ramda-adjunct' 3 | import { 4 | API_PREFIX, 5 | BREAKPOINTS_PREFIX, 6 | DATA_PREFIX, 7 | ERROR_PREFIX, 8 | MIXIN_PREFIX, 9 | MQ_PREFIX, 10 | PARSE_PREFIX, 11 | } from './const/errors' 12 | import { 13 | joinWithSpace, 14 | printObj, 15 | wrapWithSingleQuotes, 16 | } from './utils/formatting' 17 | import { whenIsUndefined } from './utils/logic' 18 | 19 | // ----------------------------------------------------------------------------- 20 | // Utils 21 | // ----------------------------------------------------------------------------- 22 | 23 | export const throwLibError = message => { 24 | throw new Error(joinWithSpace([ERROR_PREFIX, message])) 25 | } 26 | 27 | const throwPrefixedError = prefix => 28 | compose(throwLibError, joinWithSpace, appendFlipped([prefix])) 29 | 30 | export const throwWhenUndefined = error => 31 | whenIsUndefined(() => { 32 | throw error 33 | }) 34 | 35 | // ----------------------------------------------------------------------------- 36 | // Prefixed Errors 37 | // ----------------------------------------------------------------------------- 38 | 39 | export const throwDataError = throwPrefixedError(DATA_PREFIX) 40 | export const throwBreakpointError = throwPrefixedError(BREAKPOINTS_PREFIX) 41 | export const throwMQError = throwPrefixedError(MQ_PREFIX) 42 | export const throwAPIError = throwPrefixedError(API_PREFIX) 43 | export const throwMixinError = throwPrefixedError(MIXIN_PREFIX) 44 | export const throwParseError = throwPrefixedError(PARSE_PREFIX) 45 | 46 | // ----------------------------------------------------------------------------- 47 | // Messages 48 | // ----------------------------------------------------------------------------- 49 | 50 | export const noBreakpointAtIndexError = idx => 51 | `Couldn't resolve breakpoint at index ${idx}` 52 | 53 | export const noBreakpointWithNameError = name => 54 | `Couldn't resolve breakpoint with name ${wrapWithSingleQuotes(name)}` 55 | 56 | export const invalidBreakpointSyntaxError = name => 57 | `The syntax you used to describe your breakpoint range was invalid for ${wrapWithSingleQuotes( 58 | name 59 | )}` 60 | 61 | export const invalidBreakpointError = (message, args) => 62 | `${message} with args: ${printObj(args)}` 63 | 64 | export const missingDataNodeError = name => 65 | `There is no data node defined named ${wrapWithSingleQuotes(name)}` 66 | 67 | export const missingDataItemKeyError = (nodeName, keyName) => 68 | `No item has been defined for data.${nodeName} named ${wrapWithSingleQuotes( 69 | keyName 70 | )}` 71 | 72 | export const unrecognisedDataPrefixError = (prefix, validPrefixes) => 73 | `Unrecognised prefix encountered: ${wrapWithSingleQuotes( 74 | prefix 75 | )}. Available prefixes are: ${printObj(validPrefixes)}` 76 | 77 | export const unsupportedBreakpointValuesError = declarations => 78 | `When using the mq() helper you must supply only a single decaration value but you supplied: ${printObj( 79 | declarations 80 | )}` 81 | 82 | export const invalidPropertyError = name => 83 | `API doesn't support a property named ${wrapWithSingleQuotes(name)}` 84 | 85 | export const noThemeObjectError = () => 86 | `There was no theme object available on the props object` 87 | 88 | export const noAPIOnThemeError = value => 89 | `There was no api function defined on the theme object. Value was: ${printObj( 90 | value 91 | )}` 92 | 93 | export const unitedNumberError = value => 94 | `Supplied value was not a number with a unit: '${wrapWithSingleQuotes( 95 | value 96 | )}'` 97 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import cssapi from './cssapi' 2 | 3 | export default cssapi 4 | 5 | export { default as api } from './themeHelpers/api' 6 | export { default as mixin } from './themeHelpers/mixin' 7 | export { default as mq } from './themeHelpers/mq' 8 | export { scope as s, scope } from './utils/scope' 9 | -------------------------------------------------------------------------------- /src/objects/breakpointDescriptor.js: -------------------------------------------------------------------------------- 1 | import { curry } from 'ramda' 2 | import { ensureArray } from 'ramda-adjunct' 3 | 4 | const createBreakpointDescriptor = curry((name, range) => ({ 5 | name, 6 | range: ensureArray(range), 7 | })) 8 | 9 | export default createBreakpointDescriptor 10 | -------------------------------------------------------------------------------- /src/objects/breakpointMapping.js: -------------------------------------------------------------------------------- 1 | import { assoc, curry, lensProp, prop } from 'ramda' 2 | import FIELD_NAMES from '../const/breakpointMapping' 3 | 4 | const { NAME, QUERY, VALUE } = FIELD_NAMES 5 | 6 | export const propName = prop(NAME) 7 | export const propValue = prop(VALUE) 8 | export const propQuery = prop(QUERY) 9 | 10 | export const lName = lensProp(NAME) 11 | export const lQuery = lensProp(QUERY) 12 | 13 | export const assocValue = assoc(VALUE) 14 | export const assocQuery = assoc(QUERY) 15 | 16 | export const createBreakpointMapping = curry((name, value, query) => ({ 17 | name, 18 | query, 19 | value, 20 | })) 21 | -------------------------------------------------------------------------------- /src/objects/config.js: -------------------------------------------------------------------------------- 1 | import { lensPath, lensProp, prop } from 'ramda' 2 | import FIELD_NAMES from '../const/config' 3 | 4 | const { 5 | PROPERTIES, 6 | SCOPES, 7 | DATA, 8 | TRANSFORMERS, 9 | BREAKPOINTS, 10 | RESOLVE, 11 | } = FIELD_NAMES 12 | 13 | export const propData = prop(DATA) 14 | export const lDataScopes = lensPath([DATA, SCOPES]) 15 | export const lData = lensProp(DATA) 16 | export const propScopes = prop(SCOPES) 17 | export const lProperties = lensProp(PROPERTIES) 18 | export const lTransformers = lensProp(TRANSFORMERS) 19 | export const lBreakpoints = lensProp(BREAKPOINTS) 20 | export const lResolve = lensProp(RESOLVE) 21 | -------------------------------------------------------------------------------- /src/objects/queryDescriptor.js: -------------------------------------------------------------------------------- 1 | import { anyPass, isNil, reject } from 'ramda' 2 | import { isZero } from '../utils/predicate' 3 | 4 | const createQueryDescriptor = reject(anyPass([isNil, isZero])) 5 | 6 | export default createQueryDescriptor 7 | -------------------------------------------------------------------------------- /src/objects/rangeItem.js: -------------------------------------------------------------------------------- 1 | import { complement, has, prop } from 'ramda' 2 | import FIELD_NAMES from '../const/rangeItem' 3 | 4 | const { OFFSET, MODIFIER } = FIELD_NAMES 5 | 6 | export const propOffset = prop(OFFSET) 7 | 8 | export const propModifier = prop(MODIFIER) 9 | 10 | export const hasModifier = has(MODIFIER) 11 | 12 | export const hasNoModifier = complement(hasModifier) 13 | -------------------------------------------------------------------------------- /src/objects/scope.js: -------------------------------------------------------------------------------- 1 | import { has, objOf, prop } from 'ramda' 2 | import FIELDS from '../const/scope' 3 | 4 | const { SCOPE } = FIELDS 5 | 6 | export const createScope = objOf(SCOPE) 7 | 8 | export const propScope = prop(SCOPE) 9 | 10 | export const hasScope = has(SCOPE) 11 | -------------------------------------------------------------------------------- /src/resolvers/resolveKeyToObjectValue.js: -------------------------------------------------------------------------------- 1 | import { pipe, prop } from 'ramda' 2 | import { missingDataItemKeyError, throwDataError } from '../errors' 3 | import resolveKeyToValue from '../resolvers/resolveKeyToValue' 4 | import { whenIsUndefined } from '../utils/logic' 5 | 6 | const resolveKeyToObjectValue = dataPropName => ( 7 | propName, 8 | data, 9 | breakpointName 10 | ) => 11 | pipe( 12 | resolveKeyToValue(dataPropName), 13 | prop(propName), 14 | whenIsUndefined(() => 15 | throwDataError(missingDataItemKeyError(dataPropName, propName)) 16 | ) 17 | )(propName, data, breakpointName) 18 | 19 | export default resolveKeyToObjectValue 20 | -------------------------------------------------------------------------------- /src/resolvers/resolveKeyToValue.js: -------------------------------------------------------------------------------- 1 | import { 2 | always, 3 | append, 4 | cond, 5 | contains, 6 | defaultTo, 7 | either, 8 | filter, 9 | isNil, 10 | map, 11 | pipe, 12 | prop, 13 | reduce, 14 | reduced, 15 | reverse, 16 | T, 17 | when, 18 | } from 'ramda' 19 | import { isObject, lensSatisfies, mergeRight } from 'ramda-adjunct' 20 | import { DEFAULT_BREAKPOINT_NAME } from '../const/breakpoints' 21 | import { missingDataNodeError, throwDataError } from '../errors' 22 | import { lResolve, propData, propScopes } from '../objects/config' 23 | import { whenIsUndefined } from '../utils/logic' 24 | import { hasScopes, isNotDefaultBreakpoint } from '../utils/predicate' 25 | 26 | const filterResolvingScopes = breakpointName => 27 | filter(lensSatisfies(contains(breakpointName), lResolve)) 28 | 29 | // Find any scopes that resolve to this breakpoint, reversing them so that the 30 | // final scope is first, adding the default scope in last position. 31 | const findResolvingScopes = (breakpointName, data) => 32 | pipe( 33 | propScopes, 34 | filterResolvingScopes(breakpointName), 35 | map(propData), 36 | reverse, 37 | append(data) 38 | )(data) 39 | 40 | // Merge any props that don't already exist on the object 41 | const resolveObject = acc => when(always(isObject(acc)), mergeRight(acc)) 42 | 43 | const findPropOnScope = dataPropName => (acc, scopeData) => 44 | pipe( 45 | prop(dataPropName), 46 | cond([[isNil, always(acc)], [isObject, resolveObject(acc)], [T, reduced]]) 47 | )(scopeData) 48 | 49 | const findPropsOnScope = dataPropName => 50 | reduce(findPropOnScope(dataPropName), null) 51 | 52 | // Find any matching prop on this dataNode 53 | const resolveScopes = (data, dataPropName) => breakpointName => 54 | pipe(findResolvingScopes, findPropsOnScope(dataPropName))( 55 | breakpointName, 56 | data 57 | ) 58 | 59 | const shouldResolveScopes = data => 60 | either(isNotDefaultBreakpoint, () => hasScopes(data)) 61 | 62 | const findDataProp = (breakpointName, dataPropName) => data => 63 | pipe( 64 | // Try using scopes 65 | when(shouldResolveScopes(data), resolveScopes(data, dataPropName)), 66 | // Fall back to root 67 | defaultTo(prop(dataPropName, data)) 68 | )(breakpointName) 69 | 70 | const resolveKeyToValue = dataPropName => ( 71 | value, 72 | data, 73 | breakpointName = DEFAULT_BREAKPOINT_NAME 74 | ) => 75 | pipe( 76 | findDataProp(breakpointName, dataPropName), 77 | whenIsUndefined(() => throwDataError(missingDataNodeError(dataPropName))) 78 | )(data) 79 | 80 | export default resolveKeyToValue 81 | -------------------------------------------------------------------------------- /src/resolvers/resolveKeysToObjectValues.js: -------------------------------------------------------------------------------- 1 | import { map } from 'ramda' 2 | import resolveKeyToObjectValue from './resolveKeyToObjectValue' 3 | 4 | const resolveKeysToValues = (name, keys) => (data, breakpointName) => 5 | map(key => resolveKeyToObjectValue(name)(key, data, breakpointName), keys) 6 | 7 | export default resolveKeysToValues 8 | -------------------------------------------------------------------------------- /src/resolvers/resolveKeysToValues.js: -------------------------------------------------------------------------------- 1 | import { map } from 'ramda' 2 | import resolveKeyToValue from './resolveKeyToValue' 3 | 4 | const resolveKeysToValues = keys => (value, data, breakpointName) => 5 | map(key => resolveKeyToValue(key)(value, data, breakpointName), keys) 6 | 7 | export default resolveKeysToValues 8 | -------------------------------------------------------------------------------- /src/themeHelpers/api.js: -------------------------------------------------------------------------------- 1 | import { apply, flip, pipe } from 'ramda' 2 | import { throwAPIError } from '../errors' 3 | import getApiFromProps from './getApiFromProps' 4 | 5 | const api = declarations => 6 | pipe(getApiFromProps(throwAPIError), flip(apply)([declarations])) 7 | 8 | export default api 9 | -------------------------------------------------------------------------------- /src/themeHelpers/getApiFromProps.js: -------------------------------------------------------------------------------- 1 | import { pipe, prop, unless } from 'ramda' 2 | import { isFunction, isPlainObj } from 'ramda-adjunct' 3 | import { noAPIOnThemeError, noThemeObjectError } from '../errors' 4 | 5 | const getApiFromProps = errorFunc => props => 6 | pipe( 7 | prop(`theme`), 8 | unless(isPlainObj, value => errorFunc(noThemeObjectError(value))), 9 | prop(`api`), 10 | unless(isFunction, value => errorFunc(noAPIOnThemeError(value))) 11 | )(props) 12 | 13 | export default getApiFromProps 14 | -------------------------------------------------------------------------------- /src/themeHelpers/mixin.js: -------------------------------------------------------------------------------- 1 | import { apply, flip, pair, pipe } from 'ramda' 2 | import { throwMixinError } from '../errors' 3 | import getApiFromProps from './getApiFromProps' 4 | 5 | const mixin = f => (...args) => props => 6 | pipe( 7 | getApiFromProps(throwMixinError), 8 | flip(pair)(props), 9 | apply(f), 10 | flip(apply)(args) 11 | )(props) 12 | 13 | export default mixin 14 | -------------------------------------------------------------------------------- /src/themeHelpers/mq.js: -------------------------------------------------------------------------------- 1 | import { apply, flip, pipe, prop } from 'ramda' 2 | import { throwAPIError } from '../errors' 3 | import getApiFromProps from './getApiFromProps' 4 | 5 | const propMq = prop(`mq`) 6 | 7 | const api = (breakpoint, declarations) => 8 | pipe( 9 | getApiFromProps(throwAPIError), 10 | propMq, 11 | flip(apply)([breakpoint, declarations]) 12 | ) 13 | 14 | export default api 15 | -------------------------------------------------------------------------------- /src/transformers/calcTransformer.js: -------------------------------------------------------------------------------- 1 | import { isCalcFunction } from '../utils/predicate' 2 | import { transformCalcElements, transformValue } from '../utils/transformers' 3 | import transformer from './transformer' 4 | 5 | const calcTransformer = transformers => 6 | transformer(isCalcFunction, (value, data, breakpointName) => { 7 | const r = transformCalcElements( 8 | transformValue(transformers, data, breakpointName) 9 | )(value) 10 | return r 11 | }) 12 | export default calcTransformer 13 | -------------------------------------------------------------------------------- /src/transformers/composite/allPartsTransformer.js: -------------------------------------------------------------------------------- 1 | import { transformValues } from '../../utils/transformers' 2 | 3 | const allPartsTransformer = transformers => (value, data, breakpointName) => 4 | transformValues(transformers, data, breakpointName, value) 5 | 6 | export default allPartsTransformer 7 | -------------------------------------------------------------------------------- /src/transformers/composite/baselineTransformer.js: -------------------------------------------------------------------------------- 1 | import { multiply, pipe, T, unless } from 'ramda' 2 | import { isArray } from 'ramda-adjunct' 3 | import resolveKeysToObjectValues from '../../resolvers/resolveKeysToObjectValues' 4 | import resolveKeyToValue from '../../resolvers/resolveKeyToValue' 5 | import { linesForFontsize } from '../../utils/baseline' 6 | import { unitedDimensionToUnitlessPixelValue } from '../../utils/converters' 7 | import { splitOnUnnestedWhitespace } from '../../utils/formatting' 8 | import { whenIsUndefined } from '../../utils/logic' 9 | import { transformValue } from '../../utils/transformers' 10 | import transformer from '../transformer' 11 | 12 | const PROP = `baseline` 13 | const KEYS = [`lineHeight`, `minLeading`, `allowHalfLines`] 14 | 15 | const rhythmUnitsToRemsTransformer = fontSizeToLengthTransformer => 16 | transformer(T, (value, data, breakpointName) => { 17 | const [lineHeight, minLeading, allowHalfLines] = resolveKeysToObjectValues( 18 | PROP, 19 | KEYS 20 | )(data, breakpointName) 21 | 22 | const baseFontSize = resolveKeyToValue(`baseFontSize`)( 23 | value, 24 | data, 25 | breakpointName 26 | ) 27 | 28 | const [fontSize, lines] = unless(isArray, splitOnUnnestedWhitespace)(value) 29 | 30 | const transformedFontSize = transformValue( 31 | fontSizeToLengthTransformer, 32 | data, 33 | breakpointName, 34 | fontSize 35 | ) 36 | const fontSizeUnitlessPx = unitedDimensionToUnitlessPixelValue( 37 | transformedFontSize, 38 | baseFontSize 39 | ) 40 | 41 | const transformedLines = pipe( 42 | whenIsUndefined(() => 43 | linesForFontsize( 44 | minLeading, 45 | allowHalfLines, 46 | lineHeight, 47 | fontSizeUnitlessPx 48 | ) 49 | ), 50 | multiply(lineHeight), 51 | transformValue(fontSizeToLengthTransformer, data, breakpointName) 52 | )(lines) 53 | 54 | return [transformedFontSize, transformedLines] 55 | }) 56 | 57 | export default rhythmUnitsToRemsTransformer 58 | -------------------------------------------------------------------------------- /src/transformers/composite/partPositionTransformer.js: -------------------------------------------------------------------------------- 1 | import { transformValueAt } from '../../utils/transformers' 2 | 3 | const partPositionTransformer = position => transformers => ( 4 | value, 5 | data, 6 | breakpointName 7 | ) => transformValueAt(transformers, data, breakpointName, position)(value) 8 | 9 | export default partPositionTransformer 10 | -------------------------------------------------------------------------------- /src/transformers/composite/transformPartsWith.js: -------------------------------------------------------------------------------- 1 | import { pipe } from 'ramda' 2 | import { transformValues } from '../../utils/transformers' 3 | 4 | const transformPartsWith = transformers => (value, data, breakpointName) => 5 | pipe(transformValues(transformers, data, breakpointName))(value) 6 | 7 | export default transformPartsWith 8 | -------------------------------------------------------------------------------- /src/transformers/dataLookupTransformer.js: -------------------------------------------------------------------------------- 1 | import { when } from 'ramda' 2 | import resolveKeyToObjectValue from '../resolvers/resolveKeyToObjectValue' 3 | import { nameAndAliasesForNodeName } from '../utils/data' 4 | import { tokenName } from '../utils/parse' 5 | import { tokenMatches } from '../utils/predicate' 6 | 7 | // Lookup the value of an object, for example `color.primary` 8 | const dataLookupTransformer = nodeName => (value, data, breakpointName) => { 9 | const nameAndAliases = nameAndAliasesForNodeName(data.aliases, nodeName) 10 | 11 | return when(tokenMatches(nameAndAliases), () => { 12 | const name = tokenName(value) 13 | return resolveKeyToObjectValue(nodeName)(name, data, breakpointName) 14 | })(value) 15 | } 16 | 17 | export default dataLookupTransformer 18 | -------------------------------------------------------------------------------- /src/transformers/factory/dataLookupTransformers.js: -------------------------------------------------------------------------------- 1 | import { assoc, pipe, reduce, __ } from 'ramda' 2 | import dataLookupTransformer from '../dataLookupTransformer' 3 | 4 | const DATA_NODE_NAMES = [ 5 | `color`, 6 | `gradient`, 7 | `font`, 8 | `scale`, 9 | `boxShadow`, 10 | `image`, 11 | `border`, 12 | ] 13 | 14 | export const { 15 | colorLookupTransformer, 16 | gradientLookupTransformer, 17 | fontLookupTransformer, 18 | scaleLookupTransformer, 19 | boxShadowLookupTransformer, 20 | imageLookupTransformer, 21 | borderLookupTransformer, 22 | } = reduce( 23 | (acc, value) => 24 | pipe(dataLookupTransformer, assoc(`${value}LookupTransformer`, __, acc))( 25 | value 26 | ), 27 | {} 28 | )(DATA_NODE_NAMES) 29 | -------------------------------------------------------------------------------- /src/transformers/gradientTransformer.js: -------------------------------------------------------------------------------- 1 | import { isGradient } from '../utils/predicate' 2 | import { 3 | transformFunctionElements, 4 | transformValue, 5 | } from '../utils/transformers' 6 | import transformPartsWith from './composite/transformPartsWith' 7 | import transformer from './transformer' 8 | 9 | const impl = transformers => (value, data, breakpointName) => 10 | transformFunctionElements( 11 | transformValue(transformPartsWith(transformers), data, breakpointName) 12 | )(value) 13 | 14 | const gradientTransformer = transformers => (value, data, breakpointName) => 15 | transformer(isGradient, impl(transformers))(value, data, breakpointName) 16 | 17 | export default gradientTransformer 18 | -------------------------------------------------------------------------------- /src/transformers/lengthToEmsTransformer.js: -------------------------------------------------------------------------------- 1 | import rhythmUnitsToEmsTransformer from '../transformers/rhythmUnitsToEmsTransformer' 2 | import unitlessNumberToEmsTransformer from './unitlessNumberToEmsTransformer' 3 | 4 | const lengthToRemsTransformer = [ 5 | unitlessNumberToEmsTransformer, 6 | rhythmUnitsToEmsTransformer, 7 | ] 8 | 9 | export default lengthToRemsTransformer 10 | -------------------------------------------------------------------------------- /src/transformers/lengthToRemsTransformer.js: -------------------------------------------------------------------------------- 1 | import unitlessNumberToRemsTransformer from './unitlessNumberToRemsTransformer' 2 | import rhythmUnitsToRemsTransformer from '../transformers/rhythmUnitsToRemsTransformer' 3 | 4 | const lengthToRemsTransformer = [ 5 | unitlessNumberToRemsTransformer, 6 | rhythmUnitsToRemsTransformer, 7 | ] 8 | 9 | export default lengthToRemsTransformer 10 | -------------------------------------------------------------------------------- /src/transformers/lengthTransformers.js: -------------------------------------------------------------------------------- 1 | import rhythmUnitsToLengthTransformer from '../transformers/rhythmUnitsToLengthTransformer' 2 | import calcTransformer from './calcTransformer' 3 | import unitlessNumberToLengthTransformer from './unitlessNumberToLengthTransformer' 4 | 5 | const lengthTransformers = [ 6 | calcTransformer([ 7 | unitlessNumberToLengthTransformer, 8 | rhythmUnitsToLengthTransformer, 9 | ]), 10 | unitlessNumberToLengthTransformer, 11 | rhythmUnitsToLengthTransformer, 12 | ] 13 | 14 | export default lengthTransformers 15 | -------------------------------------------------------------------------------- /src/transformers/percentageStringToRatioTransformer.js: -------------------------------------------------------------------------------- 1 | import { isPercentString } from '../utils/predicate' 2 | import { percentageStringToRatio } from '../utils/converters' 3 | import transformer from './transformer' 4 | 5 | const percentageStringToRatioTransformer = transformer( 6 | isPercentString, 7 | percentageStringToRatio 8 | ) 9 | 10 | export default percentageStringToRatioTransformer 11 | -------------------------------------------------------------------------------- /src/transformers/ratioToPercentageStringTransformer.js: -------------------------------------------------------------------------------- 1 | import { isValidPositiveNumber } from '../utils/predicate' 2 | import { ratioToPercentString } from '../utils/converters' 3 | import transformer from './transformer' 4 | 5 | const ratioToPercentageStringTransformer = transformer( 6 | isValidPositiveNumber, 7 | ratioToPercentString 8 | ) 9 | 10 | export default ratioToPercentageStringTransformer 11 | -------------------------------------------------------------------------------- /src/transformers/rhythmUnitsToEmsTransformer.js: -------------------------------------------------------------------------------- 1 | import { LENGTH_UNITS } from '../const/units' 2 | import resolveKeyToValue from '../resolvers/resolveKeyToValue' 3 | import { mulitplyUnitlessNumbersToDistance } from '../utils/converters' 4 | import { isRhythmUnit } from '../utils/predicate' 5 | import transformer from './transformer' 6 | 7 | const rhythmUnitsToRemsTransformer = transformer( 8 | isRhythmUnit, 9 | (value, data, breakpointName) => { 10 | const rhythm = resolveKeyToValue(`rhythm`)(value, data, breakpointName) 11 | return mulitplyUnitlessNumbersToDistance( 12 | rhythm, 13 | data.baseFontSize, 14 | LENGTH_UNITS.REM 15 | )(value) 16 | } 17 | ) 18 | 19 | export default rhythmUnitsToRemsTransformer 20 | -------------------------------------------------------------------------------- /src/transformers/rhythmUnitsToLengthTransformer.js: -------------------------------------------------------------------------------- 1 | import resolveKeysToValues from '../resolvers/resolveKeysToValues' 2 | import { mulitplyUnitlessNumbersToDistance } from '../utils/converters' 3 | import { isRhythmUnit } from '../utils/predicate' 4 | import transformer from './transformer' 5 | 6 | const rhythmUnitsToRemsTransformer = transformer( 7 | isRhythmUnit, 8 | (value, data, breakpointName) => { 9 | const [rhythm, lengthUnit, baseFontSize] = resolveKeysToValues([ 10 | `rhythm`, 11 | `lengthUnit`, 12 | `baseFontSize`, 13 | ])(value, data, breakpointName) 14 | 15 | return mulitplyUnitlessNumbersToDistance(rhythm, lengthUnit, baseFontSize)( 16 | value 17 | ) 18 | } 19 | ) 20 | 21 | export default rhythmUnitsToRemsTransformer 22 | -------------------------------------------------------------------------------- /src/transformers/rhythmUnitsToRemsTransformer.js: -------------------------------------------------------------------------------- 1 | import { LENGTH_UNITS } from '../const/units' 2 | import resolveKeyToValue from '../resolvers/resolveKeyToValue' 3 | import { mulitplyUnitlessNumbersToDistance } from '../utils/converters' 4 | import { isRhythmUnit } from '../utils/predicate' 5 | import transformer from './transformer' 6 | 7 | const rhythmUnitsToRemsTransformer = transformer( 8 | isRhythmUnit, 9 | (value, data, breakpointName) => { 10 | const rhythm = resolveKeyToValue(`rhythm`)(value, data, breakpointName) 11 | return mulitplyUnitlessNumbersToDistance( 12 | rhythm, 13 | data.baseFontSize, 14 | LENGTH_UNITS.REM 15 | )(value) 16 | } 17 | ) 18 | 19 | export default rhythmUnitsToRemsTransformer 20 | -------------------------------------------------------------------------------- /src/transformers/rootPxToEmTransformer.js: -------------------------------------------------------------------------------- 1 | import { transformValue } from '../utils/transformers' 2 | import lengthToEmsTransformer from './lengthToEmsTransformer' 3 | 4 | // the base font size will always be 16 for media queries 5 | const rootPxToEmTransformer = transformValue( 6 | lengthToEmsTransformer, 7 | { baseFontSize: 16 }, 8 | null 9 | ) 10 | 11 | export default rootPxToEmTransformer 12 | -------------------------------------------------------------------------------- /src/transformers/transformTransformer.js: -------------------------------------------------------------------------------- 1 | import { isTransformTranslateFunction } from '../utils/predicate' 2 | import { 3 | transformFunctionElements, 4 | transformValue, 5 | } from '../utils/transformers' 6 | import transformPartsWith from './composite/transformPartsWith' 7 | import transformer from './transformer' 8 | 9 | const impl = transformers => (value, data, breakpointName) => 10 | transformFunctionElements( 11 | transformValue(transformPartsWith(transformers), data, breakpointName) 12 | )(value) 13 | 14 | const transformTransformer = transformers => (value, data, breakpointName) => 15 | transformer(isTransformTranslateFunction, impl(transformers))( 16 | value, 17 | data, 18 | breakpointName 19 | ) 20 | 21 | export default transformTransformer 22 | -------------------------------------------------------------------------------- /src/transformers/transformer.js: -------------------------------------------------------------------------------- 1 | import { when } from 'ramda' 2 | import { transformValue } from '../utils/transformers' 3 | 4 | const transformer = (predicate, transformers) => ( 5 | value, 6 | data, 7 | breakpointName 8 | ) => 9 | when(predicate, () => 10 | transformValue(transformers, data, breakpointName, value) 11 | )(value) 12 | 13 | export default transformer 14 | -------------------------------------------------------------------------------- /src/transformers/unitlessNumberToEmsTransformer.js: -------------------------------------------------------------------------------- 1 | import { LENGTH_UNITS } from '../const/units' 2 | import transformer from './transformer' 3 | import { isValidNonZeroNumber } from '../utils/predicate' 4 | import { unitlessNumberToDistance } from '../utils/converters' 5 | 6 | const unitlessNumberToRemsTransformer = transformer( 7 | isValidNonZeroNumber, 8 | (value, data) => 9 | unitlessNumberToDistance(LENGTH_UNITS.EM, data.baseFontSize)(value) 10 | ) 11 | export default unitlessNumberToRemsTransformer 12 | -------------------------------------------------------------------------------- /src/transformers/unitlessNumberToLengthTransformer.js: -------------------------------------------------------------------------------- 1 | import keysToValueResolver from '../resolvers/resolveKeysToValues' 2 | import { unitlessNumberToDistance } from '../utils/converters' 3 | import { isValidNonZeroNumber } from '../utils/predicate' 4 | import transformer from './transformer' 5 | 6 | const unitlessNumberToLengthTransformer = transformer( 7 | isValidNonZeroNumber, 8 | (value, data, breakpointName) => { 9 | const [lengthUnit, baseFontSize] = keysToValueResolver([ 10 | `lengthUnit`, 11 | `baseFontSize`, 12 | ])(value, data, breakpointName) 13 | return unitlessNumberToDistance(lengthUnit, baseFontSize)(value) 14 | } 15 | ) 16 | 17 | export default unitlessNumberToLengthTransformer 18 | -------------------------------------------------------------------------------- /src/transformers/unitlessNumberToPxTransformer.js: -------------------------------------------------------------------------------- 1 | import { LENGTH_UNITS } from '../const/units' 2 | import transformer from './transformer' 3 | import { isValidNonZeroNumber } from '../utils/predicate' 4 | import { unitlessNumberToDistance } from '../utils/converters' 5 | 6 | const unitlessNumberToRemsTransformer = transformer( 7 | isValidNonZeroNumber, 8 | (value, data) => 9 | unitlessNumberToDistance(LENGTH_UNITS.PX, data.baseFontSize)(value) 10 | ) 11 | 12 | export default unitlessNumberToRemsTransformer 13 | -------------------------------------------------------------------------------- /src/transformers/unitlessNumberToRemsTransformer.js: -------------------------------------------------------------------------------- 1 | import { LENGTH_UNITS } from '../const/units' 2 | import transformer from './transformer' 3 | import { isValidNonZeroNumber } from '../utils/predicate' 4 | import { unitlessNumberToDistance } from '../utils/converters' 5 | 6 | const unitlessNumberToRemsTransformer = transformer( 7 | isValidNonZeroNumber, 8 | (value, data) => 9 | unitlessNumberToDistance(LENGTH_UNITS.REM, data.baseFontSize)(value) 10 | ) 11 | 12 | export default unitlessNumberToRemsTransformer 13 | -------------------------------------------------------------------------------- /src/utils/baseline.js: -------------------------------------------------------------------------------- 1 | import { isTrue } from 'ramda-adjunct' 2 | import { divideBy2 } from './numbers' 3 | 4 | const wholeLinesForFontSize = (minLeading, fontSize, baselineHeight) => { 5 | const lines = Math.ceil(fontSize / baselineHeight) 6 | return lines * baselineHeight - fontSize >= minLeading ? lines : lines + 1 7 | } 8 | 9 | // eslint-disable-next-line import/prefer-default-export 10 | export const linesForFontsize = ( 11 | minLeading, 12 | allowHalfLines, 13 | baselineHeight, 14 | fontSize 15 | ) => 16 | isTrue(allowHalfLines) 17 | ? divideBy2( 18 | wholeLinesForFontSize(minLeading, fontSize, divideBy2(baselineHeight)) 19 | ) 20 | : wholeLinesForFontSize(minLeading, fontSize, baselineHeight) 21 | -------------------------------------------------------------------------------- /src/utils/breakpointMap.js: -------------------------------------------------------------------------------- 1 | import { 2 | curry, 3 | dec, 4 | equals, 5 | find, 6 | findIndex, 7 | flip, 8 | gt, 9 | identity, 10 | inc, 11 | last, 12 | lensIndex, 13 | nth, 14 | pipe, 15 | prop, 16 | useWith, 17 | } from 'ramda' 18 | import { lensSatisfies } from 'ramda-adjunct' 19 | import { 20 | noBreakpointAtIndexError, 21 | noBreakpointWithNameError, 22 | throwWhenUndefined, 23 | } from '../errors' 24 | 25 | export const findBreakpointByName = curry((breakpointMap, name) => 26 | pipe( 27 | find(lensSatisfies(equals(name), lensIndex(0))), 28 | throwWhenUndefined(noBreakpointWithNameError(name)), 29 | last 30 | )(breakpointMap) 31 | ) 32 | 33 | export const findBreakpointIndex = (breakpointMap, name) => 34 | findIndex(lensSatisfies(equals(name), lensIndex(0)), breakpointMap) 35 | 36 | export const findBreakpointByIndex = curry((breakpointMap, idx) => 37 | pipe( 38 | flip(nth)(breakpointMap), 39 | throwWhenUndefined(noBreakpointAtIndexError(idx)) 40 | )(idx) 41 | ) 42 | 43 | export const findNextBreakpointByIndex = curry((breakpointMap, idx) => 44 | pipe(inc, findBreakpointByIndex(breakpointMap))(idx) 45 | ) 46 | 47 | export const isNotLastBreakpoint = useWith(gt, [ 48 | pipe(prop(`length`), dec), 49 | identity, 50 | ]) 51 | -------------------------------------------------------------------------------- /src/utils/breakpoints.js: -------------------------------------------------------------------------------- 1 | import { 2 | apply, 3 | identity, 4 | map, 5 | pipe, 6 | prepend, 7 | split, 8 | unless, 9 | useWith, 10 | } from 'ramda' 11 | import { list } from 'ramda-adjunct' 12 | import { DEFAULT_BREAKPOINT_NAME, LT_MODIFIER } from '../const/breakpoints' 13 | import { invalidBreakpointSyntaxError } from '../errors' 14 | import createBreakpointDescriptor from '../objects/breakpointDescriptor' 15 | import rootPxToEmTransformer from '../transformers/rootPxToEmTransformer' 16 | import { isEmString, isRange, isValidModifiedMq } from './predicate' 17 | import { createRangeItem } from './range' 18 | 19 | export const addDefaultBreakpoint = prepend([DEFAULT_BREAKPOINT_NAME, 0]) 20 | 21 | export const parseBreakpoint = value => { 22 | if (!isValidModifiedMq(value)) { 23 | throw Error(invalidBreakpointSyntaxError(value)) 24 | } 25 | 26 | // A range will always have a < in a position greater than 0 27 | if (!isRange(value)) { 28 | // It's a single item 29 | const rangeItem = createRangeItem(value) 30 | return createBreakpointDescriptor(value, rangeItem) 31 | } 32 | 33 | // Otherwise it's a range (containing two items) 34 | return pipe( 35 | split(LT_MODIFIER), 36 | map(createRangeItem), 37 | createBreakpointDescriptor(value) 38 | )(value) 39 | } 40 | 41 | export const breakpointValuesToEms = map( 42 | apply(useWith(list, [identity, unless(isEmString, rootPxToEmTransformer)])) 43 | ) 44 | -------------------------------------------------------------------------------- /src/utils/converters.js: -------------------------------------------------------------------------------- 1 | import { 2 | compose, 3 | cond, 4 | curry, 5 | divide, 6 | equals, 7 | flip, 8 | multiply, 9 | pipe, 10 | } from 'ramda' 11 | import { concatRight, isNumber } from 'ramda-adjunct' 12 | import { LENGTH_UNITS, PERCENT_UNIT } from '../const/units' 13 | import { joinWithNoSpace } from './formatting' 14 | import { divideBy } from './numbers' 15 | import { 16 | elementsOfUnitedNumberString, 17 | numericPartOfUnitedNumberString, 18 | } from './parse' 19 | import { isUnitRemOrEm } from './predicate' 20 | 21 | const { PX, REM, EM } = LENGTH_UNITS 22 | 23 | export const pxValueToRemOrEmValue = (value, baseFontSize) => 24 | divide(value, baseFontSize) 25 | 26 | export const pxValueToRemOrEmString = (unit, baseFontSize, value) => 27 | joinWithNoSpace([flip(pxValueToRemOrEmValue)(baseFontSize)(value), unit]) 28 | 29 | export const pxValueToPxString = value => joinWithNoSpace([value, PX]) 30 | 31 | export const percentageStringToRatio = compose( 32 | divideBy(100), 33 | numericPartOfUnitedNumberString 34 | ) 35 | 36 | export const ratioToPercentString = compose( 37 | concatRight(PERCENT_UNIT), 38 | String, 39 | multiply(100) 40 | ) 41 | 42 | export const unitlessNumberToDistance = (unit, baseFontSize) => value => 43 | cond([ 44 | [equals(PX), () => pxValueToPxString(value)], 45 | [equals(REM), () => pxValueToRemOrEmString(REM, baseFontSize, value)], 46 | [equals(EM), () => pxValueToRemOrEmString(EM, baseFontSize, value)], 47 | ])(unit) 48 | 49 | export const mulitplyUnitlessNumbersToDistance = (factor, unit, baseFontSize) => 50 | pipe( 51 | numericPartOfUnitedNumberString, 52 | multiply(factor), 53 | unitlessNumberToDistance(unit, baseFontSize) 54 | ) 55 | 56 | export const remOrEmToPxValue = (value, baseFontSize) => 57 | multiply(value, baseFontSize) 58 | 59 | export const unitedDimensionToUnitlessPixelValue = (value, baseFontSize) => { 60 | const [number, unit] = elementsOfUnitedNumberString(value) 61 | return isUnitRemOrEm(unit) ? remOrEmToPxValue(number, baseFontSize) : number 62 | } 63 | 64 | export const adjustNumberWithUnit = curry((f, value) => { 65 | // In case a unitless number is passed in 66 | if (isNumber(value)) return f(value) 67 | // Otherwise calculate using the numeric part and reattach the unit 68 | const [n, unit] = elementsOfUnitedNumberString(value) 69 | return `${f(n)}${unit}` 70 | }) 71 | -------------------------------------------------------------------------------- /src/utils/data.js: -------------------------------------------------------------------------------- 1 | import { converge, equals, identity, pipe, prepend } from 'ramda' 2 | import { filterKeys } from '../utils/objects' 3 | 4 | const aliasesForNodeName = aliases => name => filterKeys(equals(name))(aliases) 5 | 6 | // eslint-disable-next-line import/prefer-default-export 7 | export const nameAndAliasesForNodeName = (aliases, name) => 8 | pipe(converge(prepend, [identity, aliasesForNodeName(aliases)]))(name) 9 | -------------------------------------------------------------------------------- /src/utils/declarations.js: -------------------------------------------------------------------------------- 1 | import { 2 | always, 3 | append, 4 | assoc, 5 | assocPath, 6 | both, 7 | converge, 8 | curry, 9 | equals, 10 | findIndex, 11 | flip, 12 | gte, 13 | ifElse, 14 | init, 15 | last, 16 | lensIndex, 17 | objOf, 18 | omit, 19 | over, 20 | pipe, 21 | reduce, 22 | unless, 23 | __, 24 | } from 'ramda' 25 | import { concatRight, isNotNil, lensEq } from 'ramda-adjunct' 26 | import BREAKPOINT_MAPPING_FIELDS from '../const/breakpointMapping' 27 | import QUERY_DESCRIPTOR_FIELDS from '../const/queryDescriptor' 28 | import { throwMQError, unsupportedBreakpointValuesError } from '../errors' 29 | import { 30 | createBreakpointMapping, 31 | lName, 32 | lQuery, 33 | propValue, 34 | } from '../objects/breakpointMapping' 35 | import { reduceObjIndexed } from '../utils/objects' 36 | import { isValidMqValue } from '../utils/predicate' 37 | 38 | const { QUERY } = BREAKPOINT_MAPPING_FIELDS 39 | 40 | const { TO } = QUERY_DESCRIPTOR_FIELDS 41 | 42 | const foundMatch = flip(gte)(0) 43 | 44 | const findBatchIndexForMapping = ({ name, query }, batches) => 45 | findIndex(both(lensEq(lName, name), lensEq(lQuery, query)), batches) 46 | 47 | const createNewBatch = (breakpointMapping, batches) => 48 | append(breakpointMapping, batches) 49 | 50 | const addToBatch = ({ name, query, value }) => 51 | pipe(propValue, concatRight(value), createBreakpointMapping(name, __, query)) 52 | 53 | const addToBatchAtIndex = curry((breakpointMapping, batches, index) => 54 | over(lensIndex(index), addToBatch(breakpointMapping), batches) 55 | ) 56 | 57 | export const batchDeclarations = reduce( 58 | (batches, breakpointMapping) => 59 | pipe( 60 | findBatchIndexForMapping, 61 | ifElse(foundMatch, addToBatchAtIndex(breakpointMapping, batches), () => 62 | createNewBatch(breakpointMapping, batches) 63 | ) 64 | )(breakpointMapping, batches), 65 | [] 66 | ) 67 | 68 | const addBreakpoingToDeclaration = breakpoint => (acc, [name, value]) => 69 | pipe( 70 | unless(isValidMqValue, () => 71 | throwMQError(unsupportedBreakpointValuesError(value)) 72 | ), 73 | objOf(breakpoint), 74 | assoc(name, __, acc) 75 | )(value) 76 | 77 | export const addBreakpointToDeclarations = (breakpoint, declarations) => 78 | reduceObjIndexed(addBreakpoingToDeclaration(breakpoint), {}, declarations) 79 | 80 | const assocPathQueryTo = assocPath([QUERY, TO]) 81 | 82 | const compareMappings = (previous, current) => 83 | current.value !== `_` && (!previous || !equals(previous.value, current.value)) 84 | 85 | const replaceToValue = newToValue => 86 | ifElse( 87 | always(isNotNil(newToValue)), 88 | assocPathQueryTo(newToValue), 89 | over(lQuery, omit([TO])) 90 | ) 91 | 92 | const updateQuery = newToValue => pipe(last, replaceToValue(newToValue)) 93 | 94 | const updateLastMapping = (acc, newToValue) => 95 | converge(append, [updateQuery(newToValue), init])(acc) 96 | 97 | // This is gross 98 | export const optimiseDeclarations = mappings => { 99 | let lastMapping 100 | return reduce( 101 | (acc, mapping) => { 102 | let result 103 | const shouldAdd = compareMappings(lastMapping, mapping) 104 | if (shouldAdd) { 105 | lastMapping = mapping 106 | result = append(mapping, acc) 107 | } else { 108 | result = updateLastMapping(acc, mapping.query.to) 109 | lastMapping = last(result) 110 | } 111 | return result 112 | }, 113 | [], 114 | mappings 115 | ) 116 | } 117 | -------------------------------------------------------------------------------- /src/utils/expanders.js: -------------------------------------------------------------------------------- 1 | import { 2 | assoc, 3 | compose, 4 | identity, 5 | mergeDeepRight, 6 | objOf, 7 | over, 8 | pipe, 9 | reduce, 10 | __, 11 | } from 'ramda' 12 | import { list } from 'ramda-adjunct' 13 | import { lTransformers } from '../objects/config' 14 | 15 | export const toAppendedProps = (propName, style, affixedValues, toProp) => 16 | reduce( 17 | (acc, direction) => 18 | compose(assoc(__, style, acc), toProp, list)(propName, direction), 19 | {}, 20 | affixedValues 21 | ) 22 | 23 | export const expandMainProp = (propName, style, wrapper = identity) => 24 | assoc(propName, over(lTransformers, wrapper, style)) 25 | 26 | export const expandSubProps = (toProp, suffixes, wrapper = identity) => ( 27 | propName, 28 | style 29 | ) => 30 | mergeDeepRight( 31 | toAppendedProps( 32 | propName, 33 | over(lTransformers, wrapper, style), 34 | suffixes, 35 | toProp 36 | ) 37 | ) 38 | 39 | export const wrapWithTransform = wrapper => (propName, style) => 40 | pipe(over(lTransformers, wrapper), objOf(propName))(style) 41 | -------------------------------------------------------------------------------- /src/utils/formatting.js: -------------------------------------------------------------------------------- 1 | import dasherize from 'dasherize' 2 | import { 3 | append, 4 | assoc, 5 | compose, 6 | converge, 7 | curry, 8 | flatten, 9 | head, 10 | inc, 11 | insert, 12 | join, 13 | lensIndex, 14 | map, 15 | nth, 16 | over, 17 | pipe, 18 | prepend, 19 | replace, 20 | reverse, 21 | split, 22 | tail, 23 | toUpper, 24 | trim, 25 | when, 26 | } from 'ramda' 27 | import { 28 | compact, 29 | isArray, 30 | isPlainObj, 31 | lengthGt, 32 | list, 33 | reduceIndexed, 34 | } from 'ramda-adjunct' 35 | import { 36 | RE_ARGUMENTS_OF_CSS_FUNCTION, 37 | RE_CAPITAL_LETTERS, 38 | RE_START_OF_LINE, 39 | RE_UNNESTED_COMMA, 40 | RE_UNNESTED_WHITESPACE, 41 | RE_WHITESPACE, 42 | } from '../const/regexp' 43 | import { condDefault } from './functions' 44 | import { reduceObjIndexed } from './objects' 45 | 46 | // ----------------------------------------------------------------------------- 47 | // Chars 48 | // ----------------------------------------------------------------------------- 49 | 50 | const NEWLINE = `\n` 51 | const DOUBLE_NEWLINE = `${NEWLINE}${NEWLINE}` 52 | const SPACE = ` ` 53 | const COMMA = `,` 54 | const COLON = `:` 55 | const HYPHEN = `-` 56 | const FULL_STOP = `.` 57 | const SINGLE_QUOTE = `'` 58 | const PIPE = `|` 59 | const COMMA_SPACE = `${COMMA}${SPACE}` 60 | 61 | // ----------------------------------------------------------------------------- 62 | // Create 63 | // ----------------------------------------------------------------------------- 64 | 65 | const toToken = v => new RegExp(`#{(${v})}`, `g`) 66 | 67 | // ----------------------------------------------------------------------------- 68 | // Print 69 | // ----------------------------------------------------------------------------- 70 | 71 | export const printObj = JSON.stringify 72 | 73 | // ----------------------------------------------------------------------------- 74 | // Indent 75 | // ----------------------------------------------------------------------------- 76 | 77 | export const indentLines = replace(RE_START_OF_LINE, ` `) 78 | 79 | // ----------------------------------------------------------------------------- 80 | // Join 81 | // ----------------------------------------------------------------------------- 82 | 83 | export const joinWithSpace = join(SPACE) 84 | 85 | export const joinWithComma = join(COMMA) 86 | 87 | export const joinWithCommaSpace = join(COMMA_SPACE) 88 | 89 | export const joinWithHypen = join(HYPHEN) 90 | 91 | export const joinWithNoSpace = join(``) 92 | 93 | export const joinWithDot = join(FULL_STOP) 94 | 95 | export const joinWithNewline = join(NEWLINE) 96 | 97 | export const joinWithPipe = join(PIPE) 98 | 99 | export const joinWithDoubleNewlines = join(DOUBLE_NEWLINE) 100 | 101 | // ----------------------------------------------------------------------------- 102 | // Case 103 | // ----------------------------------------------------------------------------- 104 | 105 | export const firstToUpper = compose( 106 | joinWithNoSpace, 107 | over(lensIndex(0), toUpper) 108 | ) 109 | 110 | export const toKebabCase = dasherize 111 | 112 | // ----------------------------------------------------------------------------- 113 | // Split 114 | // ----------------------------------------------------------------------------- 115 | 116 | export const splitOnWhitespace = split(RE_WHITESPACE) 117 | 118 | export const splitOnUnnestedWhitespace = split(RE_UNNESTED_WHITESPACE) 119 | 120 | export const splitOnUnnestedComma = split(RE_UNNESTED_COMMA) 121 | 122 | export const splitOnComma = split(COMMA) 123 | 124 | export const splitCamelcase = compose( 125 | split(` `), 126 | replace(RE_CAPITAL_LETTERS, ` $1`) 127 | ) 128 | 129 | export const splitOnColon = split(COLON) 130 | 131 | // ----------------------------------------------------------------------------- 132 | // Wrap 133 | // ----------------------------------------------------------------------------- 134 | 135 | export const wrapWith = (a, b = a) => 136 | compose(joinWithNoSpace, prepend(a), append(b), String) 137 | 138 | export const wrapWithSingleQuotes = wrapWith(SINGLE_QUOTE) 139 | 140 | // ----------------------------------------------------------------------------- 141 | // Replace 142 | // ----------------------------------------------------------------------------- 143 | 144 | export const replaceToken = curry((template, tokenName, value) => 145 | replace(toToken(tokenName), value, template) 146 | ) 147 | 148 | const replaceWithMap = curry((template, valueMap) => 149 | reduceObjIndexed( 150 | (acc, [key, value]) => replaceToken(acc, key, value), 151 | template, 152 | valueMap 153 | ) 154 | ) 155 | 156 | const replaceWithArray = curry((template, values) => 157 | pipe( 158 | reduceIndexed((acc, v, idx) => assoc(inc(idx), v, acc), {}), 159 | replaceWithMap(template) 160 | )(values) 161 | ) 162 | 163 | export const replaceTokens = curry((template, value) => 164 | condDefault([ 165 | [isArray, replaceWithArray(template)], 166 | [isPlainObj, replaceWithMap(template)], 167 | ])(value) 168 | ) 169 | 170 | // ----------------------------------------------------------------------------- 171 | // Extract 172 | // ----------------------------------------------------------------------------- 173 | 174 | export const extractFunctionArguments = value => { 175 | const parts = new RegExp(RE_ARGUMENTS_OF_CSS_FUNCTION).exec(value) 176 | return nth(1, parts) 177 | } 178 | 179 | // ----------------------------------------------------------------------------- 180 | // Insert / Append / Prepend 181 | // ----------------------------------------------------------------------------- 182 | 183 | export const appendSubToProp = compose( 184 | joinWithNoSpace, 185 | flatten, 186 | when(lengthGt(1), converge(list, [head, compose(map(firstToUpper), tail)])), 187 | compact 188 | ) 189 | 190 | export const prependSubToProp = compose(appendSubToProp, reverse) 191 | 192 | export const insertSubIntoProp = compose( 193 | converge(compose(appendSubToProp, insert(1)), [ 194 | head, 195 | compose(splitCamelcase, joinWithNoSpace, tail), 196 | ]), 197 | reverse 198 | ) 199 | 200 | // ----------------------------------------------------------------------------- 201 | // Misc 202 | // ----------------------------------------------------------------------------- 203 | 204 | export const trimAll = map(trim) 205 | -------------------------------------------------------------------------------- /src/utils/functions.js: -------------------------------------------------------------------------------- 1 | import { T, identity, cond, curry, append, defaultTo } from 'ramda' 2 | 3 | // eslint-disable-next-line import/prefer-default-export 4 | export const condDefault = curry((list, value) => { 5 | const listWithDefault = append([T, identity], list) 6 | return cond(listWithDefault)(value) 7 | }) 8 | 9 | export const defaultToObj = defaultTo({}) 10 | 11 | export const defaultToArray = defaultTo([]) 12 | -------------------------------------------------------------------------------- /src/utils/list.js: -------------------------------------------------------------------------------- 1 | import { 2 | compose, 3 | converge, 4 | curry, 5 | defaultTo, 6 | equals, 7 | flip, 8 | head, 9 | identity, 10 | keys, 11 | length, 12 | lensIndex, 13 | nth, 14 | reduce, 15 | } from 'ramda' 16 | import { isNotUndefined, lengthEq, lensSatisfies } from 'ramda-adjunct' 17 | 18 | export const lengthEq1 = lengthEq(1) 19 | 20 | export const headEquals = value => compose(equals(value), head) 21 | 22 | export const nthFlipped = flip(nth) 23 | 24 | export const nthOr = curry((defaultValue, n, a) => 25 | defaultTo(defaultValue, nth(n, a)) 26 | ) 27 | 28 | export const reduceWithKeys = reducer => 29 | converge(reduce(reducer), [identity, keys]) 30 | 31 | export const hasIndex = curry((n, l) => 32 | lensSatisfies(isNotUndefined, lensIndex(n))(l) 33 | ) 34 | 35 | export const numKeys = compose(length, keys) 36 | -------------------------------------------------------------------------------- /src/utils/logic.js: -------------------------------------------------------------------------------- 1 | import { when } from 'ramda' 2 | import { isUndefined } from 'ramda-adjunct' 3 | 4 | // eslint-disable-next-line import/prefer-default-export 5 | export const whenIsUndefined = f => when(isUndefined, () => f()) 6 | -------------------------------------------------------------------------------- /src/utils/numbers.js: -------------------------------------------------------------------------------- 1 | import { clamp, divide, flip } from 'ramda' 2 | 3 | export const divideBy = flip(divide) 4 | 5 | export const divideBy2 = flip(divide)(2) 6 | 7 | export const clampPositive = clamp(0, Number.POSITIVE_INFINITY) 8 | -------------------------------------------------------------------------------- /src/utils/objects.js: -------------------------------------------------------------------------------- 1 | import { 2 | append, 3 | compose, 4 | curry, 5 | flip, 6 | prop, 7 | reduce, 8 | toPairs, 9 | when, 10 | } from 'ramda' 11 | 12 | export const reduceObjIndexed = curry((f, acc, v) => 13 | compose(reduce(f, acc), toPairs)(v) 14 | ) 15 | 16 | export const propFlipped = flip(prop) 17 | 18 | export const filterKeys = curry((predicate, obj) => 19 | reduceObjIndexed( 20 | (keys, [key, value]) => when(() => predicate(value), append(key))(keys), 21 | [], 22 | obj 23 | ) 24 | ) 25 | -------------------------------------------------------------------------------- /src/utils/parse.js: -------------------------------------------------------------------------------- 1 | import { 2 | any, 3 | compose, 4 | curry, 5 | equals, 6 | gt, 7 | isNil, 8 | last, 9 | lensIndex, 10 | pipe, 11 | unless, 12 | view, 13 | when, 14 | __, 15 | } from 'ramda' 16 | import { throwParseError, unitedNumberError } from '../errors' 17 | import { splitOnColon } from './formatting' 18 | import { clampPositive } from './numbers' 19 | 20 | export const elementsOfUnitedNumberString = value => { 21 | const captures = /^(-?\d+(?:.\d+)?)([a-z|%]+)?$/.exec(value) 22 | if (!captures || any(isNil, [captures, captures[1], captures[2]])) { 23 | throwParseError(unitedNumberError(value)) 24 | } 25 | return [Number(captures[1]), captures[2]] 26 | } 27 | 28 | export const numericPartOfUnitedNumberString = compose( 29 | view(lensIndex(0)), 30 | elementsOfUnitedNumberString 31 | ) 32 | 33 | export const unitPartOfUnitedNumberString = compose( 34 | view(lensIndex(1)), 35 | elementsOfUnitedNumberString 36 | ) 37 | 38 | export const tokenName = pipe(splitOnColon, last) 39 | 40 | export const addEmValues = curry((emValue1, emValue2) => { 41 | const value1 = unless(equals(0), numericPartOfUnitedNumberString)(emValue1) 42 | const value2 = unless(equals(0), numericPartOfUnitedNumberString)(emValue2) 43 | const result = clampPositive(value1 + value2) 44 | return when(gt(__, 0), v => `${v}em`, result) 45 | }) 46 | -------------------------------------------------------------------------------- /src/utils/predicate.js: -------------------------------------------------------------------------------- 1 | import { 2 | anyPass, 3 | both, 4 | complement, 5 | compose, 6 | contains, 7 | curry, 8 | either, 9 | equals, 10 | flip, 11 | gt, 12 | has, 13 | indexOf, 14 | length, 15 | pipe, 16 | test, 17 | unless, 18 | values, 19 | __, 20 | } from 'ramda' 21 | import { 22 | ensureArray, 23 | isArray, 24 | isNotArray, 25 | isNotString, 26 | isPositive, 27 | isString, 28 | isValidNumber, 29 | } from 'ramda-adjunct' 30 | import { 31 | AT_MODIFIER, 32 | DEFAULT_BREAKPOINT_NAME, 33 | GT_MODIFIER, 34 | LT_MODIFIER, 35 | NEGATIVE_OFFSET, 36 | POSITIVE_OFFSET, 37 | } from '../const/breakpoints' 38 | import CONFIG_FIELD_NAMES from '../const/config' 39 | import { 40 | RE_CALC_FUNCTION, 41 | RE_CSS_FUNCTION, 42 | RE_LINEAR_GRADIENT, 43 | RE_MEDIA_QUERY_STRING, 44 | RE_MODIFIED_MQ, 45 | RE_PERCENT_NUMBER, 46 | RE_RADIAL_GRADIENT, 47 | RE_RHYTHM_UNITS, 48 | RE_TOKEN, 49 | RE_TRANSFORM_TRANSLATE_FUNCTION, 50 | RE_UNNESTED_COMMA, 51 | RE_UNNESTED_WHITESPACE, 52 | RE_URL, 53 | } from '../const/regexp' 54 | import { LENGTH_UNITS } from '../const/units' 55 | import { propModifier } from '../objects/rangeItem' 56 | import { joinWithPipe } from './formatting' 57 | 58 | const { SCOPES } = CONFIG_FIELD_NAMES 59 | 60 | // ----------------------------------------------------------------------------- 61 | // List 62 | // ----------------------------------------------------------------------------- 63 | 64 | export const isContained = flip(contains) 65 | 66 | export const isLengthGt = curry((l, v) => compose(gt(__, l), length)(v)) 67 | 68 | export const isLengthEqualTo = curry((l, v) => compose(equals(l), length)(v)) 69 | 70 | // ----------------------------------------------------------------------------- 71 | // Misc 72 | // ----------------------------------------------------------------------------- 73 | 74 | export const isSingletonArray = both(isArray, isLengthEqualTo(1)) 75 | 76 | export const isDefaultBreakpoint = equals(DEFAULT_BREAKPOINT_NAME) 77 | 78 | export const isNotDefaultBreakpoint = complement(isDefaultBreakpoint) 79 | 80 | export const isValidMqValue = anyPass([ 81 | isSingletonArray, 82 | isString, 83 | isValidNumber, 84 | ]) 85 | 86 | // ----------------------------------------------------------------------------- 87 | // Numeric 88 | // ----------------------------------------------------------------------------- 89 | 90 | export const isZero = equals(0) 91 | export const isNotZero = complement(isZero) 92 | 93 | // ----------------------------------------------------------------------------- 94 | // Types 95 | // ----------------------------------------------------------------------------- 96 | 97 | export const isNumberWithUnit = curry((units, value) => { 98 | const possibleUnits = compose(joinWithPipe, ensureArray)(units) 99 | const regExp = new RegExp(`^-?\\d+(?:.\\d+)?(?:${possibleUnits})$`) 100 | return test(regExp, value) 101 | }) 102 | 103 | export const isEmString = isNumberWithUnit([LENGTH_UNITS.EM]) 104 | 105 | /* eslint-disable-next-line no-restricted-globals */ 106 | export const isNumberString = both(isString, complement(isNaN)) 107 | 108 | export const isNotZeroString = complement(equals)(`0`) 109 | 110 | export const isPercentString = pipe( 111 | unless(isString, String), 112 | test(RE_PERCENT_NUMBER) 113 | ) 114 | 115 | export const isValidNonZeroNumber = pipe( 116 | unless(isString, String), 117 | both(isNumberString, isNotZeroString) 118 | ) 119 | 120 | export const isValidPositiveNumber = both(isValidNumber, isPositive) 121 | 122 | export const isLength = isNumberWithUnit(values(LENGTH_UNITS)) 123 | 124 | export const isRhythmUnit = test(RE_RHYTHM_UNITS) 125 | 126 | export const isNotStringOrArray = both(isNotString, isNotArray) 127 | 128 | export const isUrl = test(RE_URL) 129 | 130 | export const isLinearGradient = test(RE_LINEAR_GRADIENT) 131 | 132 | export const isRadialGradient = test(RE_RADIAL_GRADIENT) 133 | 134 | export const isGradient = either(isLinearGradient, isRadialGradient) 135 | 136 | export const isToken = test(RE_TOKEN) 137 | 138 | export const isCSSFunction = test(RE_CSS_FUNCTION) 139 | 140 | export const isTransformTranslateFunction = test( 141 | RE_TRANSFORM_TRANSLATE_FUNCTION 142 | ) 143 | 144 | export const isCalcFunction = test(RE_CALC_FUNCTION) 145 | 146 | export const tokenMatches = names => value => { 147 | const possibleNames = compose(joinWithPipe, ensureArray)(names) 148 | const regExp = new RegExp(`^(${possibleNames}):(.*)$`) 149 | return test(regExp, value) 150 | } 151 | 152 | export const isMediaQueryString = test(RE_MEDIA_QUERY_STRING) 153 | 154 | export const isGroup = test(RE_UNNESTED_COMMA) 155 | 156 | export const isUnitRemOrEm = isContained([LENGTH_UNITS.EM, LENGTH_UNITS.REM]) 157 | 158 | export const hasScopes = has(SCOPES) 159 | 160 | export const hasNoScopes = complement(hasScopes) 161 | 162 | export const hasUnnestedWhitespace = test(RE_UNNESTED_WHITESPACE) 163 | 164 | export const isValidModifiedMq = test(RE_MODIFIED_MQ) 165 | 166 | export const modifierIs = regExp => pipe(propModifier, equals(regExp)) 167 | 168 | export const modifierIsLtModifier = modifierIs(LT_MODIFIER) 169 | 170 | export const modifierIsGtModifier = modifierIs(GT_MODIFIER) 171 | 172 | export const modifierIsAtModifier = modifierIs(AT_MODIFIER) 173 | 174 | export const hasPositiveOffset = contains(POSITIVE_OFFSET) 175 | 176 | export const hasNegativeOffset = contains(NEGATIVE_OFFSET) 177 | 178 | export const isGroupString = both(isString, isGroup) 179 | 180 | export const isRange = value => indexOf(LT_MODIFIER, value) > 1 181 | -------------------------------------------------------------------------------- /src/utils/queryDescriptor.js: -------------------------------------------------------------------------------- 1 | import { head } from 'ramda' 2 | import createRangedQueryDescriptor from '../breakpoints/descriptors/createRangedQueryDescriptor' 3 | import createUnrangedQueryDescrptor from '../breakpoints/descriptors/createUnrangedQueryDescrptor' 4 | import { propQuery } from '../objects/breakpointMapping' 5 | import createQueryDescriptor from '../objects/queryDescriptor' 6 | import { lengthEq1 } from '../utils/list' 7 | import { applyOffsetToBreakpointValue } from '../utils/range' 8 | 9 | // ----------------------------------------------------------------------------- 10 | // Query Header 11 | // ----------------------------------------------------------------------------- 12 | 13 | // Use an array of mappings to decide which header to render 14 | export const calculateQueryDescriptor = (idx, mappedValues) => { 15 | const queryValue = propQuery(mappedValues[idx]) 16 | let nextQueryValue 17 | if (idx < mappedValues.length - 1) { 18 | nextQueryValue = propQuery(mappedValues[idx + 1]) 19 | } 20 | return createQueryDescriptor({ from: queryValue, to: nextQueryValue }) 21 | } 22 | 23 | export const calculateQueryDescriptorForRange = breakpointMap => range => { 24 | const firstRangeItem = head(range) 25 | const firstItemValue = applyOffsetToBreakpointValue(firstRangeItem) 26 | const renderQuery = lengthEq1(range) 27 | ? createUnrangedQueryDescrptor(firstRangeItem) 28 | : createRangedQueryDescriptor 29 | return renderQuery(breakpointMap, firstRangeItem, firstItemValue, range) 30 | } 31 | -------------------------------------------------------------------------------- /src/utils/range.js: -------------------------------------------------------------------------------- 1 | import { 2 | any, 3 | cond, 4 | equals, 5 | head, 6 | isNil, 7 | of, 8 | pipe, 9 | reject, 10 | split, 11 | T, 12 | tail, 13 | unless, 14 | } from 'ramda' 15 | import { 16 | MODIFIERS, 17 | NEGATIVE_OFFSET, 18 | POSITIVE_OFFSET, 19 | } from '../const/breakpoints' 20 | import { propValue } from '../objects/breakpointMapping' 21 | import { propOffset } from '../objects/rangeItem' 22 | import rootPxToEmTransformer from '../transformers/rootPxToEmTransformer' 23 | import { addEmValues } from './parse' 24 | import { hasNegativeOffset, hasPositiveOffset, isEmString } from './predicate' 25 | 26 | const extractPositiveOffset = split(POSITIVE_OFFSET) 27 | 28 | const extractNegativeOffset = v => { 29 | const [name, offset] = split(NEGATIVE_OFFSET, v) 30 | return [name, `-${offset}`] 31 | } 32 | 33 | const extractOffset = cond([ 34 | [hasPositiveOffset, extractPositiveOffset], 35 | [hasNegativeOffset, extractNegativeOffset], 36 | [T, of], 37 | ]) 38 | 39 | const firstCharIsModifier = value => any(equals(value[0]), MODIFIERS) 40 | 41 | // ----------------------------------------------------------------------------- 42 | // Exports 43 | // ----------------------------------------------------------------------------- 44 | 45 | export const createRangeItem = value => { 46 | const modifier = firstCharIsModifier(value) ? head(value) : null 47 | if (modifier) value = tail(value) 48 | 49 | const [name, offset] = extractOffset(value) 50 | 51 | const result = { 52 | name, 53 | offset, 54 | modifier, 55 | } 56 | 57 | return reject(isNil, result) 58 | } 59 | 60 | export const hasNoOffset = pipe(propOffset, isNil) 61 | 62 | export const applyOffset = rangeItem => 63 | pipe( 64 | propOffset, 65 | unless(isEmString, rootPxToEmTransformer), 66 | addEmValues(rangeItem.value) 67 | ) 68 | 69 | export const applyOffsetToBreakpointValue = rangeItem => 70 | cond([[hasNoOffset, propValue], [T, applyOffset(rangeItem)]])(rangeItem) 71 | -------------------------------------------------------------------------------- /src/utils/scope.js: -------------------------------------------------------------------------------- 1 | import { append, flatten, last, pipe, zip } from 'ramda' 2 | import { createScope } from '../objects/scope' 3 | import { joinWithNoSpace } from './formatting' 4 | 5 | // Note: When called as a template-literal, the argument to a function will be 6 | // wrapped in an array, so we need to extract it using head. 7 | // eslint-disable-next-line import/prefer-default-export 8 | export const scope = (strings, ...values) => 9 | pipe(zip, append(last(strings)), flatten, joinWithNoSpace, createScope)( 10 | strings, 11 | values 12 | ) 13 | -------------------------------------------------------------------------------- /src/utils/templates.js: -------------------------------------------------------------------------------- 1 | import { curry, flip, pipe, subtract } from 'ramda' 2 | import { 3 | CSS_FUNCTION_TEMPLATE, 4 | DECLARATION_TEMPLATE, 5 | QUERY_MAX_TEMPLATE, 6 | QUERY_MIN_MAX_TEMPLATE, 7 | QUERY_MIN_TEMPLATE, 8 | QUERY_TEMPLATE, 9 | } from '../const/templates' 10 | import { adjustNumberWithUnit } from './converters' 11 | import { replaceToken, replaceTokens } from './formatting' 12 | 13 | const subtractMinimumEm = flip(subtract)(0.01) 14 | 15 | const reduceMaxWidthValue = adjustNumberWithUnit(subtractMinimumEm) 16 | 17 | export const createQueryMinHeaderFromTemplate = replaceToken( 18 | QUERY_MIN_TEMPLATE, 19 | `minWidth` 20 | ) 21 | 22 | export const createQueryMaxHeaderFromTemplate = pipe( 23 | reduceMaxWidthValue, 24 | replaceToken(QUERY_MAX_TEMPLATE, `maxWidth`) 25 | ) 26 | 27 | export const createQueryMinMaxHeaderFromTemplate = curry((maxWidth, minWidth) => 28 | replaceTokens(QUERY_MIN_MAX_TEMPLATE, { 29 | minWidth, 30 | maxWidth: reduceMaxWidthValue(maxWidth), 31 | }) 32 | ) 33 | 34 | export const createQueryFromTemplate = replaceTokens(QUERY_TEMPLATE) 35 | 36 | export const createDeclarationFromTemplate = replaceTokens(DECLARATION_TEMPLATE) 37 | 38 | export const createCSSFunctionFromTemplate = typeOfFunction => value => 39 | replaceTokens(CSS_FUNCTION_TEMPLATE, { 40 | typeOfFunction, 41 | value, 42 | }) 43 | -------------------------------------------------------------------------------- /src/utils/transformers.js: -------------------------------------------------------------------------------- 1 | import { 2 | adjust, 3 | curry, 4 | flatten, 5 | ifElse, 6 | map, 7 | match, 8 | nth, 9 | pipe, 10 | reduce, 11 | replace, 12 | trim, 13 | when, 14 | } from 'ramda' 15 | import { ensureArray, isString } from 'ramda-adjunct' 16 | import { RE_CALC_VALUES, RE_CSS_FUNCTION_NAME } from '../const/regexp' 17 | import { 18 | extractFunctionArguments, 19 | joinWithCommaSpace, 20 | joinWithSpace, 21 | splitOnUnnestedComma, 22 | splitOnUnnestedWhitespace, 23 | splitOnWhitespace, 24 | trimAll, 25 | } from './formatting' 26 | import { isGroupString, isNotStringOrArray } from './predicate' 27 | import { createCSSFunctionFromTemplate } from './templates' 28 | 29 | export const prepareForTransform = pipe( 30 | when(isNotStringOrArray, String), 31 | when(isString, splitOnUnnestedWhitespace) 32 | ) 33 | 34 | // ----------------------------------------------------------------------------- 35 | // CSS Function name() 36 | // ----------------------------------------------------------------------------- 37 | 38 | const transformArgumentPart = transformers => 39 | pipe( 40 | trim, 41 | splitOnWhitespace, 42 | map(pipe(ensureArray, transformers)), 43 | joinWithSpace 44 | ) 45 | 46 | const transformArgumentParts = transformers => 47 | map(transformArgumentPart(transformers)) 48 | 49 | const transformFunctionArguments = transformers => 50 | pipe( 51 | splitOnUnnestedComma, 52 | transformArgumentParts(transformers), 53 | joinWithCommaSpace 54 | ) 55 | 56 | const typeOfFunction = pipe(match(RE_CSS_FUNCTION_NAME), nth(1)) 57 | 58 | export const transformFunctionElements = curry((transformers, value) => 59 | pipe( 60 | trim, 61 | extractFunctionArguments, 62 | transformFunctionArguments(transformers), 63 | createCSSFunctionFromTemplate(typeOfFunction(value)) 64 | )(value) 65 | ) 66 | 67 | export const transformCalcElements = transformers => 68 | pipe( 69 | trim, 70 | extractFunctionArguments, 71 | replace(RE_CALC_VALUES, transformers), 72 | createCSSFunctionFromTemplate(`calc`) 73 | ) 74 | 75 | // ----------------------------------------------------------------------------- 76 | // Transform Basic Value - after we have parsed to smallest possible value 77 | // ----------------------------------------------------------------------------- 78 | 79 | const prepareTransformers = pipe(ensureArray, flatten) 80 | 81 | const applyTransformers = (value, data, breakpointName) => 82 | reduce( 83 | (currentValue, transformer) => 84 | transformer(currentValue, data, breakpointName), 85 | value 86 | ) 87 | 88 | export const transformValue = curry( 89 | (transformers, data, breakpointName, value) => 90 | pipe(prepareTransformers, applyTransformers(value, data, breakpointName))( 91 | transformers 92 | ) 93 | ) 94 | 95 | export const transformValues = curry( 96 | (transformers, data, breakpointName, values) => 97 | pipe( 98 | prepareForTransform, 99 | map(transformValue(transformers, data, breakpointName)) 100 | )(values) 101 | ) 102 | 103 | export const transformValueAt = curry( 104 | (transformers, data, breakpointName, position, value) => 105 | pipe( 106 | prepareForTransform, 107 | adjust(transformValues(transformers, data, breakpointName), position) 108 | )(value) 109 | ) 110 | 111 | // ----------------------------------------------------------------------------- 112 | // Transform Group (values, separated, with, comma) 113 | // ----------------------------------------------------------------------------- 114 | 115 | const transformGroupMember = (transformers, data, breakpointName) => 116 | pipe( 117 | splitOnUnnestedWhitespace, 118 | transformValues(transformers, data, breakpointName), 119 | joinWithSpace 120 | ) 121 | 122 | const transformGroupMembers = (transformers, data, breakpointName) => 123 | map(transformGroupMember(transformers, data, breakpointName)) 124 | 125 | export const transformGroup = (transformers, data, breakpointName) => 126 | pipe( 127 | splitOnUnnestedComma, 128 | trimAll, 129 | transformGroupMembers(transformers, data, breakpointName), 130 | joinWithCommaSpace 131 | ) 132 | 133 | export const transformDeclarationValue = (transformers, breakpointName, data) => 134 | ifElse( 135 | isGroupString, 136 | transformGroup(transformers, data, breakpointName), 137 | transformValue(transformers, data, breakpointName) 138 | ) 139 | --------------------------------------------------------------------------------