├── .editorconfig ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── index.js ├── package.json └── tests ├── font-size ├── input.css └── output.css └── padding ├── input.css └── output.css /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{json,yml}] 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | npm-debug.log 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .editorconfig 3 | 4 | node_modules/ 5 | npm-debug.log 6 | 7 | *.test.js 8 | .travis.yml 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sedlukha/postcss-interpolate/c58cac6b105e7b83ab6613ae499e3d238fd34005/CHANGELOG.md -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright 2016 Artur Sedlukha 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostCSS-Interpolate 2 | 3 | >[PostCSS] plugin for values interpolation between breakpoints. 4 | 5 | [PostCSS]: https://github.com/postcss/postcss 6 | 7 | This plugin made for automatically interpolation of property values between breakpoints. You can specify as much breakpoints, as you need. 8 | 9 | Use this plugin for any __px/rem__ value interpolation (font-size, padding, margin and other). It __doesn't__ work with __%__ and __em__. 10 | 11 | ![](https://media.giphy.com/media/3og0IQyIEtGJYrCPNm/giphy.gif) 12 | 13 | Inspired by this [draft]. 14 | 15 | [draft]: https://github.com/w3c/csswg-drafts/issues/581 16 | 17 | ## Installation 18 | 19 | ``` 20 | $ npm i --save-dev postcss-interpolate 21 | ``` 22 | 23 | ## Syntax 24 | 25 | `interpolate(direction, mediaquery-1, value-1, ... mediaquery-n, value-n)` 26 | 27 | `direction` 28 | * __none__ — if you will not specify direction, plugin will you __vertically__ as defaul direction 29 | * __vertically__ or __vw__ — default derection. 30 | * __horizontally__ or __vh__ 31 | 32 | 33 | `mediaquery` 34 | works only with **px** units 35 | 36 | `value` 37 | works only with **px** or **rem** units 38 | 39 | 40 | ## Examples 41 | More examples at the tests folder. 42 | 43 | ![](https://media.giphy.com/media/3og0IOnvXx4GLSkG40/giphy.gif) 44 | ```css 45 | /* Input */ 46 | .foo { 47 | font-size: interpolate(320px, 10px, 600px, 40px, 1200px, 10px); 48 | } 49 | 50 | /* Output */ 51 | .foo { 52 | font-size: 10px; 53 | } 54 | @media screen and (min-width: 320px) { 55 | .foo { 56 | font-size: calc( 10px + 30 * (100vw - 320px) / 280); 57 | } 58 | } 59 | @media screen and (min-width: 600px) { 60 | .foo { 61 | font-size: calc( 40px + -30 * (100vw - 600px) / 600); 62 | } 63 | } 64 | @media screen and (min-width: 1200px) { 65 | .foo { 66 | font-size: 10px; 67 | } 68 | } 69 | ``` 70 | 71 | Padding example 72 | 73 | ![](https://media.giphy.com/media/3og0IKBuh7cKgaOxc4/giphy.gif) 74 | ```css 75 | /* Input */ 76 | .foo { 77 | padding-top: interpolate(320px, 5px, 600px, 80px, 1200px, 5px); 78 | } 79 | 80 | /* Output */ 81 | .foo { 82 | padding-top: 5px; 83 | } 84 | @media screen and (min-width: 320px) { 85 | .foo { 86 | padding-top: calc( 5px + 75 * (100vw - 320px) / 280); 87 | } 88 | } 89 | @media screen and (min-width: 600px) { 90 | .foo { 91 | padding-top: calc( 80px + -75 * (100vw - 600px) / 600); 92 | } 93 | } 94 | @media screen and (min-width: 1200px) { 95 | .foo { 96 | padding-top: 5px; 97 | } 98 | } 99 | ``` 100 | 101 | ## Usage 102 | 103 | ```js 104 | postcss([ require('postcss-interpolate') ]) 105 | ``` 106 | 107 | If you are using **postcss-cssnext**, please, turn off pxrem plugin 108 | ```js 109 | postcssInterpolate(), 110 | postcssCssnext({ 111 | features: { 112 | rem: false, 113 | } 114 | }) 115 | ``` 116 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var postcss = require('postcss'); 2 | 3 | module.exports = postcss.plugin('postcss-interpolate', () => { 4 | return css => { 5 | 6 | // Html font-size value 7 | var rootString; 8 | var startFrom; 9 | var directionViewport; 10 | var minDirection; 11 | var defaultRem = true; 12 | var defaultRemValue = '16px'; // default value of 1rem 13 | 14 | css.walkRules(rule => { 15 | if (rule.selector.indexOf('html') > -1) { 16 | rule.walkDecls(decl => { 17 | if (decl.prop.indexOf('font-size') > -1) { 18 | rootString = decl.value; 19 | defaultRem = false; 20 | } 21 | }); 22 | } 23 | if (defaultRem) { 24 | rootString = defaultRemValue; 25 | } 26 | }); 27 | 28 | function isString(array) { 29 | return isNaN(parseFloat(array)); 30 | } 31 | 32 | function isOdd(array) { 33 | return (array.length & 1); 34 | } 35 | 36 | function isPx(i) { 37 | return i.indexOf('px') > -1; 38 | } 39 | 40 | function isRem(i) { 41 | return i.indexOf('rem') > -1; 42 | } 43 | 44 | // Get html font-size value 45 | function getHtmlValue(actualViewport, mediaArray, valueArray, decl) { 46 | actualViewport = parseFloat(actualViewport); 47 | 48 | if ((mediaArray.every(isPx) && valueArray.every(isPx)) ) { 49 | return 1; 50 | } else if (mediaArray.every(isPx) && valueArray.every(isRem)) { 51 | var rootMediaArray = []; 52 | var rootValueArray = []; 53 | var rootReadyArray; 54 | var rootStartFrom; 55 | 56 | if (rootString.indexOf('interpolate(') > -1) { 57 | rootReadyArray = rootString.match(/\(([^)]+)\)/)[1].split(','); 58 | 59 | if (!isOdd(rootReadyArray) && !isString(rootReadyArray[0])) { 60 | rootStartFrom = 0 61 | } else if (isOdd(rootReadyArray) && isString(rootReadyArray[0])) { 62 | rootStartFrom = 1; 63 | } else { 64 | throw decl.error('Deprecated value syntax', { 65 | word: decl.value 66 | }); 67 | } 68 | 69 | // Get array of mediaqueries and array of values 70 | for (var i = rootStartFrom; i < rootReadyArray.length; i += 2) { 71 | rootMediaArray.push(rootReadyArray[i]); 72 | rootValueArray.push(rootReadyArray[i + 1]); 73 | } 74 | 75 | var minrootMediaArray, 76 | maxrootMediaArray, 77 | minrootValueArray, 78 | maxrootValueArray; 79 | 80 | for (var i = 0; i < rootMediaArray.length; i++) { 81 | if ((actualViewport <= parseFloat(rootMediaArray[i])) && (actualViewport >= parseFloat(rootMediaArray[i - 1]))) { 82 | maxrootMediaArray = parseFloat(rootMediaArray[i]); 83 | minrootMediaArray = parseFloat(rootMediaArray[i - 1]); 84 | maxrootValueArray = parseFloat(rootValueArray[i]); 85 | minrootValueArray = parseFloat(rootValueArray[i - 1]); 86 | } 87 | } 88 | 89 | var formula; 90 | 91 | if (minrootMediaArray === maxrootMediaArray) { 92 | formula = parseFloat(minrootValueArray); 93 | } else { 94 | formula = minrootValueArray + (maxrootValueArray - minrootValueArray) * (actualViewport - minrootMediaArray) / (maxrootMediaArray - minrootMediaArray); 95 | } 96 | 97 | return formula; 98 | 99 | } else if (rootString.indexOf('px') > -1) { 100 | return parseFloat(rootString); 101 | } 102 | } 103 | else { 104 | throw decl.error( 105 | 'This combination of units is not supported', { 106 | word: decl.value 107 | }); 108 | } 109 | } 110 | 111 | // Generate media and values arrays 112 | function interpolate(interpolateArray, decl) { 113 | const rule = decl.parent; 114 | const root = rule.parent; 115 | var mediaArray = []; 116 | var valueArray = []; 117 | 118 | if (!isOdd(interpolateArray) && !isString(interpolateArray[0])) { 119 | directionViewport = 'vw'; 120 | minDirection = 'width'; 121 | startFrom = 0; 122 | } else if (isOdd(interpolateArray) && isString(interpolateArray[0])) { 123 | var direction = interpolateArray[0]; 124 | 125 | if ((direction.indexOf('horizontally') > -1) || (direction.indexOf('vw') > -1)) { 126 | directionViewport = 'vw'; 127 | minDirection = 'width'; 128 | } else if ((direction.indexOf('vertically') > -1) || (direction.indexOf('vh') > -1)) { 129 | directionViewport = 'vh'; 130 | minDirection = 'height'; 131 | } else { 132 | throw decl.error('Deprecated direction property', { 133 | word: interpolateArray[0] 134 | }); 135 | } 136 | startFrom = 1; 137 | } else { 138 | // Syntax error 139 | throw decl.error('Deprecated value syntax', { 140 | word: decl.value 141 | }); 142 | } 143 | 144 | for (var i = startFrom; i < interpolateArray.length; i += 2) { 145 | mediaArray.push(interpolateArray[i]); 146 | valueArray.push(interpolateArray[i + 1]); 147 | } 148 | 149 | // First @media rule 150 | decl.replaceWith({ 151 | prop: decl.prop, 152 | value: interpolateArray[startFrom + 1] 153 | }); 154 | 155 | // Middle @media rule 156 | for (var i = 0; i < mediaArray.length - 1; i++) { 157 | for (var i = 0; i < valueArray.length - 1; i++) { 158 | 159 | // Generate @media atrule 160 | var mediaAtrule = postcss.atRule({ 161 | name: 'media', 162 | params: 'screen and (min-' + minDirection + ': ' + mediaArray[i] + ')' 163 | }); 164 | 165 | var maxMedia = mediaArray[i + 1]; 166 | var minMedia = mediaArray[i]; 167 | var maxValue = valueArray[i + 1]; 168 | var minValue = valueArray[i]; 169 | 170 | // Append current selector with properties and values 171 | mediaAtrule.append({ 172 | selector: rule.selector 173 | }).walkRules( function (selector) { 174 | selector.append({ 175 | prop: decl.prop, 176 | value: 'calc(' + minValue + ' + ' + (parseFloat(maxValue) - parseFloat(minValue)) + ' * (100' + directionViewport + ' - ' + minMedia + ') / ' + ((parseFloat(maxMedia) - parseFloat(minMedia)) / getHtmlValue(maxMedia, mediaArray, valueArray, decl)) + ')' 177 | }); 178 | }); 179 | 180 | // Insert middle mediaquery 181 | root.insertAfter(root.last, mediaAtrule); 182 | } 183 | } 184 | 185 | // Last @media rule 186 | var lastMedia = postcss.atRule({ 187 | name: 'media', 188 | params: 'screen and (min-' + minDirection + ': ' + mediaArray[mediaArray.length - 1] + ')' 189 | }); 190 | 191 | // Append current selector with properties and values 192 | lastMedia.append({ 193 | selector: rule.selector 194 | }).walkRules( function (selector) { 195 | selector.append({ 196 | prop: decl.prop, 197 | value: valueArray[valueArray.length - 1] 198 | }); 199 | }); 200 | 201 | // Insert last media rule 202 | root.insertAfter(root.last, lastMedia); 203 | 204 | return; 205 | } 206 | 207 | css.walkDecls(decl => { 208 | 209 | // Get property line 210 | if (decl.value.indexOf('interpolate(') !== -1) { 211 | 212 | // Get array of interolate() values 213 | const getInterpolateArray = decl.value.match(/\(([^)]+)\)/)[1]; 214 | const interpolateArray = getInterpolateArray.split(','); 215 | 216 | if (interpolateArray.length > 3) { 217 | 218 | // Get array of mediaqueries and array of values 219 | interpolate(interpolateArray, decl); 220 | 221 | } else { 222 | throw decl.error('Deprecated property amount', { 223 | word: decl.value 224 | }); 225 | } 226 | } 227 | }); 228 | }; 229 | }); 230 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "postcss-interpolate", 3 | "version": "1.1.4", 4 | "description": "PostCSS plugin to interpolate everything", 5 | "keywords": [ 6 | "postcss", 7 | "css", 8 | "postcss-plugin", 9 | "interpolate" 10 | ], 11 | "author": "Artur Sedlukha ", 12 | "license": "MIT", 13 | "repository": "sedlukha/postcss-interpolate", 14 | "bugs": { 15 | "url": "https://github.com/sedlukha/postcss-interpolate/issues" 16 | }, 17 | "homepage": "https://github.com/sedlukha/postcss-interpolate", 18 | "dependencies": { 19 | "postcss": "^5.2.6" 20 | }, 21 | "devDependencies": { 22 | "eslint": "^3.12.2", 23 | "eslint-config-postcss": "^2.0.2", 24 | "jest": "^18.0.0" 25 | }, 26 | "scripts": { 27 | "test": "jest && eslint *.js" 28 | }, 29 | "eslintConfig": { 30 | "extends": "eslint-config-postcss/es5", 31 | "env": { 32 | "jest": true 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/font-size/input.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | font-size: interpolate(320px, 10px, 600px, 15px, 1200px, 40px); 3 | } 4 | -------------------------------------------------------------------------------- /tests/font-size/output.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | font-size: 10px; 3 | } 4 | @media screen and (min-width: 320px) { 5 | .foo { 6 | font-size: calc( 10px + 5 * (100vw - 320px) / 280); 7 | } 8 | } 9 | @media screen and (min-width: 600px) { 10 | .foo { 11 | font-size: calc( 15px + 25 * (100vw - 600px) / 600); 12 | } 13 | } 14 | @media screen and (min-width: 1200px) { 15 | .foo { 16 | font-size: 40px; 17 | } 18 | } 19 | 20 | 21 | .foo { 22 | margin: 0; 23 | } 24 | -------------------------------------------------------------------------------- /tests/padding/input.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | padding-top: interpolate(320px, 10px, 1200px, 40px); 3 | padding-bottom: interpolate(320px, 20px, 1200px, 50px); 4 | } 5 | -------------------------------------------------------------------------------- /tests/padding/output.css: -------------------------------------------------------------------------------- 1 | .foo { 2 | padding-top: 10px; 3 | padding-bottom: 20px; 4 | } 5 | @media screen and (min-width: 320px) { 6 | .foo { 7 | padding-top: calc( 10px + 30 * (100vw - 320px) / 880); 8 | } 9 | } 10 | @media screen and (min-width: 1200px) { 11 | .foo { 12 | padding-top: 40px; 13 | } 14 | } 15 | @media screen and (min-width: 320px) { 16 | .foo { 17 | padding-bottom: calc( 20px + 30 * (100vw - 320px) / 880); 18 | } 19 | } 20 | @media screen and (min-width: 1200px) { 21 | .foo { 22 | padding-bottom: 50px; 23 | } 24 | } 25 | --------------------------------------------------------------------------------