Skip to content

Commit

Permalink
implement AST cloning (fixes #296)
Browse files Browse the repository at this point in the history
- new function `clone` to deep clone of AST node
- new option `clone` for `compress()` to transform copy of input AST
(useful in case of AST reuse)
  • Loading branch information
lahmatiy committed May 16, 2016
1 parent 9a0ff7e commit ecc017a
Show file tree
Hide file tree
Showing 5 changed files with 110 additions and 35 deletions.
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,11 +286,14 @@ var ast = csso.parse('.foo.bar', {

#### compress(ast[, options])

Do the main task – compress AST.
Does the main task – compress AST.

> NOTE: `compress` performs AST compression by transforming input AST by default (since AST cloning is expensive and needed in rare cases). Use `clone` option with truthy value in case you want to keep input AST untouched.
Options:

- restructure `Boolean` – do the structure optimisations or not (`true` by default)
- clone `Boolean` - transform a copy of input AST if `true`, useful in case of AST reuse (`false` by default)
- comments `String` or `Boolean` – specify what comments to left
- `'exclamation'` or `true` (default) – left all exclamation comments (i.e. `/*! .. */`)
- `'first-exclamation'` – remove every comments except first one
Expand Down
65 changes: 32 additions & 33 deletions lib/compressor/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
var List = require('../utils/list');
var clone = require('../utils/clone');
var usageUtils = require('./usage');
var clean = require('./clean');
var compress = require('./compress');
Expand Down Expand Up @@ -83,10 +84,34 @@ function getRestructureOption(options) {
true;
}

function wrapBlockAst(block) {
return {
type: 'StyleSheet',
rules: new List([{
type: 'Ruleset',
selector: {
type: 'Selector',
selectors: new List([{
type: 'SimpleSelector',
sequence: new List([{
type: 'Identifier',
name: 'x'
}])
}])
},
block: block
}])
};
}

module.exports = function compress(ast, options) {
options = options || {};
ast = ast || { type: 'StyleSheet', rules: new List() };

if (options.clone) {
ast = clone(ast);
}

var logger = typeof options.logger === 'function' ? options.logger : Function();
var specialComments = getCommentsOption(options);
var restructuring = getRestructureOption(options);
Expand All @@ -95,29 +120,11 @@ module.exports = function compress(ast, options) {
var firstAtrulesAllowed = true;
var blockNum = 1;
var blockRules;
var blockMode = false;
var usageData = false;
var info = ast.info || null;
var resultAst = ast;

if (ast.type !== 'StyleSheet') {
blockMode = true;
ast = {
type: 'StyleSheet',
rules: new List([{
type: 'Ruleset',
selector: {
type: 'Selector',
selectors: new List([{
type: 'SimpleSelector',
sequence: new List([{
type: 'Identifier',
name: 'x'
}])
}])
},
block: ast
}])
};
ast = wrapBlockAst(ast);
}

if (options.usage) {
Expand Down Expand Up @@ -172,21 +179,13 @@ module.exports = function compress(ast, options) {
result.appendList(blockRules);
} while (!ast.rules.isEmpty());

if (blockMode) {
result = !result.isEmpty() ? result.first().block : {
type: 'Block',
info: info,
declarations: new List()
};
} else {
result = {
type: 'StyleSheet',
info: info,
rules: result
};
// resultAst and ast equals for stylesheet nodes
// replace rules list in this case
if (resultAst === ast) {
resultAst.rules = result;
}

return {
ast: result
ast: resultAst
};
};
6 changes: 5 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ var compress = require('./compressor');
var translate = require('./utils/translate');
var translateWithSourceMap = require('./utils/translateWithSourceMap');
var walkers = require('./utils/walk');
var clone = require('./utils/clone');
var List = require('./utils/list');

function debugOutput(name, options, startTime, data) {
Expand Down Expand Up @@ -124,5 +125,8 @@ module.exports = {
// walkers
walk: walkers.all,
walkRules: walkers.rules,
walkRulesRight: walkers.rulesRight
walkRulesRight: walkers.rulesRight,

// utils
clone: clone
};
23 changes: 23 additions & 0 deletions lib/utils/clone.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
var List = require('./list');

module.exports = function clone(node) {
var result = {};

for (var key in node) {
var value = node[key];

if (value) {
if (Array.isArray(value)) {
value = value.slice(0);
} else if (value instanceof List) {
value = new List(value.map(clone));
} else if (value.constructor === Object) {
value = clone(value);
}
}

result[key] = value;
}

return result;
};
46 changes: 46 additions & 0 deletions test/clone.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
var assert = require('assert');
var csso = require('../lib/index.js');

function sumMarker(ast) {
var result = 0;

csso.walk(ast, function(node) {
result += node.marker;
});

return result;
}

describe('AST clone', function() {
it('clone()', function() {
var ast = csso.parse('.test{color:red;}@media foo{div{color:green}}');
var astCopy = csso.clone(ast);

csso.walk(ast, function(node) {
node.marker = 1;
});

csso.walk(astCopy, function(node) {
node.marker = 2;
});

assert(sumMarker(ast) > 1);
assert.equal(sumMarker(ast) * 2, sumMarker(astCopy));
});

it('compress(ast, { clone: false })', function() {
var ast = csso.parse('.foo{color:red}.bar{color:#ff0000}');
var compressedAst = csso.compress(ast, { clone: false }).ast;

assert.equal(csso.translate(compressedAst), '.bar,.foo{color:red}');
assert.equal(csso.translate(ast), '.bar,.foo{color:red}');
});

it('compress(ast, { clone: true })', function() {
var ast = csso.parse('.foo{color:red}.bar{color:#ff0000}');
var compressedAst = csso.compress(ast, { clone: true }).ast;

assert.equal(csso.translate(compressedAst), '.bar,.foo{color:red}');
assert.equal(csso.translate(ast), '.foo{color:red}.bar{color:#ff0000}');
});
});

0 comments on commit ecc017a

Please sign in to comment.