Skip to content

Commit

Permalink
Add preserve option
Browse files Browse the repository at this point in the history
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 reworkcss/rework-vars#28
reworkcss/rework-vars#29 &
segmentio/myth#64
  • Loading branch information
MoOx committed May 28, 2014
1 parent 9e41e96 commit f00771c
Show file tree
Hide file tree
Showing 17 changed files with 161 additions and 109 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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:
Expand Down
131 changes: 70 additions & 61 deletions lib/calc.js
Original file line number Diff line number Diff line change
@@ -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;
* }
Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -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
}

/**
Expand All @@ -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'), '(');
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
9 changes: 9 additions & 0 deletions test/fixtures/preserve.in.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@

body {
width: 100%;
}

body > header {
height: calc(3em * 2);
font-size: calc(6em / 2);
}
11 changes: 11 additions & 0 deletions test/fixtures/preserve.out.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

body {
width: 100%;
}

body > header {
height: 6em;
height: calc(3em * 2);
font-size: 3em;
font-size: calc(6em / 2);
}
3 changes: 3 additions & 0 deletions test/fixtures/substitution-empty.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
div {
width: calc();
}
4 changes: 4 additions & 0 deletions test/fixtures/substitution-malformed.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
div {
/* missing closing ')' */
width: calc(10px - 5px;
}
47 changes: 0 additions & 47 deletions test/plugins.js

This file was deleted.

53 changes: 53 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
@@ -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});
});
});

0 comments on commit f00771c

Please sign in to comment.