From: Jakub Pawlowicz Date: Thu, 6 Nov 2014 13:45:48 +0000 (+0000) Subject: Adds source map stringifier. X-Git-Url: https://git.ndcode.org/public/gitweb.cgi?a=commitdiff_plain;h=e2b309f93db0442e423e3006901132c642b258bb;p=clean-css.git Adds source map stringifier. * 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. --- diff --git a/README.md b/README.md index 9bc5de12..5e444a98 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,7 @@ CleanCSS constructor accepts a hash as a parameter, i.e., * `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? diff --git a/lib/clean.js b/lib/clean.js index 703fb5a8..1db575b7 100644 --- a/lib/clean.js +++ b/lib/clean.js @@ -9,6 +9,7 @@ var ImportInliner = require('./imports/inliner'); 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'); @@ -34,6 +35,7 @@ var CleanCSS = module.exports = function CleanCSS(options) { relativeTo: options.relativeTo, root: options.root, roundingPrecision: options.roundingPrecision, + sourceMap: !!options.sourceMap, target: options.target }; @@ -113,13 +115,14 @@ function minify(data) { 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' ? @@ -136,7 +139,7 @@ function minify(data) { 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; diff --git a/lib/selectors/optimizer.js b/lib/selectors/optimizer.js index 3f6abf4e..051d5e96 100644 --- a/lib/selectors/optimizer.js +++ b/lib/selectors/optimizer.js @@ -8,7 +8,7 @@ function SelectorsOptimizer(options, context) { } 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) diff --git a/lib/selectors/source-map-stringifier.js b/lib/selectors/source-map-stringifier.js new file mode 100644 index 00000000..ea185395 --- /dev/null +++ b/lib/selectors/source-map-stringifier.js @@ -0,0 +1,103 @@ +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; diff --git a/package.json b/package.json index 3e59c9a8..5f1df2e9 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,8 @@ "test": "vows" }, "dependencies": { - "commander": "2.5.x" + "commander": "2.5.x", + "source-map": "0.1.x" }, "devDependencies": { "browserify": "6.x", diff --git a/test/module-test.js b/test/module-test.js index f478c7e2..be02e4d7 100644 --- a/test/module-test.js +++ b/test/module-test.js @@ -2,6 +2,7 @@ var vows = require('vows'); 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': { @@ -162,7 +163,7 @@ vows.describe('module tests').addBatch({ 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); @@ -179,7 +180,7 @@ vows.describe('module tests').addBatch({ 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); @@ -196,7 +197,7 @@ vows.describe('module tests').addBatch({ 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); @@ -262,5 +263,14 @@ vows.describe('module tests').addBatch({ 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); diff --git a/test/source-map-test.js b/test/source-map-test.js new file mode 100644 index 00000000..0654acfc --- /dev/null +++ b/test/source-map-test.js @@ -0,0 +1,158 @@ +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);