From f00771c8bea65396c7ff09db457b0b9d7e5506e2 Mon Sep 17 00:00:00 2001 From: Maxime Thirouin Date: Mon, 26 May 2014 08:16:00 +0200 Subject: [PATCH] Add `preserve` option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Setting `preserve` to `true` will preserve `calc()` in the output, so that they can be used by supporting browsers. Useful now that browsers are starting to implement vars (eg Firefox 31+) & it’s even better for debug [to see real rules](http://cl.ly/image/3W3O0E41173X). I’ve refactored the code a little bit to make it easier to use the `preserve` in the main function (& also use rework-visit which is more bullet proof) like rework-vars. This should be a major update since now calc need to be called as a function (like rework-vars for example) to be able to pass options. So this should be tagged as 0.3 (or maybe it’s time to ship 1.0 :) ?) I also quickly refactor tests to make it more understandable (plugins.js near preserve.css was confusing imo). I also add some error reporting like we have in rework-vars. Btw, maybe we can expand the use of balanced-match to rewrite other parts of the plugin. I hope it’s ok. FWI, I did this since I’m more seeing rework-vars & rework-calc as fallback than replacement. See https://github.com/reworkcss/rework-vars/issues/28 https://github.com/reworkcss/rework-vars/issues/29 & https://github.com/segmentio/myth/issues/64 --- README.md | 8 +- lib/calc.js | 131 ++++++++++++----------- package.json | 4 + test/{ => fixtures}/calc-complex.in.css | 0 test/{ => fixtures}/calc-complex.out.css | 0 test/{ => fixtures}/calc-percent.in.css | 0 test/{ => fixtures}/calc-percent.out.css | 0 test/{ => fixtures}/calc-prefix.in.css | 0 test/{ => fixtures}/calc-prefix.out.css | 0 test/{ => fixtures}/calc.in.css | 0 test/{ => fixtures}/calc.out.css | 0 test/fixtures/preserve.in.css | 9 ++ test/fixtures/preserve.out.css | 11 ++ test/fixtures/substitution-empty.css | 3 + test/fixtures/substitution-malformed.css | 4 + test/plugins.js | 47 -------- test/test.js | 53 +++++++++ 17 files changed, 161 insertions(+), 109 deletions(-) rename test/{ => fixtures}/calc-complex.in.css (100%) rename test/{ => fixtures}/calc-complex.out.css (100%) rename test/{ => fixtures}/calc-percent.in.css (100%) rename test/{ => fixtures}/calc-percent.out.css (100%) rename test/{ => fixtures}/calc-prefix.in.css (100%) rename test/{ => fixtures}/calc-prefix.out.css (100%) rename test/{ => fixtures}/calc.in.css (100%) rename test/{ => fixtures}/calc.out.css (100%) create mode 100644 test/fixtures/preserve.in.css create mode 100644 test/fixtures/preserve.out.css create mode 100644 test/fixtures/substitution-empty.css create mode 100644 test/fixtures/substitution-malformed.css delete mode 100644 test/plugins.js create mode 100644 test/test.js diff --git a/README.md b/README.md index cf98322..f77c599 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ particularly useful together with the [rework-vars](https://npmjs.org/package/re When multiple units are mixed together in the same expression, the calc() statement is left as is, to fallback to the CSS3 Calc feature. -**Example** (with rework-vars enabled as well): +**Example** (with [rework-vars](https://github.com/reworkcss/rework-vars) enabled as well): ```css :root { @@ -62,6 +62,12 @@ h1 { See unit tests for another example. +### Options + +#### `preserve` (default: `false`) + +Setting `preserve` to `true` will preserve `calc()` in the output, so that they can be used by supporting browsers. + ## Unit tests Make sure the dev-dependencies are installed, and then run: diff --git a/lib/calc.js b/lib/calc.js index 979495a..f23bfd6 100644 --- a/lib/calc.js +++ b/lib/calc.js @@ -1,24 +1,20 @@ - /** * Calculation Plugin * * Useful in combination with the [rework-vars](https://npmjs.org/package/rework-vars) plugin, e.g: * * :root { - * var-base-font-size: 16px; + * --base-font-size: 16px; * } * body { - * font-size: var(base-font-size); + * font-size: var(--base-font-size); * } * h1 { - * font-size: calc(var(base-font-size) * 2); + * font-size: calc(var(--base-font-size) * 2); * } * * Yields: * - * :root { - * var-base-font-size: 16px; - * } * body { * font-size: 16px; * } @@ -28,58 +24,66 @@ * */ -module.exports = function (style) { - rules(style.rules); -}; +/** + * Module dependencies. + */ + +var balanced = require('balanced-match'); +var visit = require('rework-visit'); /** - * Constants + * Constants. */ var DEFAULT_UNIT = 'px', - EXPRESSION_METHOD_NAME = 'calc', + CALC_FUNC_IDENTIFIER = 'calc', + EXPRESSION_OPT_VENDOR_PREFIX = '(\\-[a-z]+\\-)?', - EXPRESSION_METHOD_REGEXP = EXPRESSION_OPT_VENDOR_PREFIX + EXPRESSION_METHOD_NAME, + EXPRESSION_METHOD_REGEXP = EXPRESSION_OPT_VENDOR_PREFIX + CALC_FUNC_IDENTIFIER, EXPRESSION_REGEXP = '\\b' + EXPRESSION_METHOD_REGEXP + '\\('; /** - * Visit all rules - * - * @param {Array} arr Array with css rules - * @api private + * Module export. */ -function rules(arr) { - arr.forEach(function (rule) { - if (rule.rules) rules(rule.rules); - if (rule.declarations) visit(rule.declarations); - }); -} -/** - * Visit all declarations (in a rule) - * - * @param {Array} declarations - * @api private - */ -function visit(declarations) { - declarations.forEach(function (decl) { - if (!hasExpressions(decl.value)) return; +module.exports = function (options) { - var expressions = getExpressionsFromValue(decl.value); + return function vars(style) { + options = options || {}; + var preserve = (options.preserve === true ? true : false); - evaluateAndApplyExpressions(expressions, decl); - }); -} + // resolve variables + visit(style, function (declarations, node) { + var decl; + var resolvedValue; + var value; -/** - * Checks if a value contains an expression - * - * @param {String} value - * @returns {Boolean} - * @api private - */ -function hasExpressions(value) { - return (new RegExp(EXPRESSION_REGEXP)).exec(value); -} + for (var i = 0; i < declarations.length; i++) { + decl = declarations[i]; + value = decl.value; + + // skip comments + if (decl.type !== 'declaration') continue; + // skip values that don't contain variable functions + if (!value || value.indexOf(CALC_FUNC_IDENTIFIER + '(') === -1) continue; + + resolvedValue = resolveValue(value); + + if (!preserve) { + decl.value = resolvedValue; + } + else { + declarations.splice(i, 0, { + type: decl.type, + property: decl.property, + value: resolvedValue + }); + // skip ahead of preserved declaration + i++; + } + } + }); + }; +}; /** * Parses expressions in a value @@ -95,7 +99,7 @@ function getExpressionsFromValue(value) { // Parse value and extract expressions: for (var i = 0; i < value.length; i++) { - if (value[i] == '(' && value.slice(i - 4, i) == EXPRESSION_METHOD_NAME && !start) { + if (value[i] == '(' && value.slice(i - 4, i) == CALC_FUNC_IDENTIFIER && !start) { start = i; parentheses++; } else if (value[i] == '(' && start !== null) { @@ -118,19 +122,24 @@ function getExpressionsFromValue(value) { * @param {Object} declaration * @api private */ -function evaluateAndApplyExpressions(expressions, declaration) { - expressions.forEach(function (expression) { - var result = evaluateExpression(expression); - - if (!result) return; - - // Insert the evaluated value: - var expRegexp = new RegExp( - EXPRESSION_METHOD_REGEXP + '\\(' + - escapeExp(expression) + '\\)' - ); - declaration.value = declaration.value.replace(expRegexp, result); - }); +function resolveValue(value) { + var balancedParens = balanced('(', ')', value); + var calcRef = balanced(CALC_FUNC_IDENTIFIER + '(', ')', value); + + if (!balancedParens) throw new Error('rework-calc: missing closing ")" in the value "' + value + '"'); + if (!calcRef || calcRef.body === '') throw new Error('rework-calc: calc() must contain a non-whitespace string'); + + getExpressionsFromValue(value).forEach(function (expression) { + var result = evaluateExpression(expression); + + if (!result) return; + + // Insert the evaluated value: + var expRegexp = new RegExp(EXPRESSION_METHOD_REGEXP + '\\(' + escapeExp(expression) + '\\)'); + value = value.replace(expRegexp, result); + }); + + return value } /** @@ -141,7 +150,7 @@ function evaluateAndApplyExpressions(expressions, declaration) { * @api private */ function evaluateExpression (expression) { - var originalExpression = EXPRESSION_METHOD_NAME + '(' + expression + ')'; + var originalExpression = CALC_FUNC_IDENTIFIER + '(' + expression + ')'; // Remove method names for possible nested expressions: expression = expression.replace(new RegExp(EXPRESSION_REGEXP, 'g'), '('); diff --git a/package.json b/package.json index 35bc87d..0a834ae 100644 --- a/package.json +++ b/package.json @@ -19,5 +19,9 @@ "mocha": "~1.15.1", "rework": "~0.18.3", "chai": "~1.8.1" + }, + "dependencies": { + "balanced-match": "^0.1.0", + "rework-visit": "^1.0.0" } } diff --git a/test/calc-complex.in.css b/test/fixtures/calc-complex.in.css similarity index 100% rename from test/calc-complex.in.css rename to test/fixtures/calc-complex.in.css diff --git a/test/calc-complex.out.css b/test/fixtures/calc-complex.out.css similarity index 100% rename from test/calc-complex.out.css rename to test/fixtures/calc-complex.out.css diff --git a/test/calc-percent.in.css b/test/fixtures/calc-percent.in.css similarity index 100% rename from test/calc-percent.in.css rename to test/fixtures/calc-percent.in.css diff --git a/test/calc-percent.out.css b/test/fixtures/calc-percent.out.css similarity index 100% rename from test/calc-percent.out.css rename to test/fixtures/calc-percent.out.css diff --git a/test/calc-prefix.in.css b/test/fixtures/calc-prefix.in.css similarity index 100% rename from test/calc-prefix.in.css rename to test/fixtures/calc-prefix.in.css diff --git a/test/calc-prefix.out.css b/test/fixtures/calc-prefix.out.css similarity index 100% rename from test/calc-prefix.out.css rename to test/fixtures/calc-prefix.out.css diff --git a/test/calc.in.css b/test/fixtures/calc.in.css similarity index 100% rename from test/calc.in.css rename to test/fixtures/calc.in.css diff --git a/test/calc.out.css b/test/fixtures/calc.out.css similarity index 100% rename from test/calc.out.css rename to test/fixtures/calc.out.css diff --git a/test/fixtures/preserve.in.css b/test/fixtures/preserve.in.css new file mode 100644 index 0000000..42b4e6a --- /dev/null +++ b/test/fixtures/preserve.in.css @@ -0,0 +1,9 @@ + +body { + width: 100%; +} + +body > header { + height: calc(3em * 2); + font-size: calc(6em / 2); +} diff --git a/test/fixtures/preserve.out.css b/test/fixtures/preserve.out.css new file mode 100644 index 0000000..41c2720 --- /dev/null +++ b/test/fixtures/preserve.out.css @@ -0,0 +1,11 @@ + +body { + width: 100%; +} + +body > header { + height: 6em; + height: calc(3em * 2); + font-size: 3em; + font-size: calc(6em / 2); +} diff --git a/test/fixtures/substitution-empty.css b/test/fixtures/substitution-empty.css new file mode 100644 index 0000000..1bb8b5b --- /dev/null +++ b/test/fixtures/substitution-empty.css @@ -0,0 +1,3 @@ +div { + width: calc(); +} diff --git a/test/fixtures/substitution-malformed.css b/test/fixtures/substitution-malformed.css new file mode 100644 index 0000000..00ba839 --- /dev/null +++ b/test/fixtures/substitution-malformed.css @@ -0,0 +1,4 @@ +div { + /* missing closing ')' */ + width: calc(10px - 5px; +} diff --git a/test/plugins.js b/test/plugins.js deleted file mode 100644 index a86700a..0000000 --- a/test/plugins.js +++ /dev/null @@ -1,47 +0,0 @@ -var calc = require('../index'), - rework = require('rework'), - should = require('chai').Should(), - read = require('fs').readFileSync; - -var css = { - in: function (name) { - return this._read(name, 'in'); - }, - out: function (name) { - return this._read(name, 'out'); - }, - _read: function (name, type) { - return read(__dirname + '/' + name + '.' + type + '.css', 'utf8'); - } -}; - -describe('rework-calc', function() { - it('should calculate expressions with only one unit involved', function() { - rework(css.in('calc')) - .use(calc) - .toString() - .should.equal(css.out('calc')); - }); - - it('should calculate expressions with percents correctly', function () { - rework(css.in('calc-percent')) - .use(calc) - .toString() - .should.equal(css.out('calc-percent')); - }); - - it('should use CSS3 Calc function as fallback for expressions with multiple units', function () { - rework(css.in('calc-complex')) - .use(calc) - .toString() - .should.equal(css.out('calc-complex')); - }); - - it('should handle vendor prefixed expressions', function () { - rework(css.in('calc-prefix')) - .use(calc) - .toString() - .should.equal(css.out('calc-prefix')); - }); - -}); diff --git a/test/test.js b/test/test.js new file mode 100644 index 0000000..4b910cb --- /dev/null +++ b/test/test.js @@ -0,0 +1,53 @@ +var calc = require('../index'), + rework = require('rework'), + expect = require('chai').expect, + read = require('fs').readFileSync; + +function fixture(name){ + return read('test/fixtures/' + name + '.css', 'utf8').trim(); +} + +function compareFixtures(name, options){ + return expect( + rework(fixture(name + '.in')) + .use(calc(options)) + .toString().trim() + ).to.equal(fixture(name + '.out')); +} + +describe('rework-calc', function() { + it('throws an error when a calc function is empty', function () { + var output = function () { + return rework(fixture('substitution-empty')).use(calc()).toString(); + }; + expect(output).to.Throw(Error, 'rework-calc: calc() must contain a non-whitespace string'); + }); + + it('throws an error when a variable function is malformed', function () { + var output = function () { + return rework(fixture('substitution-malformed')).use(calc()).toString(); + }; + expect(output).to.Throw(Error, 'rework-calc: missing closing ")" in the value "calc(10px - 5px"'); + }); + + + it('should calculate expressions with only one unit involved', function() { + compareFixtures('calc'); + }); + + it('should calculate expressions with percents correctly', function () { + compareFixtures('calc-percent'); + }); + + it('should use CSS3 Calc function as fallback for expressions with multiple units', function () { + compareFixtures('calc-complex'); + }); + + it('should handle vendor prefixed expressions', function () { + compareFixtures('calc-prefix'); + }); + + it('should preserves calc() when `preserve` is `true`', function() { + compareFixtures('preserve', {preserve: true}); + }); +});