├── .babelrc ├── .editorconfig ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .node-version ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── parser.jison └── src ├── __tests__ └── index.js ├── index.js └── lib ├── convert.js ├── reducer.js └── stringifier.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [["env", { 3 | "targets": { 4 | "node": 4, 5 | "browsers": ["last 2 versions"] 6 | } 7 | }]], 8 | "plugins": ["add-module-exports"], 9 | "env": { 10 | "development": { 11 | "sourceMaps": "inline" 12 | }, 13 | "test": { 14 | "plugins": ["istanbul"] 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | indent_style = space 10 | indent_size = 2 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - uses: actions/setup-node@v2 11 | with: 12 | node-version-file: ".node-version" 13 | 14 | - uses: actions/cache@v2 15 | with: 16 | path: ~/.npm 17 | key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} 18 | restore-keys: | 19 | ${{ runner.os }}-node- 20 | 21 | - run: npm ci 22 | 23 | - run: npm test 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog of `reduce-css-call` 2 | 3 | ## 2.1.8 - 2020-01-08 4 | 5 | - Fix Parse error on custom property fallback ([#68](https://github.com/MoOx/reduce-css-calc/pull/68)) - @snowystinger) 6 | 7 | ## 2.1.7 - 2019-10-22 8 | 9 | - Switch to a maintained jison fork ([#57](https://github.com/MoOx/reduce-css-calc/pull/57)) - @davidgovea) 10 | 11 | ## 2.1.6 - 2019-01-11 12 | 13 | - Fixed: Incorrect calculation when subtracting (e.g. `calc(100% - calc(120px + 1em + 2em + 100px))`) ([#52](https://github.com/MoOx/reduce-css-calc/pull/53) - @sylvainpolletvillard) 14 | 15 | ## 2.1.5 - 2018-09-20 16 | 17 | - [Avoid breaking when seeing ` constant()`` or `env()`](https://github.com/MoOx/reduce-css-calc/commit/409c9ba2cd5e06e7f8f679f7f0c3c3a14ff3e673) by @dlee 18 | 19 | ## 2.1.4 - 2018-01-22 20 | 21 | - Prevent webpack parsing issue 22 | (see https://github.com/zaach/jison/pull/352) 23 | 24 | ## 2.1.3 - 2017-11-27 25 | 26 | - Fixed: Incorrect reduction for a specific case (e.g. `calc(1em + (1em - 5px))`) ([#43](https://github.com/MoOx/reduce-css-calc/pull/43) - @Justineo) 27 | 28 | ## 2.1.2 - 2017-11-26 29 | 30 | - Fixed: Incorrect reduction of division with custom property (e.g. `calc(var(--foo) / 2)`) ([#41](https://github.com/MoOx/reduce-css-calc/issues/41) - @Semigradsky) 31 | 32 | ## 2.1.1 - 2017-10-12 33 | 34 | - Fixed: Incorrect reduction of nested expression (e.g. `calc( (1em - calc( 10px + 1em)) / 2)`) ([#39](https://github.com/MoOx/reduce-css-calc/pull/39) - @gyoshev) 35 | 36 | ## 2.1.0 - 2017-10-10 37 | 38 | - Added: Support for working in browsers without transpiling ([#36](https://github.com/MoOx/reduce-css-calc/pull/36) - @Semigradsky) 39 | - Fixed: `calc(100vw - (100vw - 100%))` does not evaluate to `100%` ([#35](https://github.com/MoOx/reduce-css-calc/pull/35) - @Semigradsky) 40 | 41 | ## 2.0.5 - 2017-05-12 42 | 43 | - Fixed: Support division with a CSS variable. 44 | 45 | ## 2.0.4 - 2017-05-09 46 | 47 | - Fixed: CSS variable regex was overly greedy and caused a crash in some 48 | cases. ([#27](https://github.com/MoOx/reduce-css-calc/pull/27) - @andyjansson) 49 | 50 | ## 2.0.3 - 2017-05-09 51 | 52 | - Fixed: Regression in handling decimals without having any numbers after 53 | the decimal place (e.g. `10.px`). 54 | 55 | ## 2.0.2 - 2017-05-08 56 | 57 | - Fixed: Regression in consecutive subtraction handling 58 | ([#25](https://github.com/MoOx/reduce-css-calc/pull/25) - @andyjansson) 59 | 60 | ## 2.0.1 - 2017-05-08 61 | 62 | - Fixed: Support for nested calc e.g. `calc(100% - calc(50px - 25px))`. 63 | - Fixed: Support for CSS variables e.g. `calc(var(--mouseX) * 1px)`. 64 | 65 | ## 2.0.0 - 2017-05-08 66 | 67 | - Rewritten with a jison parser for more accurate value parsing. 68 | - Breaking: reduce-css-calc will now throw when trying to multiply or divide 69 | by the same unit (e.g. `calc(200px * 20px)`), and also when trying to divide 70 | by zero. 71 | - Added: Better handling of zero values (e.g. `calc(100vw / 2 - 6px + 0px)` 72 | becomes `calc(100vw / 2 - 6px)`). 73 | - Added: Better handling of mixed time values (e.g. `calc(1s - 50ms)` 74 | becomes `0.95s`). 75 | - Added: Inner parentheses calculation to simplify complex expressions (e.g. 76 | `calc(14px + 6 * ((100vw - 320px) / 448))` becomes `calc(9.71px + 1.34vw)` 77 | with precision set to `2`). 78 | - Fixed: `calc(1px + 1)` does not evaluate to `2px`. 79 | 80 | ([#22](https://github.com/MoOx/reduce-css-calc/pull/22) - @andyjansson) 81 | 82 | ## 1.3.0 - 2016-08-26 83 | 84 | - Added: calc identifier from unresolved nested expressions are removed for 85 | better browser support 86 | ([#19](https://github.com/MoOx/reduce-css-calc/pull/19) - @ben-eb) 87 | 88 | ## 1.2.8 - 2016-08-26 89 | 90 | - Fixed: regression from 1.2.5 on calc() with value without leading 0 91 | ([#17](https://github.com/MoOx/reduce-css-calc/pull/17) - @ben-eb) 92 | 93 | ## 1.2.7 - 2016-08-22 94 | 95 | - Fixed: regression from 1.2.5 on calc() with value without leading 0 96 | (@MoOx) 97 | 98 | ## 1.2.6 - 2016-08-22 99 | 100 | - Fixed: regression from 1.2.5 on calc() on multiple lines 101 | (@MoOx) 102 | 103 | ## 1.2.5 - 2016-08-22 104 | 105 | - Fixed: security issue due to the usage of `eval()`. 106 | This is to avoid an arbitrary code execution. 107 | Now operations are resolved using 108 | [`math-expression-evaluator`](https://github.com/redhivesoftware/math-expression-evaluator) 109 | 110 | ## 1.2.4 - 2016-06-09 111 | 112 | - Fixed: zero values are not unitless anymore. 113 | Browsers do not calculate calc() with 0 unitless values. 114 | http://jsbin.com/punivivipo/edit?html,css,output 115 | ([#11](https://github.com/MoOx/reduce-css-calc/pull/11)) 116 | 117 | ## 1.2.3 - 2016-04-28 118 | 119 | - Fixed: wrong rouding in some edge cases 120 | ([#10](https://github.com/MoOx/reduce-css-calc/pull/10)) 121 | 122 | ## 1.2.2 - 2016-04-19 123 | 124 | - Fixed: Don't reduce expression containing CSS variables. 125 | ([#9](https://github.com/MoOx/reduce-css-calc/pull/9)) 126 | 127 | ## 1.2.1 - 2016-02-22 128 | 129 | - Fixed: uppercase letters in units are now supported 130 | ([#8](https://github.com/MoOx/reduce-css-calc/pull/8)) 131 | 132 | ## 1.2.0 - 2014-11-24 133 | 134 | - Decimal precision is now customisable as the `precision` option 135 | 136 | ## 1.1.4 - 2014-11-12 137 | 138 | - 5 decimals rounding for everything 139 | 140 | ## 1.1.3 - 2014-08-13 141 | 142 | - 5 decimals rounding for percentage 143 | 144 | ## 1.1.2 - 2014-08-10 145 | 146 | - Prevent infinite loop by adding a `Call stack overflow` 147 | - Correctly ignore unrecognized values (safer evaluation for nested expressions, 148 | see [postcss/postcss-calc#2](https://github.com/postcss/postcss-calc/issues/2)) 149 | - Handle rounding issues (eg: 10% \* 20% now give 2%, not 2.0000000000000004%) 150 | 151 | ## 1.1.1 - 2014-08-06 152 | 153 | - Fix issue when using mutiples differents prefixes in the same function 154 | 155 | ## 1.1.0 - 2014-08-06 156 | 157 | - support more complex formulas 158 | - use `reduce-function-call` 159 | - better error message 160 | 161 | ## 1.0.0 - 2014-08-04 162 | 163 | First release 164 | 165 | - based on [rework-calc](https://github.com/reworkcss/rework-calc) v1.1.0 166 | - add error if the calc() embed an empty calc() or empty () 167 | - jscs + jshint added before tests 168 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Maxime Thirouin & Joakim Bengtson 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 | # reduce-css-calc 2 | 3 | [![Build Status](https://github.com/MoOx/reduce-css-calc/workflows/Build/badge.svg)](https://github.com/MoOx/reduce-css-calc/actions) 4 | 5 | > Reduce CSS calc() function to the maximum. 6 | 7 | Particularly useful for packages like 8 | [rework-calc](https://github.com/reworkcss/rework-calc) or 9 | [postcss-calc](https://github.com/postcss/postcss-calc). 10 | 11 | ## Installation 12 | 13 | ```console 14 | npm install reduce-css-calc 15 | ``` 16 | 17 | ## Usage 18 | 19 | ### `var reducedString = reduceCSSCalc(string, precision)` 20 | 21 | ```javascript 22 | var reduceCSSCalc = require("reduce-css-calc"); 23 | 24 | reduceCSSCalc("calc(1 + 1)"); 25 | // 2 26 | 27 | reduceCSSCalc("calc((6 / 2) - (4 * 2) + 1)"); 28 | // -4 29 | 30 | reduceCSSCalc("calc(1/3)"); 31 | // 0.33333 32 | 33 | reduceCSSCalc("calc(1/3)", 10); 34 | // 0.3333333333 35 | 36 | reduceCSSCalc("calc(3rem * 2 - 1rem)"); 37 | // 5rem 38 | 39 | reduceCSSCalc("calc(2 * 50%)"); 40 | // 100% 41 | 42 | reduceCSSCalc("calc(120% * 50%)"); 43 | // 60% 44 | 45 | reduceCSSCalc("a calc(1 + 1) b calc(1 - 1) c"); 46 | // a 2 b 0 c 47 | 48 | reduceCSSCalc("calc(calc(calc(1rem * 0.75) * 1.5) - 1rem)"); 49 | // 0.125rem 50 | 51 | reduceCSSCalc("calc(calc(calc(1rem * 0.75) * 1.5) - 1px)"); 52 | // calc(1.125rem - 1px) 53 | 54 | reduceCSSCalc("-moz-calc(100px / 2)"); 55 | // 50px 56 | 57 | reduceCSSCalc("-moz-calc(50% - 2em)"); 58 | // -moz-calc(50% - 2em) 59 | ``` 60 | 61 | See [unit tests](src/__tests__/index.js) for others examples. 62 | 63 | --- 64 | 65 | ## Contributing 66 | 67 | Work on a branch, install dev-dependencies, respect coding style & run tests 68 | before submitting a bug fix or a feature. 69 | 70 | ```console 71 | git clone https://github.com/MoOx/reduce-css-calc.git 72 | git checkout -b patch-1 73 | npm install 74 | npm test 75 | ``` 76 | 77 | ## [Changelog](CHANGELOG.md) 78 | 79 | ## [License](LICENSE-MIT) 80 | 81 | ## Security contact information 82 | 83 | To report a security vulnerability, please use the 84 | [Tidelift security contact](https://tidelift.com/security). Tidelift will 85 | coordinate the fix and disclosure. 86 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reduce-css-calc", 3 | "version": "2.1.8", 4 | "description": "Reduce CSS calc() function to the maximum", 5 | "keywords": [ 6 | "css", 7 | "calculation", 8 | "calc" 9 | ], 10 | "main": "dist/index.js", 11 | "files": [ 12 | "dist" 13 | ], 14 | "scripts": { 15 | "prepublish": "npm run build && del-cli dist/__tests__", 16 | "build": "del-cli dist && cross-env BABEL_ENV=publish babel src --out-dir dist && jison parser.jison -o dist/parser.js", 17 | "pretest": "eslint src && npm run build", 18 | "test": "ava dist/__tests__/", 19 | "release": "npmpub" 20 | }, 21 | "author": "Maxime Thirouin", 22 | "license": "MIT", 23 | "repository": "https://github.com/MoOx/reduce-css-calc.git", 24 | "devDependencies": { 25 | "ava": "^0.18.2", 26 | "babel-cli": "^6.18.0", 27 | "babel-core": "^6.21.0", 28 | "babel-eslint": "^7.1.1", 29 | "babel-plugin-add-module-exports": "^0.2.1", 30 | "babel-preset-env": "^1.6.0", 31 | "babel-register": "^6.18.0", 32 | "cross-env": "^3.1.4", 33 | "del-cli": "^0.2.1", 34 | "eslint": "^3.12.2", 35 | "eslint-config-i-am-meticulous": "^6.0.1", 36 | "eslint-plugin-babel": "^4.0.0", 37 | "eslint-plugin-import": "^2.2.0", 38 | "jison-gho": "^0.6.1-216", 39 | "npmpub": "^5.0.0" 40 | }, 41 | "dependencies": { 42 | "css-unit-converter": "^1.1.1", 43 | "postcss-value-parser": "^3.3.0" 44 | }, 45 | "eslintConfig": { 46 | "parser": "babel-eslint", 47 | "extends": "eslint-config-i-am-meticulous" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /parser.jison: -------------------------------------------------------------------------------- 1 | /* description: Parses expressions. */ 2 | 3 | /* lexical grammar */ 4 | %lex 5 | %% 6 | (--[0-9a-z-A-Z-]*) return 'CSS_CPROP'; 7 | \s+ /* skip whitespace */ 8 | "*" return 'MUL'; 9 | "/" return 'DIV'; 10 | "+" return 'ADD'; 11 | "-" return 'SUB'; 12 | 13 | ([0-9]+("."[0-9]*)?|"."[0-9]+)px\b return 'LENGTH'; 14 | ([0-9]+("."[0-9]*)?|"."[0-9]+)cm\b return 'LENGTH'; 15 | ([0-9]+("."[0-9]*)?|"."[0-9]+)mm\b return 'LENGTH'; 16 | ([0-9]+("."[0-9]*)?|"."[0-9]+)in\b return 'LENGTH'; 17 | ([0-9]+("."[0-9]*)?|"."[0-9]+)pt\b return 'LENGTH'; 18 | ([0-9]+("."[0-9]*)?|"."[0-9]+)pc\b return 'LENGTH'; 19 | ([0-9]+("."[0-9]*)?|"."[0-9]+)deg\b return 'ANGLE'; 20 | ([0-9]+("."[0-9]*)?|"."[0-9]+)grad\b return 'ANGLE'; 21 | ([0-9]+("."[0-9]*)?|"."[0-9]+)rad\b return 'ANGLE'; 22 | ([0-9]+("."[0-9]*)?|"."[0-9]+)turn\b return 'ANGLE'; 23 | ([0-9]+("."[0-9]*)?|"."[0-9]+)s\b return 'TIME'; 24 | ([0-9]+("."[0-9]*)?|"."[0-9]+)ms\b return 'TIME'; 25 | ([0-9]+("."[0-9]*)?|"."[0-9]+)Hz\b return 'FREQ'; 26 | ([0-9]+("."[0-9]*)?|"."[0-9]+)kHz\b return 'FREQ'; 27 | ([0-9]+("."[0-9]*)?|"."[0-9]+)dpi\b return 'RES'; 28 | ([0-9]+("."[0-9]*)?|"."[0-9]+)dpcm\b return 'RES'; 29 | ([0-9]+("."[0-9]*)?|"."[0-9]+)dppx\b return 'RES'; 30 | ([0-9]+("."[0-9]*)?|"."[0-9]+)em\b return 'EMS'; 31 | ([0-9]+("."[0-9]*)?|"."[0-9]+)ex\b return 'EXS'; 32 | ([0-9]+("."[0-9]*)?|"."[0-9]+)ch\b return 'CHS'; 33 | ([0-9]+("."[0-9]*)?|"."[0-9]+)rem\b return 'REMS'; 34 | ([0-9]+("."[0-9]*)?|"."[0-9]+)vw\b return 'VWS'; 35 | ([0-9]+("."[0-9]*)?|"."[0-9]+)vh\b return 'VHS'; 36 | ([0-9]+("."[0-9]*)?|"."[0-9]+)vmin\b return 'VMINS'; 37 | ([0-9]+("."[0-9]*)?|"."[0-9]+)vmax\b return 'VMAXS'; 38 | ([0-9]+("."[0-9]*)?|"."[0-9]+)\% return 'PERCENTAGE'; 39 | ([0-9]+("."[0-9]*)?|"."[0-9]+)\b return 'NUMBER'; 40 | 41 | (calc) return 'NESTED_CALC'; 42 | (var) return 'CSS_VAR'; 43 | ([a-z]+) return 'PREFIX'; 44 | 45 | "(" return 'LPAREN'; 46 | ")" return 'RPAREN'; 47 | "," return 'COMMA'; 48 | 49 | <> return 'EOF'; 50 | 51 | /lex 52 | 53 | %left ADD SUB 54 | %left MUL DIV 55 | %left UPREC 56 | 57 | 58 | %start expression 59 | 60 | %% 61 | 62 | expression 63 | : math_expression EOF { return $1; } 64 | ; 65 | 66 | math_expression 67 | : math_expression ADD math_expression { $$ = { type: 'MathExpression', operator: $2, left: $1, right: $3 }; } 68 | | math_expression SUB math_expression { $$ = { type: 'MathExpression', operator: $2, left: $1, right: $3 }; } 69 | | math_expression MUL math_expression { $$ = { type: 'MathExpression', operator: $2, left: $1, right: $3 }; } 70 | | math_expression DIV math_expression { $$ = { type: 'MathExpression', operator: $2, left: $1, right: $3 }; } 71 | | LPAREN math_expression RPAREN { $$ = $2; } 72 | | NESTED_CALC LPAREN math_expression RPAREN { $$ = { type: 'Calc', value: $3 }; } 73 | | SUB PREFIX SUB NESTED_CALC LPAREN math_expression RPAREN { $$ = { type: 'Calc', value: $6, prefix: $2 }; } 74 | | css_variable { $$ = $1; } 75 | | css_value { $$ = $1; } 76 | | value { $$ = $1; } 77 | ; 78 | 79 | value 80 | : NUMBER { $$ = { type: 'Value', value: parseFloat($1) }; } 81 | | SUB NUMBER { $$ = { type: 'Value', value: parseFloat($2) * -1 }; } 82 | ; 83 | 84 | css_variable 85 | : CSS_VAR LPAREN CSS_CPROP RPAREN { $$ = { type: 'CssVariable', value: $3 }; } 86 | | CSS_VAR LPAREN CSS_CPROP COMMA math_expression RPAREN { $$ = { type: 'CssVariable', value: $3, fallback: $5 }; } 87 | ; 88 | 89 | css_value 90 | : LENGTH { $$ = { type: 'LengthValue', value: parseFloat($1), unit: /[a-z]+/.exec($1)[0] }; } 91 | | ANGLE { $$ = { type: 'AngleValue', value: parseFloat($1), unit: /[a-z]+/.exec($1)[0] }; } 92 | | TIME { $$ = { type: 'TimeValue', value: parseFloat($1), unit: /[a-z]+/.exec($1)[0] }; } 93 | | FREQ { $$ = { type: 'FrequencyValue', value: parseFloat($1), unit: /[a-z]+/.exec($1)[0] }; } 94 | | RES { $$ = { type: 'ResolutionValue', value: parseFloat($1), unit: /[a-z]+/.exec($1)[0] }; } 95 | | EMS { $$ = { type: 'EmValue', value: parseFloat($1), unit: 'em' }; } 96 | | EXS { $$ = { type: 'ExValue', value: parseFloat($1), unit: 'ex' }; } 97 | | CHS { $$ = { type: 'ChValue', value: parseFloat($1), unit: 'ch' }; } 98 | | REMS { $$ = { type: 'RemValue', value: parseFloat($1), unit: 'rem' }; } 99 | | VHS { $$ = { type: 'VhValue', value: parseFloat($1), unit: 'vh' }; } 100 | | VWS { $$ = { type: 'VwValue', value: parseFloat($1), unit: 'vw' }; } 101 | | VMINS { $$ = { type: 'VminValue', value: parseFloat($1), unit: 'vmin' }; } 102 | | VMAXS { $$ = { type: 'VmaxValue', value: parseFloat($1), unit: 'vmax' }; } 103 | | PERCENTAGE { $$ = { type: 'PercentageValue', value: parseFloat($1), unit: '%' }; } 104 | | SUB css_value { var prev = $2; prev.value *= -1; $$ = prev; } 105 | ; 106 | -------------------------------------------------------------------------------- /src/__tests__/index.js: -------------------------------------------------------------------------------- 1 | import test from 'ava' 2 | 3 | import reduceCalc from '..' 4 | 5 | function testFixture(t, fixture, expected = null, precision = 5) { 6 | if (expected === null) 7 | expected = fixture 8 | 9 | const out = reduceCalc(fixture, precision) 10 | t.deepEqual(out, expected) 11 | } 12 | 13 | function testThrows(t, fixture, expected, precision = 5) { 14 | t.throws(() => reduceCalc(fixture, precision), expected) 15 | } 16 | 17 | test( 18 | 'should reduce simple calc (1)', 19 | testFixture, 20 | 'calc(1px + 1px)', 21 | '2px' 22 | ) 23 | 24 | test( 25 | 'should reduce simple calc (2)', 26 | testFixture, 27 | 'calc(3em - 1em)', 28 | '2em' 29 | ) 30 | 31 | test( 32 | 'should reduce simple calc (3)', 33 | testFixture, 34 | 'calc(1rem * 1.5)', 35 | '1.5rem' 36 | ) 37 | 38 | test( 39 | 'should reduce simple calc (4)', 40 | testFixture, 41 | 'calc(2ex / 2)', 42 | '1ex' 43 | ) 44 | 45 | test( 46 | 'should reduce simple calc (5)', 47 | testFixture, 48 | 'calc(50px - (20px - 30px))', 49 | '60px' 50 | ) 51 | 52 | test( 53 | 'should reduce simple calc (6)', 54 | testFixture, 55 | 'calc(100px - (100px - 100%))', 56 | '100%' 57 | ) 58 | 59 | test( 60 | 'should reduce simple calc (7)', 61 | testFixture, 62 | 'calc(100px + (100px - 100%))', 63 | 'calc(200px - 100%)' 64 | ) 65 | 66 | test( 67 | 'should reduce additions and subtractions (1)', 68 | testFixture, 69 | 'calc(100% - 10px + 20px)', 70 | 'calc(100% + 10px)' 71 | ) 72 | 73 | test( 74 | 'should reduce additions and subtractions (2)', 75 | testFixture, 76 | 'calc(100% + 10px - 20px)', 77 | 'calc(100% - 10px)' 78 | ) 79 | 80 | test( 81 | 'should handle fractions', 82 | testFixture, 83 | 'calc(10.px + .0px)', 84 | '10px' 85 | ) 86 | 87 | test( 88 | 'should ignore value surrounding calc function (1)', 89 | testFixture, 90 | 'a calc(1px + 1px)', 91 | 'a 2px' 92 | ) 93 | 94 | 95 | test( 96 | 'should ignore value surrounding calc function (2)', 97 | testFixture, 98 | 'calc(1px + 1px) a', 99 | '2px a' 100 | ) 101 | 102 | test( 103 | 'should ignore value surrounding calc function (3)', 104 | testFixture, 105 | 'a calc(1px + 1px) b', 106 | 'a 2px b' 107 | ) 108 | 109 | test( 110 | 'should ignore value surrounding calc function (4)', 111 | testFixture, 112 | 'a calc(1px + 1px) b calc(1em + 2em) c', 113 | 'a 2px b 3em c' 114 | ) 115 | 116 | test( 117 | 'should reduce nested calc', 118 | testFixture, 119 | 'calc(100% - calc(50% + 25px))', 120 | 'calc(50% - 25px)' 121 | ) 122 | 123 | test( 124 | 'should reduce prefixed nested calc', 125 | testFixture, 126 | '-webkit-calc(100% - -webkit-calc(50% + 25px))', 127 | '-webkit-calc(50% - 25px)' 128 | ) 129 | 130 | test( 131 | 'should ignore calc with css variables (1)', 132 | testFixture, 133 | 'calc(var(--mouseX) * 1px)' 134 | ) 135 | 136 | test( 137 | 'should ignore calc with css variables (2)', 138 | testFixture, 139 | 'calc(10px - (100px * var(--mouseX)))', 140 | 'calc(10px - 100px * var(--mouseX))' 141 | ) 142 | 143 | test( 144 | 'should ignore calc with css variables (3)', 145 | testFixture, 146 | 'calc(10px - (100px + var(--mouseX)))', 147 | 'calc(-90px - var(--mouseX))' 148 | ) 149 | 150 | test( 151 | 'should ignore calc with css variables (4)', 152 | testFixture, 153 | 'calc(10px - (100px / var(--mouseX)))', 154 | 'calc(10px - 100px / var(--mouseX))' 155 | ) 156 | 157 | test( 158 | 'should ignore calc with css variables (5)', 159 | testFixture, 160 | 'calc(10px - (100px - var(--mouseX)))', 161 | 'calc(-90px + var(--mouseX))' 162 | ) 163 | 164 | test( 165 | 'should ignore calc with css variables (6)', 166 | testFixture, 167 | 'calc(var(--popupHeight) / 2)', 168 | 'calc(var(--popupHeight) / 2)' 169 | ) 170 | 171 | test( 172 | 'should ignore calc with css variables (7)', 173 | testFixture, 174 | 'calc(var(--popupHeight, var(--defaultHeight, var(--height-150))) / 2)', 175 | 'calc(var(--popupHeight, var(--defaultHeight, var(--height-150))) / 2)' 176 | ) 177 | 178 | test( 179 | 'should ignore calc with css variables (8)', 180 | testFixture, 181 | 'calc(var(--popupHeight, var(--defaultHeight, calc(100% - 50px))) / 2)', 182 | 'calc(var(--popupHeight, var(--defaultHeight, calc(100% - 50px))) / 2)' 183 | ) 184 | 185 | test( 186 | 'should ignore calc with css variables (9)', 187 | testFixture, 188 | 'calc(var(--popupHeight, var(--defaultHeight, calc(100% - 50px + 25px))) / 2)', 189 | 'calc(var(--popupHeight, var(--defaultHeight, calc(100% - 25px))) / 2)' 190 | ) 191 | 192 | test( 193 | 'should ignore calc with css variables (10)', 194 | testFixture, 195 | 'calc(var(--popupHeight, var(--defaultHeight, 150px)) / 2)', 196 | 'calc(var(--popupHeight, var(--defaultHeight, 150px)) / 2)' 197 | ) 198 | 199 | 200 | test( 201 | 'should reduce calc with newline characters', 202 | testFixture, 203 | 'calc(\n1rem \n* 2 \n* 1.5)', 204 | '3rem' 205 | ) 206 | 207 | test( 208 | 'should preserve calc with incompatible units', 209 | testFixture, 210 | 'calc(100% + 1px)', 211 | 'calc(100% + 1px)' 212 | ) 213 | 214 | test( 215 | 'should parse fractions without leading zero', 216 | testFixture, 217 | 'calc(2rem - .14285em)', 218 | 'calc(2rem - 0.14285em)' 219 | ) 220 | 221 | test( 222 | 'should handle precision correctly (1)', 223 | testFixture, 224 | 'calc(1/100)', 225 | '0.01' 226 | ) 227 | 228 | test( 229 | 'should handle precision correctly (2)', 230 | testFixture, 231 | 'calc(5/1000000)', 232 | '0.00001' 233 | ) 234 | 235 | test( 236 | 'should handle precision correctly (3)', 237 | testFixture, 238 | 'calc(5/1000000)', 239 | '0.000005', 240 | 6 241 | ) 242 | 243 | test( 244 | 'should reduce browser-prefixed calc (1)', 245 | testFixture, 246 | '-webkit-calc(1px + 1px)', 247 | '2px' 248 | ) 249 | 250 | test( 251 | 'should reduce browser-prefixed calc (2)', 252 | testFixture, 253 | '-moz-calc(1px + 1px)', 254 | '2px' 255 | ) 256 | 257 | test( 258 | 'should discard zero values (#2) (1)', 259 | testFixture, 260 | 'calc(100vw / 2 - 6px + 0px)', 261 | 'calc(50vw - 6px)' 262 | ) 263 | 264 | test( 265 | 'should discard zero values (#2) (2)', 266 | testFixture, 267 | 'calc(500px - 0px)', 268 | '500px' 269 | ) 270 | 271 | test( 272 | 'should not perform addition on unitless values (#3)', 273 | testFixture, 274 | 'calc(1px + 1)', 275 | 'calc(1px + 1)' 276 | ) 277 | 278 | test( 279 | 'should reduce consecutive substractions (#24) (1)', 280 | testFixture, 281 | 'calc(100% - 120px - 60px)', 282 | 'calc(100% - 180px)' 283 | ) 284 | 285 | test( 286 | 'should reduce consecutive substractions (#24) (2)', 287 | testFixture, 288 | 'calc(100% - 10px - 20px)', 289 | 'calc(100% - 30px)' 290 | ) 291 | 292 | test( 293 | 'should produce simpler result (postcss-calc#25) (1)', 294 | testFixture, 295 | 'calc(14px + 6 * ((100vw - 320px) / 448))', 296 | 'calc(9.71px + 1.34vw)', 297 | 2 298 | ) 299 | 300 | test( 301 | 'should produce simpler result (postcss-calc#25) (2)', 302 | testFixture, 303 | '-webkit-calc(14px + 6 * ((100vw - 320px) / 448))', 304 | '-webkit-calc(9.71px + 1.34vw)', 305 | 2 306 | ) 307 | 308 | test( 309 | 'should reduce mixed units of time (postcss-calc#33)', 310 | testFixture, 311 | 'calc(1s - 50ms)', 312 | '0.95s' 313 | ) 314 | 315 | test( 316 | 'should correctly reduce calc with mixed units (cssnano#211)', 317 | testFixture, 318 | 'bar:calc(99.99% * 1/1 - 0rem)', 319 | 'bar:99.99%' 320 | ) 321 | 322 | test( 323 | 'should apply algebraic reduction (cssnano#319)', 324 | testFixture, 325 | 'bar:calc((100px - 1em) + (-50px + 1em))', 326 | 'bar:50px' 327 | ) 328 | 329 | test( 330 | 'should apply optimization (cssnano#320)', 331 | testFixture, 332 | 'bar:calc(50% + (5em + 5%))', 333 | 'bar:calc(55% + 5em)' 334 | ) 335 | 336 | test( 337 | 'should throw an exception when attempting to divide by zero', 338 | testThrows, 339 | 'calc(500px/0)', 340 | /Cannot divide by zero/ 341 | ) 342 | 343 | test( 344 | 'should throw an exception when attempting to divide by unit (#1)', 345 | testThrows, 346 | 'calc(500px/2px)', 347 | 'Cannot divide by "px", number expected' 348 | ) 349 | 350 | test( 351 | 'should reduce substraction from zero', 352 | testFixture, 353 | 'calc( 0 - 10px)', 354 | '-10px' 355 | ) 356 | 357 | test( 358 | 'should reduce subtracted expression from zero', 359 | testFixture, 360 | 'calc( 0 - calc(1px + 1em) )', 361 | 'calc(-1px + -1em)' 362 | ) 363 | 364 | test( 365 | 'should reduce nested expression', 366 | testFixture, 367 | 'calc( (1em - calc( 10px + 1em)) / 2)', 368 | '-5px' 369 | ) 370 | 371 | test( 372 | 'should skip constant()', 373 | testFixture, 374 | 'calc(constant(safe-area-inset-left))', 375 | 'calc(constant(safe-area-inset-left))' 376 | ) 377 | 378 | test( 379 | 'should skip env()', 380 | testFixture, 381 | 'calc(env(safe-area-inset-left))', 382 | 'calc(env(safe-area-inset-left))' 383 | ) 384 | 385 | test( 386 | 'should handle subtractions with different units', 387 | testFixture, 388 | 'calc(100% - calc(666px + 1em + 2em + 100px))', 389 | 'calc(100% - 766px - 3em)' 390 | ) 391 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import valueParser from 'postcss-value-parser' 2 | 3 | import { parser } from './parser' // eslint-disable-line 4 | import reducer from './lib/reducer' 5 | import stringifier from './lib/stringifier' 6 | 7 | const MATCH_CALC = /((?:\-[a-z]+\-)?calc)/ 8 | 9 | export default (value, precision = 5) => { 10 | return valueParser(value).walk(node => { 11 | // skip anything which isn't a calc() function 12 | if (node.type !== 'function' || !MATCH_CALC.test(node.value)) 13 | return 14 | 15 | // stringify calc expression and produce an AST 16 | const contents = valueParser.stringify(node.nodes) 17 | 18 | // skip constant() and env() 19 | if (contents.indexOf('constant') >= 0 || contents.indexOf('env') >= 0) return; 20 | 21 | const ast = parser.parse(contents) 22 | 23 | // reduce AST to its simplest form, that is, either to a single value 24 | // or a simplified calc expression 25 | const reducedAst = reducer(ast, precision) 26 | 27 | // stringify AST and write it back 28 | node.type = 'word' 29 | node.value = stringifier(node.value, reducedAst, precision) 30 | 31 | }, true).toString() 32 | } 33 | -------------------------------------------------------------------------------- /src/lib/convert.js: -------------------------------------------------------------------------------- 1 | import convertUnits from 'css-unit-converter' 2 | 3 | function convertNodes(left, right, precision) { 4 | switch (left.type) { 5 | case 'LengthValue': 6 | case 'AngleValue': 7 | case 'TimeValue': 8 | case 'FrequencyValue': 9 | case 'ResolutionValue': 10 | return convertAbsoluteLength(left, right, precision) 11 | default: 12 | return { left, right } 13 | } 14 | } 15 | 16 | function convertAbsoluteLength(left, right, precision) { 17 | if (right.type === left.type) { 18 | right = { 19 | type: left.type, 20 | value: convertUnits(right.value, right.unit, left.unit, precision), 21 | unit: left.unit 22 | } 23 | } 24 | return { left, right } 25 | } 26 | 27 | export default convertNodes 28 | -------------------------------------------------------------------------------- /src/lib/reducer.js: -------------------------------------------------------------------------------- 1 | import convert from './convert' 2 | 3 | function reduce(node, precision) { 4 | if (node.type === "MathExpression") 5 | return reduceMathExpression(node, precision) 6 | if (node.type === "Calc") 7 | return reduce(node.value, precision) 8 | 9 | return node 10 | } 11 | 12 | function isEqual(left, right) { 13 | return left.type === right.type && left.value === right.value 14 | } 15 | 16 | function isValueType(type) { 17 | switch (type) { 18 | case 'LengthValue': 19 | case 'AngleValue': 20 | case 'TimeValue': 21 | case 'FrequencyValue': 22 | case 'ResolutionValue': 23 | case 'EmValue': 24 | case 'ExValue': 25 | case 'ChValue': 26 | case 'RemValue': 27 | case 'VhValue': 28 | case 'VwValue': 29 | case 'VminValue': 30 | case 'VmaxValue': 31 | case 'PercentageValue': 32 | case 'Value': 33 | return true; 34 | } 35 | return false; 36 | } 37 | 38 | function convertMathExpression(node, precision) { 39 | let nodes = convert(node.left, node.right, precision) 40 | let left = reduce(nodes.left, precision) 41 | let right = reduce(nodes.right, precision) 42 | 43 | if (left.type === "MathExpression" && right.type === "MathExpression") { 44 | 45 | if (((left.operator === '/' && right.operator === '*') || 46 | (left.operator === '-' && right.operator === '+')) || 47 | ((left.operator === '*' && right.operator === '/') || 48 | (left.operator === '+' && right.operator === '-'))) { 49 | 50 | if (isEqual(left.right, right.right)) 51 | nodes = convert(left.left, right.left, precision) 52 | 53 | else if (isEqual(left.right, right.left)) 54 | nodes = convert(left.left, right.right, precision) 55 | 56 | left = reduce(nodes.left, precision) 57 | right = reduce(nodes.right, precision) 58 | 59 | } 60 | } 61 | 62 | node.left = left 63 | node.right = right 64 | return node 65 | } 66 | 67 | export function flip(operator) { 68 | return operator === '+' ? '-' : '+' 69 | } 70 | 71 | function flipValue(node) { 72 | if (isValueType(node.type)) 73 | node.value = -node.value 74 | else if (node.type == 'MathExpression') { 75 | node.left = flipValue(node.left) 76 | node.right = flipValue(node.right) 77 | } 78 | return node 79 | } 80 | 81 | function reduceAddSubExpression(node, precision) { 82 | const {left, right, operator: op} = node 83 | 84 | if (left.type === 'CssVariable' || right.type === 'CssVariable') 85 | return node 86 | 87 | // something + 0 => something 88 | // something - 0 => something 89 | if (right.value === 0) 90 | return left 91 | 92 | // 0 + something => something 93 | if (left.value === 0 && op === "+") 94 | return right 95 | 96 | // 0 - something => -something 97 | if (left.value === 0 && op === "-") 98 | return flipValue(right) 99 | 100 | // value + value 101 | // value - value 102 | if (left.type === right.type && isValueType(left.type)) { 103 | node = Object.assign({ }, left) 104 | if (op === "+") 105 | node.value = left.value + right.value 106 | else 107 | node.value = left.value - right.value 108 | } 109 | 110 | // value (expr) 111 | if ( 112 | isValueType(left.type) && 113 | (right.operator === '+' || right.operator === '-') && 114 | right.type === 'MathExpression' 115 | ) { 116 | // value + (value + something) => (value + value) + something 117 | // value + (value - something) => (value + value) - something 118 | // value - (value + something) => (value - value) - something 119 | // value - (value - something) => (value - value) + something 120 | if (left.type === right.left.type) { 121 | node = Object.assign({ }, node) 122 | node.left = reduce({ 123 | type: 'MathExpression', 124 | operator: op, 125 | left: left, 126 | right: right.left 127 | }, precision) 128 | node.right = right.right 129 | node.operator = op === '-' ? flip(right.operator) : right.operator 130 | return reduce(node, precision) 131 | } 132 | // value + (something + value) => (value + value) + something 133 | // value + (something - value) => (value - value) + something 134 | // value - (something + value) => (value - value) - something 135 | // value - (something - value) => (value + value) - something 136 | else if (left.type === right.right.type) { 137 | node = Object.assign({ }, node) 138 | node.left = reduce({ 139 | type: 'MathExpression', 140 | operator: op === '-' ? flip(right.operator) : right.operator, 141 | left: left, 142 | right: right.right 143 | }, precision) 144 | node.right = right.left 145 | return reduce(node, precision) 146 | } 147 | } 148 | 149 | // (expr) value 150 | if ( 151 | left.type === 'MathExpression' && 152 | (left.operator === '+' || left.operator === '-') && 153 | isValueType(right.type) 154 | ) { 155 | // (value + something) + value => (value + value) + something 156 | // (value - something) + value => (value + value) - something 157 | // (value + something) - value => (value - value) + something 158 | // (value - something) - value => (value - value) - something 159 | if (right.type === left.left.type) { 160 | node = Object.assign({ }, left) 161 | node.left = reduce({ 162 | type: 'MathExpression', 163 | operator: op, 164 | left: left.left, 165 | right: right 166 | }, precision) 167 | return reduce(node, precision) 168 | } 169 | // (something + value) + value => something + (value + value) 170 | // (something - value1) + value2 => something - (value2 - value1) 171 | // (something + value) - value => something + (value - value) 172 | // (something - value) - value => something - (value + value) 173 | else if (right.type === left.right.type) { 174 | node = Object.assign({ }, left) 175 | if (left.operator === '-') { 176 | node.right = reduce({ 177 | type: 'MathExpression', 178 | operator: op === '-' ? '+' : '-', 179 | left: right, 180 | right: left.right 181 | }, precision) 182 | node.operator = op === '-' ? '-' : '+'; 183 | } 184 | else { 185 | node.right = reduce({ 186 | type: 'MathExpression', 187 | operator: op, 188 | left: left.right, 189 | right: right 190 | }, precision) 191 | } 192 | if (node.right.value < 0) { 193 | node.right.value *= -1; 194 | node.operator = node.operator === '-' ? '+' : '-'; 195 | } 196 | return reduce(node, precision) 197 | } 198 | } 199 | return node 200 | } 201 | 202 | function reduceDivisionExpression(node, precision) { 203 | if (!isValueType(node.right.type)) 204 | return node 205 | 206 | if (node.right.type !== 'Value') 207 | throw new Error(`Cannot divide by "${node.right.unit}", number expected`) 208 | 209 | if (node.right.value === 0) 210 | throw new Error('Cannot divide by zero') 211 | 212 | // (expr) / value 213 | if (node.left.type === 'MathExpression') { 214 | if ( 215 | isValueType(node.left.left.type) && 216 | isValueType(node.left.right.type) 217 | ) { 218 | node.left.left.value /= node.right.value 219 | node.left.right.value /= node.right.value 220 | return reduce(node.left, precision) 221 | } 222 | return node 223 | } 224 | // something / value 225 | else if (isValueType(node.left.type)) { 226 | node.left.value /= node.right.value 227 | return node.left 228 | } 229 | return node 230 | } 231 | 232 | function reduceMultiplicationExpression(node) { 233 | // (expr) * value 234 | if (node.left.type === 'MathExpression' && node.right.type === 'Value') { 235 | if ( 236 | isValueType(node.left.left.type) && 237 | isValueType(node.left.right.type) 238 | ) { 239 | node.left.left.value *= node.right.value 240 | node.left.right.value *= node.right.value 241 | return node.left 242 | } 243 | } 244 | // something * value 245 | else if (isValueType(node.left.type) && node.right.type === 'Value') { 246 | node.left.value *= node.right.value 247 | return node.left 248 | } 249 | // value * (expr) 250 | else if (node.left.type === 'Value' && node.right.type === 'MathExpression') { 251 | if ( 252 | isValueType(node.right.left.type) && 253 | isValueType(node.right.right.type) 254 | ) { 255 | node.right.left.value *= node.left.value 256 | node.right.right.value *= node.left.value 257 | return node.right 258 | } 259 | } 260 | // value * something 261 | else if (node.left.type === 'Value' && isValueType(node.right.type)) { 262 | node.right.value *= node.left.value 263 | return node.right 264 | } 265 | return node 266 | } 267 | 268 | function reduceMathExpression(node, precision) { 269 | node = convertMathExpression(node, precision) 270 | 271 | switch (node.operator) { 272 | case "+": 273 | case "-": 274 | return reduceAddSubExpression(node, precision) 275 | case "/": 276 | return reduceDivisionExpression(node, precision) 277 | case "*": 278 | return reduceMultiplicationExpression(node) 279 | } 280 | return node 281 | } 282 | 283 | export default reduce 284 | -------------------------------------------------------------------------------- /src/lib/stringifier.js: -------------------------------------------------------------------------------- 1 | import { flip } from "./reducer"; 2 | 3 | const order = { 4 | "*": 0, 5 | "/": 0, 6 | "+": 1, 7 | "-": 1 8 | } 9 | 10 | function round(value, prec) { 11 | if (prec !== false) { 12 | const precision = Math.pow(10, prec) 13 | return Math.round(value * precision) / precision 14 | } 15 | return value 16 | } 17 | 18 | function stringify(node, prec) { 19 | switch (node.type) { 20 | case "MathExpression": { 21 | const {left, right, operator: op} = node 22 | let str = "" 23 | 24 | if (left.type === 'MathExpression' && order[op] < order[left.operator]) 25 | str += "(" + stringify(left, prec) + ")" 26 | else 27 | str += stringify(left, prec) 28 | 29 | str += " " + node.operator + " " 30 | 31 | if (right.type === 'MathExpression' && order[op] < order[right.operator]) { 32 | str += "(" + stringify(right, prec) + ")" 33 | } else if (right.type === 'MathExpression' && op === "-" && ["+", "-"].includes(right.operator)) { 34 | // fix #52 : a-(b+c) = a-b-c 35 | right.operator = flip(right.operator); 36 | str += stringify(right, prec) 37 | } else { 38 | str += stringify(right, prec) 39 | } 40 | 41 | return str 42 | } 43 | case "Value": 44 | return round(node.value, prec) 45 | case 'CssVariable': 46 | if (node.fallback) { 47 | return `var(${node.value}, ${stringify(node.fallback, prec, true)})` 48 | } 49 | return `var(${node.value})` 50 | case 'Calc': 51 | if (node.prefix) { 52 | return `-${node.prefix}-calc(${stringify(node.value, prec)})`; 53 | } 54 | return `calc(${stringify(node.value, prec)})`; 55 | default: 56 | return round(node.value, prec) + node.unit 57 | } 58 | } 59 | 60 | export default function (calc, node, precision) { 61 | let str = stringify(node, precision) 62 | 63 | if (node.type === "MathExpression") { 64 | // if calc expression couldn't be resolved to a single value, re-wrap it as 65 | // a calc() 66 | str = calc + "(" + str + ")" 67 | } 68 | return str 69 | } 70 | --------------------------------------------------------------------------------