* InputSourceMapTracker tracks source maps on per-file basis.
var UrlsProcessor = require('./text/urls-processor');
var Compatibility = require('./utils/compatibility');
-
-var fs = require('fs');
-var path = require('path');
-
-var SOURCE_MAP_MARKER = '/*# sourceMappingURL=';
+var InputSourceMapTracker = require('./utils/input-source-map-tracker');
var CleanCSS = module.exports = function CleanCSS(options) {
options = options || {};
if (Buffer.isBuffer(data))
data = data.toString();
- if (data.indexOf(SOURCE_MAP_MARKER) > 0)
- overrideSourceMap(self.options, data);
-
if (options.processImport || data.indexOf('@shallow') > 0) {
// inline all imports
var runner = callback ?
}
};
-function overrideSourceMap(options, source) {
- var markerAt = source.indexOf(SOURCE_MAP_MARKER);
- var inputMapPath = source.substring(markerAt + SOURCE_MAP_MARKER.length, source.indexOf('*/', markerAt)).trim();
- options.sourceMap = fs.readFileSync(path.join(options.root || '', inputMapPath), 'utf-8');
-}
-
function runMinifier(callback, self) {
return function (data) {
+ self.inputSourceMapTracker = new InputSourceMapTracker(self.options).track(data);
+
data = self.options.debug ?
minifyWithDebug(self, data) :
minify.call(self, data);
function minifyWithDebug(self, data) {
var startedAt = process.hrtime();
- self.stats.originalSize = data.replace(/__ESCAPED_SOURCE_CLEAN_CSS\(.+\)__/g, '').length;
+ self.stats.originalSize = data.replace(/__ESCAPED_SOURCE_CLEAN_CSS\(.+\)__/g, '').replace(/__ESCAPED_SOURCE_END_CLEAN_CSS__/g, '').length;
data = minify.call(self, data);
function minify(data) {
var options = this.options;
var context = this.context;
+ var sourceMapTracker = this.inputSourceMapTracker;
var commentsProcessor = new CommentsProcessor(context, options.keepSpecialComments, options.keepBreaks, options.sourceMap);
var expressionsProcessor = new ExpressionsProcessor(options.sourceMap);
data = options.rebase ? urlRebase.process(data) : data;
data = expressionsProcessor.restore(data);
return commentsProcessor.restore(data);
- });
+ }, sourceMapTracker);
return selectorsOptimizer.process(data, stringifier);
});
return target;
};
+function wrap(data, source) {
+ return '__ESCAPED_SOURCE_CLEAN_CSS(' + source + ')__' +
+ data +
+ '__ESCAPED_SOURCE_END_CLEAN_CSS__';
+}
+
module.exports = function Inliner(context, options) {
var defaultOptions = {
timeout: 5000,
res.on('end', function() {
var importedData = chunks.join('');
importedData = UrlRewriter.process(importedData, { toBase: importedUrl });
- importedData = '__ESCAPED_SOURCE_CLEAN_CSS(' + url + ')__' + importedData;
+ importedData = wrap(importedData, url);
if (mediaQuery.length > 0)
importedData = '@media ' + mediaQuery + '{' + importedData + '}';
fromBase: importRelativeTo,
toBase: options._baseRelativeTo
});
- importedData = '__ESCAPED_SOURCE_CLEAN_CSS(' + path.relative(options.root, fullPath) + ')__' + importedData;
+ importedData = wrap(importedData, path.relative(options.root, fullPath));
if (mediaQuery.length > 0)
importedData = '@media ' + mediaQuery + '{' + importedData + '}';
-var SourceMapConsumer = require('source-map').SourceMapConsumer;
var SourceMapGenerator = require('source-map').SourceMapGenerator;
var lineBreak = require('os').EOL;
-function SourceMapStringifier(options, restoreCallback) {
+function SourceMapStringifier(options, restoreCallback, inputMapTracker) {
this.keepBreaks = options.keepBreaks;
this.restoreCallback = restoreCallback;
this.outputMap = new SourceMapGenerator();
- this.inputMap = typeof options.sourceMap == 'string' ?
- new SourceMapConsumer(options.sourceMap) :
- null;
+ this.inputMapTracker = inputMapTracker;
}
function valueRebuilder(list, store, separator) {
function track(context, value, metadata) {
if (metadata) {
- var original = context.inputMap ?
- context.inputMap.originalPositionFor(metadata) :
+ var original = context.inputMapTracker.isTracking() ?
+ context.inputMapTracker.originalPositionFor(metadata) :
{};
context.outputMap.addMapping({
var context = {
column: 1,
line: 1,
- inputMap: this.inputMap,
+ inputMapTracker: this.inputMapTracker,
outputMap: this.outputMap
};
outer: this.minifyContext,
addMetadata: this.addMetadata,
addSourceMap: this.addSourceMap,
+ state: [],
line: 1,
column: 1,
source: undefined
} else if (what == 'escape') {
nextEnd = chunk.indexOf('__', nextSpecial + 1);
var escaped = chunk.substring(context.cursor, nextEnd + 2);
- var isSourceMarker = escaped.indexOf('__ESCAPED_SOURCE_CLEAN_CSS') > -1;
+ var isStartSourceMarker = escaped.indexOf('__ESCAPED_SOURCE_CLEAN_CSS') > -1;
+ var isEndSourceMarker = escaped.indexOf('__ESCAPED_SOURCE_END_CLEAN_CSS') > -1;
- if (isSourceMarker) {
+ if (isStartSourceMarker) {
+ if (addSourceMap)
+ SourceMaps.track(escaped, context);
+
+ context.state.push({
+ source: context.source,
+ line: context.line,
+ column: context.column
+ });
context.source = escaped.substring(escaped.indexOf('(') + 1, escaped.indexOf(')'));
+ context.line = 1;
+ context.column = 1;
+ } else if (isEndSourceMarker) {
+ var oldState = context.state.pop();
+ context.source = oldState.source;
+ context.line = oldState.line;
+ context.column = oldState.column;
+
+ if (addSourceMap)
+ SourceMaps.track(escaped, context);
} else {
tokenized.push({ kind: 'text', value: escaped });
- }
- if (addSourceMap)
- SourceMaps.track(escaped, context);
+ if (addSourceMap)
+ SourceMaps.track(escaped, context);
+ }
context.cursor = nextEnd + 2;
} else if (what == 'bodyStart') {
--- /dev/null
+var SourceMapConsumer = require('source-map').SourceMapConsumer;
+
+var fs = require('fs');
+var path = require('path');
+
+var SOURCE_MARKER_START = /__ESCAPED_SOURCE_CLEAN_CSS\(([^~][^\)]+)\)__/;
+var SOURCE_MARKER_END = /__ESCAPED_SOURCE_END_CLEAN_CSS__/;
+var MAP_MARKER = /\/\*# sourceMappingURL=(\S+) \*\//;
+
+function InputSourceMapStore(options) {
+ this.options = options;
+ this.maps = {};
+}
+
+InputSourceMapStore.prototype.track = function (data) {
+ if (typeof this.options.sourceMap == 'string') {
+ this.maps[undefined] = new SourceMapConsumer(this.options.sourceMap);
+ this.options.sourceMap = true;
+ return this;
+ }
+
+ var files = [];
+ for (var cursor = 0, len = data.length; cursor < len; ) {
+ var fragment = data.substring(cursor, len);
+
+ var markerStartMatch = SOURCE_MARKER_START.exec(fragment) || { index: -1 };
+ var markerEndMatch = SOURCE_MARKER_END.exec(fragment) || { index: -1 };
+ var mapMatch = MAP_MARKER.exec(fragment) || { index: -1 };
+
+ var nextAt = len;
+ if (markerStartMatch.index > -1)
+ nextAt = markerStartMatch.index;
+ if (markerEndMatch.index > -1 && markerEndMatch.index < nextAt)
+ nextAt = markerEndMatch.index;
+ if (mapMatch.index > -1 && mapMatch.index < nextAt)
+ nextAt = mapMatch.index;
+
+ if (nextAt == len)
+ break;
+
+ if (nextAt == markerStartMatch.index) {
+ files.push(markerStartMatch[1]);
+ } else if (nextAt == markerEndMatch.index) {
+ files.pop();
+ } else if (nextAt == mapMatch.index) {
+ var inputMapData = fs.readFileSync(path.join(this.options.root || '', mapMatch[1]), 'utf-8');
+ this.maps[files[files.length - 1] || undefined] = new SourceMapConsumer(inputMapData);
+ this.options.sourceMap = true;
+ }
+
+ cursor += nextAt + 1;
+ }
+
+ return this;
+};
+
+InputSourceMapStore.prototype.isTracking = function () {
+ return Object.keys(this.maps).length > 0;
+};
+
+InputSourceMapStore.prototype.originalPositionFor = function (sourceInfo) {
+ return this.maps[sourceInfo.source].originalPositionFor(sourceInfo);
+};
+
+module.exports = InputSourceMapStore;
--- /dev/null
+@import url(some.css);
+@import url(styles.css);
--- /dev/null
+div {
+ color: red;
+}
+/*# sourceMappingURL=some.css.map */
\ No newline at end of file
--- /dev/null
+{"version":3,"sources":["some.less"],"names":[],"mappings":"AAAA;EACE,UAAA","file":"some.css"}
\ No newline at end of file
--- /dev/null
+div > a {
+ color: blue;
+}
+/*# sourceMappingURL=styles.css.map */
-{"version":3,"sources":["styles.less"],"names":[],"mappings":"AAAA,GACE;EACE,UAAA","file":"styles.css"}
\ No newline at end of file
+{"version":3,"sources":["styles.less"],"names":[],"mappings":"AAAA,GACE;EACE,WAAA","file":"styles.css"}
\ No newline at end of file
.addBatch(
sourceMapContext('sources', {
'one': [
- '__ESCAPED_SOURCE_CLEAN_CSS(one.css)__a{}',
+ '__ESCAPED_SOURCE_CLEAN_CSS(one.css)__a{}__ESCAPED_SOURCE_END_CLEAN_CSS__',
[{
kind: 'selector',
value: [{ value: 'a', metadata: { line: 1, column: 1, source: 'one.css' } }],
}]
],
'two': [
- '__ESCAPED_SOURCE_CLEAN_CSS(one.css)__a{}\n__ESCAPED_SOURCE_CLEAN_CSS(two.css)__a{color:red}',
+ '__ESCAPED_SOURCE_CLEAN_CSS(one.css)__a{}__ESCAPED_SOURCE_END_CLEAN_CSS____ESCAPED_SOURCE_CLEAN_CSS(two.css)__\na{color:red}__ESCAPED_SOURCE_END_CLEAN_CSS__',
[
{
kind: 'selector',
{
kind: 'selector',
value: [
- { value: 'a', metadata: { line: 2, column: 1, source: 'two.css' } }
+ { value: '\na', metadata: { line: 2, column: 1, source: 'two.css' } }
],
body: [{ value: 'color:red', metadata: { line: 2, column: 3, source: 'two.css' } }]
}
var fs = require('fs');
var path = require('path');
-var inputMapPath = path.join('test', 'data', 'source-maps', 'sample.map');
+var inputMapPath = path.join('test', 'data', 'source-maps', 'styles.css.map');
var inputMap = fs.readFileSync(inputMapPath, 'utf-8');
vows.describe('source-map')
}
},
'input map from source with root': {
- 'topic': new CleanCSS({ root: path.dirname(inputMapPath) }).minify('div > a {\n color: red;\n}/*# sourceMappingURL=sample.map */'),
+ 'topic': new CleanCSS({ root: path.dirname(inputMapPath) }).minify('div > a {\n color: red;\n}/*# sourceMappingURL=styles.css.map */'),
'should have 2 mappings': function (minified) {
assert.equal(2, minified.sourceMap._mappings.length);
},
};
assert.deepEqual(mapping, minified.sourceMap._mappings[1]);
}
+ },
+ 'complex input map': {
+ 'topic': new CleanCSS({ root: path.dirname(inputMapPath) }).minify('@import url(import.css);'),
+ 'should have 4 mappings': function (minified) {
+ assert.equal(4, minified.sourceMap._mappings.length);
+ },
+ 'should have first selector mapping': function (minified) {
+ var mapping = {
+ generatedLine: 1,
+ generatedColumn: 1,
+ originalLine: 1,
+ originalColumn: 1,
+ source: 'some.less',
+ name: 'div'
+ };
+ assert.deepEqual(mapping, minified.sourceMap._mappings[0]);
+ },
+ 'should have _color:red_ mapping': function (minified) {
+ var mapping = {
+ generatedLine: 1,
+ generatedColumn: 5,
+ originalLine: 2,
+ originalColumn: 2,
+ source: 'some.less',
+ name: 'color:red'
+ };
+ assert.deepEqual(mapping, minified.sourceMap._mappings[1]);
+ },
+ 'should have second selector mapping': function (minified) {
+ var mapping = {
+ generatedLine: 2,
+ generatedColumn: 1,
+ originalLine: 1,
+ originalColumn: 1,
+ source: 'styles.less',
+ name: 'div>a'
+ };
+ assert.deepEqual(mapping, minified.sourceMap._mappings[2]);
+ },
+ 'should have _color:blue_ mapping': function (minified) {
+ var mapping = {
+ generatedLine: 2,
+ generatedColumn: 7,
+ originalLine: 3,
+ originalColumn: 4,
+ source: 'styles.less',
+ name: 'color:#00f'
+ };
+ assert.deepEqual(mapping, minified.sourceMap._mappings[3]);
+ }
}
})
.export(module);