* Stringifying with source maps "on" requires doing it in different order.
* Basically restoring escaped data has to be done on every token rather than at the end.
* `relativeTo` - path to __resolve__ relative `@import` rules and URLs
* `root` - path to __resolve__ absolute `@import` rules and __rebase__ relative URLs
* `roundingPrecision` - rounding precision; defaults to `2`; `-1` disables rounding
+* `sourceMap` - exposes source map under `sourceMap` property, e.g. `new CleanCSS().minify(source).sourceMap` (default is false)
* `target` - path to a folder or an output file to which __rebase__ all URLs
### How to use clean-css with build tools?
var UrlRebase = require('./images/url-rebase');
var SelectorsOptimizer = require('./selectors/optimizer');
var Stringifier = require('./selectors/stringifier');
+var SourceMapStringifier = require('./selectors/source-map-stringifier');
var CommentsProcessor = require('./text/comments-processor');
var ExpressionsProcessor = require('./text/expressions-processor');
relativeTo: options.relativeTo,
root: options.root,
roundingPrecision: options.roundingPrecision,
+ sourceMap: !!options.sourceMap,
target: options.target
};
var options = this.options;
var context = this.context;
- var commentsProcessor = new CommentsProcessor(context, options.keepSpecialComments, options.keepBreaks);
- var expressionsProcessor = new ExpressionsProcessor();
- var freeTextProcessor = new FreeTextProcessor();
- var urlsProcessor = new UrlsProcessor(context);
+ var commentsProcessor = new CommentsProcessor(context, options.keepSpecialComments, options.keepBreaks, options.sourceMap);
+ var expressionsProcessor = new ExpressionsProcessor(options.sourceMap);
+ var freeTextProcessor = new FreeTextProcessor(options.sourceMap);
+ var urlsProcessor = new UrlsProcessor(context, options.sourceMap);
var urlRebase = new UrlRebase(options, context);
var selectorsOptimizer = new SelectorsOptimizer(options, context);
+ var stringifierClass = options.sourceMap ? SourceMapStringifier : Stringifier;
var run = function (processor, action) {
data = typeof processor == 'function' ?
run(freeTextProcessor, 'escape');
run(function() {
- var stringifier = new Stringifier(options.keepBreaks, function (data) {
+ var stringifier = new stringifierClass(options.keepBreaks, function (data) {
data = freeTextProcessor.restore(data);
data = urlsProcessor.restore(data);
data = options.rebase ? urlRebase.process(data) : data;
}
SelectorsOptimizer.prototype.process = function (data, stringifier) {
- var tokens = new Tokenizer(this.context, this.options.advanced).toTokens(data);
+ var tokens = new Tokenizer(this.context, this.options.advanced, this.options.sourceMap).toTokens(data);
new SimpleOptimizer(this.options).optimize(tokens);
if (this.options.advanced)
--- /dev/null
+var SourceMapGenerator = require('source-map').SourceMapGenerator;
+
+var lineBreak = require('os').EOL;
+
+function SourceMapStringifier(keepBreaks, restoreCallback) {
+ this.keepBreaks = keepBreaks;
+ this.restoreCallback = restoreCallback;
+ this.sourceMap = new SourceMapGenerator();
+}
+
+function valueRebuilder(list, store, separator) {
+ for (var i = 0, l = list.length; i < l; i++) {
+ store(list[i]);
+ store(i < l - 1 ? separator : '');
+ }
+}
+
+function rebuild(tokens, store, keepBreaks, isFlatBlock) {
+ var joinCharacter = isFlatBlock ? ';' : (keepBreaks ? lineBreak : '');
+
+ for (var i = 0, l = tokens.length; i < l; i++) {
+ var token = tokens[i];
+
+ if (token.kind === 'text' || token.kind == 'at-rule') {
+ store(token);
+ continue;
+ }
+
+ // FIXME: broken due to joining/splitting
+ if (token.body && (token.body.length === 0 || (token.body.length == 1 && token.body[0].value === '')))
+ continue;
+
+ if (token.kind == 'block') {
+ if (token.body.length > 0) {
+ valueRebuilder([{ value: token.value, metadata: token.metadata }], store, '');
+ store('{');
+ if (token.isFlatBlock)
+ valueRebuilder(token.body, store, ';');
+ else
+ rebuild(token.body, store, keepBreaks, false);
+ store('}');
+ }
+ } else {
+ valueRebuilder(token.value, store, ',');
+ store('{');
+ valueRebuilder(token.body, store, ';');
+ store('}');
+ }
+
+ store(joinCharacter);
+ }
+}
+
+function track(context, value, metadata) {
+ if (metadata) {
+ context.sourceMap.addMapping({
+ generated: {
+ line: context.line,
+ column: context.column,
+ },
+ source: metadata.source || '__stdin__.css',
+ original: {
+ line: metadata.line,
+ column: metadata.column
+ },
+ name: value
+ });
+ }
+
+ var parts = value.split('\n');
+ context.line += parts.length - 1;
+ context.column = parts.length > 1 ? 1 : (context.column + parts.pop().length);
+}
+
+SourceMapStringifier.prototype.toString = function (tokens) {
+ var self = this;
+ var output = [];
+ var context = {
+ column: 1,
+ line: 1,
+ sourceMap: this.sourceMap
+ };
+
+ function store(token) {
+ if (typeof token == 'string') {
+ track(context, token);
+ output.push(token);
+ } else {
+ var val = self.restoreCallback(token.value);
+ track(context, val, token.metadata);
+ output.push(val);
+ }
+ }
+
+ rebuild(tokens, store, this.keepBreaks, false);
+
+ return {
+ sourceMap: this.sourceMap,
+ styles: output.join('').trim()
+ };
+};
+
+module.exports = SourceMapStringifier;
"test": "vows"
},
"dependencies": {
- "commander": "2.5.x"
+ "commander": "2.5.x",
+ "source-map": "0.1.x"
},
"devDependencies": {
"browserify": "6.x",
var assert = require('assert');
var path = require('path');
var CleanCSS = require('../index');
+var SourceMapGenerator = require('source-map').SourceMapGenerator;
vows.describe('module tests').addBatch({
'imported as a function': {
this.callback(null, minified, minifier);
},
'should output correct content': function(error, minified) {
- assert.equal(minified, 'a{background:url(image/}');
+ assert.equal(minified.styles, 'a{background:url(image/}');
},
'should raise no errors': function(error, minified, minifier) {
assert.equal(minifier.errors.length, 0);
this.callback(null, minified, minifier);
},
'should output correct content': function(error, minified) {
- assert.equal(minified, '');
+ assert.equal(minified.styles, '');
},
'should raise no errors': function(error, minified, minifier) {
assert.equal(minifier.errors.length, 0);
this.callback(null, minified, minifier);
},
'should output correct content': function(error, minified) {
- assert.equal(minified, '');
+ assert.equal(minified.styles, '');
},
'should raise no errors': function(error, minified, minifier) {
assert.equal(minifier.errors.length, 0);
assert.include(minified.styles, 'url(/test/data/dummy.png)');
}
}
+ },
+ 'source map': {
+ 'topic': new CleanCSS({ sourceMap: true }).minify('/*! a */div[data-id=" abc "] { color:red; }'),
+ 'should minify correctly': function (minified) {
+ assert.equal('/*! a */div[data-id=" abc "]{color:red}', minified.styles);
+ },
+ 'should include source map': function (minified) {
+ assert.instanceOf(minified.sourceMap, SourceMapGenerator);
+ }
}
}).export(module);
--- /dev/null
+var vows = require('vows');
+var assert = require('assert');
+var CleanCSS = require('../index');
+
+vows.describe('source-map')
+ .addBatch({
+ 'module #1': {
+ 'topic': new CleanCSS({ sourceMap: true }).minify('/*! a */div[data-id=" abc "] { color:red; }'),
+ 'should have 2 mappings': function(minified) {
+ assert.equal(2, minified.sourceMap._mappings.length);
+ },
+ 'should have selector mapping': function (minified) {
+ var mapping = {
+ generatedLine: 1,
+ generatedColumn: 9,
+ originalLine: 1,
+ originalColumn: 9,
+ source: '__stdin__.css',
+ name: 'div[data-id=" abc "]'
+ };
+ assert.deepEqual(mapping, minified.sourceMap._mappings[0]);
+ },
+ 'should have body mapping': function (minified) {
+ var mapping = {
+ generatedLine: 1,
+ generatedColumn: 30,
+ originalLine: 1,
+ originalColumn: 32,
+ source: '__stdin__.css',
+ name: 'color:red'
+ };
+ assert.deepEqual(mapping, minified.sourceMap._mappings[1]);
+ }
+ },
+ 'module #2': {
+ 'topic': new CleanCSS({ sourceMap: true }).minify('@media screen {\n@font-face \n{ \nfont-family: test; } }'),
+ 'should have 3 mappings': function(minified) {
+ assert.equal(3, minified.sourceMap._mappings.length);
+ },
+ 'should have @media mapping': function (minified) {
+ var mapping = {
+ generatedLine: 1,
+ generatedColumn: 1,
+ originalLine: 1,
+ originalColumn: 1,
+ source: '__stdin__.css',
+ name: '@media screen'
+ };
+ assert.deepEqual(mapping, minified.sourceMap._mappings[0]);
+ },
+ 'should have @font-face mapping': function (minified) {
+ var mapping = {
+ generatedLine: 1,
+ generatedColumn: 15,
+ originalLine: 2,
+ originalColumn: 1,
+ source: '__stdin__.css',
+ name: '@font-face'
+ };
+ assert.deepEqual(mapping, minified.sourceMap._mappings[1]);
+ },
+ 'should have font-family mapping': function (minified) {
+ var mapping = {
+ generatedLine: 1,
+ generatedColumn: 26,
+ originalLine: 4,
+ originalColumn: 1,
+ source: '__stdin__.css',
+ name: 'font-family:test'
+ };
+ assert.deepEqual(mapping, minified.sourceMap._mappings[2]);
+ }
+ },
+ 'with keepBreaks': {
+ 'topic': new CleanCSS({ sourceMap: true, keepBreaks: true }).minify('@media screen { a{color:red} p {color:blue} }div{color:pink}'),
+ 'should have 7 mappings': function(minified) {
+ assert.equal(7, minified.sourceMap._mappings.length);
+ },
+ 'should have @media mapping': function (minified) {
+ var mapping = {
+ generatedLine: 1,
+ generatedColumn: 1,
+ originalLine: 1,
+ originalColumn: 1,
+ source: '__stdin__.css',
+ name: '@media screen'
+ };
+ assert.deepEqual(mapping, minified.sourceMap._mappings[0]);
+ },
+ 'should have _a_ mapping': function (minified) {
+ var mapping = {
+ generatedLine: 1,
+ generatedColumn: 15,
+ originalLine: 1,
+ originalColumn: 17,
+ source: '__stdin__.css',
+ name: 'a'
+ };
+ assert.deepEqual(mapping, minified.sourceMap._mappings[1]);
+ },
+ 'should have _color:red_ mapping': function (minified) {
+ var mapping = {
+ generatedLine: 1,
+ generatedColumn: 17,
+ originalLine: 1,
+ originalColumn: 19,
+ source: '__stdin__.css',
+ name: 'color:red'
+ };
+ assert.deepEqual(mapping, minified.sourceMap._mappings[2]);
+ },
+ 'should have _p_ mapping': function (minified) {
+ var mapping = {
+ generatedLine: 2,
+ generatedColumn: 1,
+ originalLine: 1,
+ originalColumn: 30,
+ source: '__stdin__.css',
+ name: 'p'
+ };
+ assert.deepEqual(mapping, minified.sourceMap._mappings[3]);
+ },
+ 'should have _color:blue_ mapping': function (minified) {
+ var mapping = {
+ generatedLine: 2,
+ generatedColumn: 3,
+ originalLine: 1,
+ originalColumn: 33,
+ source: '__stdin__.css',
+ name: 'color:#00f'
+ };
+ assert.deepEqual(mapping, minified.sourceMap._mappings[4]);
+ },
+ 'should have _div_ mapping': function (minified) {
+ var mapping = {
+ generatedLine: 4,
+ generatedColumn: 1,
+ originalLine: 1,
+ originalColumn: 46,
+ source: '__stdin__.css',
+ name: 'div'
+ };
+ assert.deepEqual(mapping, minified.sourceMap._mappings[5]);
+ },
+ 'should have _color:pink_ mapping': function (minified) {
+ var mapping = {
+ generatedLine: 4,
+ generatedColumn: 5,
+ originalLine: 1,
+ originalColumn: 50,
+ source: '__stdin__.css',
+ name: 'color:pink'
+ };
+ assert.deepEqual(mapping, minified.sourceMap._mappings[6]);
+ }
+ }
+ })
+ .export(module);