From 78928d469dcff2a2b0376e2679fd47bf92aaaedb Mon Sep 17 00:00:00 2001 From: Jakub Pawlowicz Date: Tue, 11 Nov 2014 18:41:54 +0000 Subject: [PATCH] Adds basic support for input source maps. * `sourceMap` option can be given as source map string. * Particularly useful for preprocessed stylesheets. --- README.md | 1 + lib/clean.js | 4 ++-- lib/selectors/source-map-stringifier.js | 27 ++++++++++++++------- lib/selectors/stringifier.js | 4 ++-- test/source-map-test.js | 32 +++++++++++++++++++++++++ 5 files changed, 55 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 4705230d..248c14b3 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,7 @@ CleanCSS constructor accepts a hash as a parameter, i.e., * `roundingPrecision` - rounding precision; defaults to `2`; `-1` disables rounding * `shorthandCompacting` - set to false to skip shorthand compacting (default is true unless sourceMap is set when it's false) * `sourceMap` - exposes source map under `sourceMap` property, e.g. `new CleanCSS().minify(source).sourceMap` (default is false) + If input styles are a product of CSS preprocessor (LESS, SASS) an input source map can be passed as a string. * `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 babb0f2a..d0c1f9d0 100644 --- a/lib/clean.js +++ b/lib/clean.js @@ -36,7 +36,7 @@ var CleanCSS = module.exports = function CleanCSS(options) { root: options.root, roundingPrecision: options.roundingPrecision, shorthandCompacting: !!options.sourceMap ? false : (undefined === options.shorthandCompacting ? true : !!options.shorthandCompacting), - sourceMap: !!options.sourceMap, + sourceMap: options.sourceMap, target: options.target }; @@ -140,7 +140,7 @@ function minify(data) { run(freeTextProcessor, 'escape'); run(function() { - var stringifier = new stringifierClass(options.keepBreaks, function (data) { + var stringifier = new stringifierClass(options, function (data) { data = freeTextProcessor.restore(data); data = urlsProcessor.restore(data); data = options.rebase ? urlRebase.process(data) : data; diff --git a/lib/selectors/source-map-stringifier.js b/lib/selectors/source-map-stringifier.js index ea185395..86e46037 100644 --- a/lib/selectors/source-map-stringifier.js +++ b/lib/selectors/source-map-stringifier.js @@ -1,11 +1,15 @@ +var SourceMapConsumer = require('source-map').SourceMapConsumer; var SourceMapGenerator = require('source-map').SourceMapGenerator; var lineBreak = require('os').EOL; -function SourceMapStringifier(keepBreaks, restoreCallback) { - this.keepBreaks = keepBreaks; +function SourceMapStringifier(options, restoreCallback) { + this.keepBreaks = options.keepBreaks; this.restoreCallback = restoreCallback; - this.sourceMap = new SourceMapGenerator(); + this.outputMap = new SourceMapGenerator(); + this.inputMap = typeof options.sourceMap == 'string' ? + new SourceMapConsumer(options.sourceMap) : + null; } function valueRebuilder(list, store, separator) { @@ -53,15 +57,19 @@ function rebuild(tokens, store, keepBreaks, isFlatBlock) { function track(context, value, metadata) { if (metadata) { - context.sourceMap.addMapping({ + var original = context.inputMap ? + context.inputMap.originalPositionFor(metadata) : + {}; + + context.outputMap.addMapping({ generated: { line: context.line, column: context.column, }, - source: metadata.source || '__stdin__.css', + source: original.source || metadata.source || '__stdin__.css', original: { - line: metadata.line, - column: metadata.column + line: original.line || metadata.line, + column: original.column || metadata.column }, name: value }); @@ -78,7 +86,8 @@ SourceMapStringifier.prototype.toString = function (tokens) { var context = { column: 1, line: 1, - sourceMap: this.sourceMap + inputMap: this.inputMap, + outputMap: this.outputMap }; function store(token) { @@ -95,7 +104,7 @@ SourceMapStringifier.prototype.toString = function (tokens) { rebuild(tokens, store, this.keepBreaks, false); return { - sourceMap: this.sourceMap, + sourceMap: this.outputMap, styles: output.join('').trim() }; }; diff --git a/lib/selectors/stringifier.js b/lib/selectors/stringifier.js index fcbc3d96..685a0cf2 100644 --- a/lib/selectors/stringifier.js +++ b/lib/selectors/stringifier.js @@ -1,7 +1,7 @@ var lineBreak = require('os').EOL; -function Stringifier(keepBreaks, restoreCallback) { - this.keepBreaks = keepBreaks; +function Stringifier(options, restoreCallback) { + this.keepBreaks = options.keepBreaks; this.restoreCallback = restoreCallback; } diff --git a/test/source-map-test.js b/test/source-map-test.js index 31169691..8d94c985 100644 --- a/test/source-map-test.js +++ b/test/source-map-test.js @@ -2,6 +2,8 @@ var vows = require('vows'); var assert = require('assert'); var CleanCSS = require('../index'); +var inputSourceMap = '{"version":3,"sources":["styles.less"],"names":[],"mappings":"AAAA,GACE;EACE,UAAA","file":"styles.css"}'; + vows.describe('source-map') .addBatch({ 'module #1': { @@ -194,4 +196,34 @@ vows.describe('source-map') } }, }) + .addBatch({ + 'input map as string': { + 'topic': new CleanCSS({ sourceMap: inputSourceMap }).minify('div > a {\n color: red;\n}'), + 'should have 2 mappings': function (minified) { + assert.equal(2, minified.sourceMap._mappings.length); + }, + 'should have selector mapping': function (minified) { + var mapping = { + generatedLine: 1, + generatedColumn: 1, + originalLine: 1, + originalColumn: 1, + source: 'styles.less', + name: 'div>a' + }; + assert.deepEqual(mapping, minified.sourceMap._mappings[0]); + }, + 'should have _color:red_ mapping': function (minified) { + var mapping = { + generatedLine: 1, + generatedColumn: 7, + originalLine: 3, + originalColumn: 4, + source: 'styles.less', + name: 'color:red' + }; + assert.deepEqual(mapping, minified.sourceMap._mappings[1]); + } + } + }) .export(module); -- 2.34.1