From 8c1f2ecd95ebc0aa1ce2f473580d3e947d89b350 Mon Sep 17 00:00:00 2001 From: Andrey Popp <8mayday@gmail.com> Date: Sun, 3 Nov 2013 02:30:13 +0400 Subject: [PATCH 01/14] Initial support for source map generation --- index.js | 6 +- lib/identity.js | 179 +++++++++++++++++++++++++++++++++--------------- package.json | 3 + 3 files changed, 131 insertions(+), 57 deletions(-) diff --git a/index.js b/index.js index 193b75e..a3bb2e3 100644 --- a/index.js +++ b/index.js @@ -22,6 +22,10 @@ module.exports = function(node, options){ ? new Compressed(options) : new Identity(options); - return compiler.compile(node); + var code = compiler.compile(node); + if (options.sourceMap) + return {code: code, map: compiler.map} + else + return code; }; diff --git a/lib/identity.js b/lib/identity.js index abf97f4..81a4ce8 100644 --- a/lib/identity.js +++ b/lib/identity.js @@ -1,3 +1,4 @@ +var SourceMapGenerator = require('source-map').SourceMapGenerator; /** * Expose compiler. @@ -11,9 +12,56 @@ module.exports = Compiler; function Compiler(options) { options = options || {}; + this.map = new SourceMapGenerator({file: options.filename || 'generated.css'}); + this.position = {line: 1, column: 0}; this.indentation = options.indent; } +/** + * Emit string and update current position + */ + +Compiler.prototype.updatePosition = function(str) { + var lines = str.match(/\n/g); + if (lines) this.position.line += lines.length; + var i = str.lastIndexOf('\n'); + this.position.column = ~i ? str.length - i : this.position.column + str.length; +} + +Compiler.prototype.emit = function(str, pos, startOnly) { + if (pos && pos.start) { + this.map.addMapping({ + generated: { + line: this.position.line, + column: Math.max(this.position.column - 1, 0) + }, + source: 'source.css', + original: { + line: pos.start.line, + column: pos.start.column - 1 + } + }); + } + + this.updatePosition(str); + + if (!startOnly && pos && pos.end) { + this.map.addMapping({ + generated: { + line: this.position.line, + column: Math.max(this.position.column - 1, 0) + }, + source: 'source.css', + original: { + line: pos.end.line, + column: pos.end.column - 1 + } + }); + } + + return str; +} + /** * Compile `node`. */ @@ -30,14 +78,22 @@ Compiler.prototype.visit = function(node){ return this[node.type](node); }; +Compiler.prototype.visitMap = function(nodes, join){ + join = join || ''; + var result = ''; + for (var i = 0, length = nodes.length; i < length; i++) { + result += this.visit(nodes[i]); + if (i < length - 1) result += this.emit(join); + } + return result; +}; + /** * Visit stylesheet node. */ Compiler.prototype.stylesheet = function(node){ - return node.stylesheet - .rules.map(this.visit, this) - .join('\n\n'); + return this.visitMap(node.stylesheet.rules, '\n\n'); }; /** @@ -45,7 +101,7 @@ Compiler.prototype.stylesheet = function(node){ */ Compiler.prototype.comment = function(node){ - return this.indent() + '/*' + node.comment + '*/'; + return this.emit(this.indent() + '/*' + node.comment + '*/', node.position); }; /** @@ -53,7 +109,7 @@ Compiler.prototype.comment = function(node){ */ Compiler.prototype.import = function(node){ - return '@import ' + node.import + ';'; + return this.emit('@import ' + node.import + ';', node.position); }; /** @@ -61,13 +117,14 @@ Compiler.prototype.import = function(node){ */ Compiler.prototype.media = function(node){ - return '@media ' - + node.media - + ' {\n' - + this.indent(1) - + node.rules.map(this.visit, this).join('\n\n') - + this.indent(-1) - + '\n}'; + return this.emit('@media ' + node.media, node.position, true) + + this.emit( + ' {\n' + + this.indent(1)) + + this.visitMap(node.rules, '\n\n') + + this.emit( + this.indent(-1) + + '\n}'); }; /** @@ -77,12 +134,15 @@ Compiler.prototype.media = function(node){ Compiler.prototype.document = function(node){ var doc = '@' + (node.vendor || '') + 'document ' + node.document; - return doc + ' ' - + ' {\n' - + this.indent(1) - + node.rules.map(this.visit, this).join('\n\n') - + this.indent(-1) - + '\n}'; + return this.emit(doc, node.position, true) + + this.emit( + ' ' + + ' {\n' + + this.indent(1)) + + this.visitMap(node.rules, '\n\n') + + this.emit( + this.indent(-1) + + '\n}'); }; /** @@ -90,7 +150,7 @@ Compiler.prototype.document = function(node){ */ Compiler.prototype.charset = function(node){ - return '@charset ' + node.charset + ';\n'; + return this.emit('@charset ' + node.charset + ';', node.position); }; /** @@ -98,7 +158,7 @@ Compiler.prototype.charset = function(node){ */ Compiler.prototype.namespace = function(node){ - return '@namespace ' + node.namespace + ';\n'; + return this.emit('@namespace ' + node.namespace + ';', node.position); }; /** @@ -106,13 +166,14 @@ Compiler.prototype.namespace = function(node){ */ Compiler.prototype.supports = function(node){ - return '@supports ' - + node.supports - + ' {\n' - + this.indent(1) - + node.rules.map(this.visit, this).join('\n\n') - + this.indent(-1) - + '\n}'; + return this.emit('@supports ' + node.supports, node.position, true) + + this.emit( + ' {\n' + + this.indent(1)) + + this.visitMap(node.rules, '\n\n') + + this.emit( + this.indent(-1) + + '\n}'); }; /** @@ -120,15 +181,14 @@ Compiler.prototype.supports = function(node){ */ Compiler.prototype.keyframes = function(node){ - return '@' - + (node.vendor || '') - + 'keyframes ' - + node.name - + ' {\n' - + this.indent(1) - + node.keyframes.map(this.visit, this).join('\n') - + this.indent(-1) - + '}'; + return this.emit('@' + (node.vendor || '') + 'keyframes ' + node.name, node.position, true) + + this.emit( + ' {\n' + + this.indent(1)) + + this.visitMap(node.keyframes, '\n') + + this.emit( + this.indent(-1) + + '}'); }; /** @@ -138,13 +198,16 @@ Compiler.prototype.keyframes = function(node){ Compiler.prototype.keyframe = function(node){ var decls = node.declarations; - return this.indent() - + node.values.join(', ') - + ' {\n' - + this.indent(1) - + decls.map(this.visit, this).join('\n') - + this.indent(-1) - + '\n' + this.indent() + '}\n'; + return this.emit(this.indent()) + + this.emit(node.values.join(', '), node.position, true) + + this.emit( + ' {\n' + + this.indent(1)) + + this.visitMap(decls, '\n') + + this.emit( + this.indent(-1) + + '\n' + + this.indent() + '}\n'); }; /** @@ -156,12 +219,12 @@ Compiler.prototype.page = function(node){ ? node.selectors.join(', ') + ' ' : ''; - return '@page ' + sel - + '{\n' - + this.indent(1) - + node.declarations.map(this.visit, this).join('\n') - + this.indent(-1) - + '\n}'; + return this.emit('@page ' + sel, node.position, true) + + this.emit('{\n') + + this.emit(this.indent(1)) + + this.visitMap(node.declarations, '\n') + + this.emit(this.indent(-1)) + + this.emit('\n}'); }; /** @@ -173,12 +236,12 @@ Compiler.prototype.rule = function(node){ var decls = node.declarations; if (!decls.length) return ''; - return node.selectors.map(function(s){ return indent + s }).join(',\n') - + ' {\n' - + this.indent(1) - + decls.map(this.visit, this).join('\n') - + this.indent(-1) - + '\n' + this.indent() + '}'; + return this.emit(node.selectors.map(function(s){ return indent + s }).join(',\n'), node.position, true) + + this.emit(' {\n') + + this.emit(this.indent(1)) + + this.visitMap(decls, '\n') + + this.emit(this.indent(-1)) + + this.emit('\n' + this.indent() + '}'); }; /** @@ -186,7 +249,11 @@ Compiler.prototype.rule = function(node){ */ Compiler.prototype.declaration = function(node){ - return this.indent() + node.property + ': ' + node.value + ';'; + return this.emit(this.indent()) + + this.emit(node.property, node.position) + + this.emit(': ') + + this.emit(node.value, node.position) + + this.emit(';'); }; /** diff --git a/package.json b/package.json index 305f585..8094e87 100644 --- a/package.json +++ b/package.json @@ -20,5 +20,8 @@ }, "scripts": { "test": "make test" + }, + "dependencies": { + "source-map": "~0.1.31" } } From c0e6fea0a0e6494b08565f91e15141ce3f6bf104 Mon Sep 17 00:00:00 2001 From: Andrey Popp <8mayday@gmail.com> Date: Sun, 3 Nov 2013 02:43:34 +0400 Subject: [PATCH 02/14] Fix how declarations are mapped --- lib/identity.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/identity.js b/lib/identity.js index 81a4ce8..0ce5658 100644 --- a/lib/identity.js +++ b/lib/identity.js @@ -17,10 +17,6 @@ function Compiler(options) { this.indentation = options.indent; } -/** - * Emit string and update current position - */ - Compiler.prototype.updatePosition = function(str) { var lines = str.match(/\n/g); if (lines) this.position.line += lines.length; @@ -28,6 +24,10 @@ Compiler.prototype.updatePosition = function(str) { this.position.column = ~i ? str.length - i : this.position.column + str.length; } +/** + * Emit string and update current position + */ + Compiler.prototype.emit = function(str, pos, startOnly) { if (pos && pos.start) { this.map.addMapping({ @@ -250,9 +250,7 @@ Compiler.prototype.rule = function(node){ Compiler.prototype.declaration = function(node){ return this.emit(this.indent()) - + this.emit(node.property, node.position) - + this.emit(': ') - + this.emit(node.value, node.position) + + this.emit(node.property + ': ' + node.value, node.position) + this.emit(';'); }; From 7a84c848df068adee8c962ba9116062fac874f45 Mon Sep 17 00:00:00 2001 From: Andrey Popp <8mayday@gmail.com> Date: Sun, 3 Nov 2013 02:43:47 +0400 Subject: [PATCH 03/14] Source mapping for compressing compiler --- lib/compress.js | 128 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 92 insertions(+), 36 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index 0a9b98b..36f52cb 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1,3 +1,4 @@ +var SourceMapGenerator = require('source-map').SourceMapGenerator; /** * Expose compiler. @@ -11,6 +12,53 @@ module.exports = Compiler; function Compiler(options) { options = options || {}; + this.map = new SourceMapGenerator({file: options.filename || 'generated.css'}); + this.position = {line: 1, column: 1}; +} + +Compiler.prototype.updatePosition = function(str) { + var lines = str.match(/\n/g); + if (lines) this.position.line += lines.length; + var i = str.lastIndexOf('\n'); + this.position.column = ~i ? str.length - i : this.position.column + str.length; +} + +/** + * Emit string and update current position + */ + +Compiler.prototype.emit = function(str, pos, startOnly) { + if (pos && pos.start) { + this.map.addMapping({ + generated: { + line: this.position.line, + column: Math.max(this.position.column - 1, 0) + }, + source: 'source.css', + original: { + line: pos.start.line, + column: pos.start.column - 1 + } + }); + } + + this.updatePosition(str); + + if (!startOnly && pos && pos.end) { + this.map.addMapping({ + generated: { + line: this.position.line, + column: Math.max(this.position.column - 1, 0) + }, + source: 'source.css', + original: { + line: pos.end.line, + column: pos.end.column - 1 + } + }); + } + + return str; } /** @@ -31,12 +79,22 @@ Compiler.prototype.visit = function(node){ return this[node.type](node); }; +Compiler.prototype.visitMap = function(nodes, join){ + join = join || ''; + var result = ''; + for (var i = 0, length = nodes.length; i < length; i++) { + result += this.visit(nodes[i]); + if (i < length - 1) result += this.emit(join); + } + return result; +}; + /** * Visit comment node. */ Compiler.prototype.comment = function(node){ - return ''; + return this.emit('', node.position); }; /** @@ -44,7 +102,7 @@ Compiler.prototype.comment = function(node){ */ Compiler.prototype.import = function(node){ - return '@import ' + node.import + ';'; + return this.emit('@import ' + node.import + ';', node.position); }; /** @@ -52,11 +110,10 @@ Compiler.prototype.import = function(node){ */ Compiler.prototype.media = function(node){ - return '@media ' - + node.media - + '{' - + node.rules.map(this.visit, this).join('') - + '}'; + return this.emit('@media ' + node.media, node.position, true) + + this.emit('{') + + this.visitMap(node.rules) + + this.emit('}'); }; /** @@ -66,10 +123,10 @@ Compiler.prototype.media = function(node){ Compiler.prototype.document = function(node){ var doc = '@' + (node.vendor || '') + 'document ' + node.document; - return doc - + '{' - + node.rules.map(this.visit, this).join('') - + '}'; + return this.emit(doc, node.position, true) + + this.emit('{') + + this.visitMap(node.rules) + + this.emit('}'); }; /** @@ -77,7 +134,7 @@ Compiler.prototype.document = function(node){ */ Compiler.prototype.charset = function(node){ - return '@charset ' + node.charset + ';'; + return this.emit('@charset ' + node.charset + ';', node.position); }; /** @@ -85,7 +142,7 @@ Compiler.prototype.charset = function(node){ */ Compiler.prototype.namespace = function(node){ - return '@namespace ' + node.namespace + ';'; + return this.emit('@namespace ' + node.namespace + ';', node.position); }; /** @@ -93,11 +150,10 @@ Compiler.prototype.namespace = function(node){ */ Compiler.prototype.supports = function(node){ - return '@supports ' - + node.supports - + '{' - + node.rules.map(this.visit, this).join('') - + '}'; + return this.emit('@supports ' + node.supports, node.position, true) + + this.emit('{') + + this.visitMap(node.rules) + + this.emit('}'); }; /** @@ -105,13 +161,13 @@ Compiler.prototype.supports = function(node){ */ Compiler.prototype.keyframes = function(node){ - return '@' + return this.emit('@' + (node.vendor || '') + 'keyframes ' - + node.name - + '{' - + node.keyframes.map(this.visit, this).join('') - + '}'; + + node.name, node.position, true) + + this.emit('{') + + this.visitMap(node.keyframes) + + this.emit('}'); }; /** @@ -121,10 +177,10 @@ Compiler.prototype.keyframes = function(node){ Compiler.prototype.keyframe = function(node){ var decls = node.declarations; - return node.values.join(',') - + '{' - + decls.map(this.visit, this).join('') - + '}'; + return this.emit(node.values.join(','), node.position, true) + + this.emit('{') + + this.visitMap(decls) + + this.emit('}'); }; /** @@ -136,10 +192,10 @@ Compiler.prototype.page = function(node){ ? node.selectors.join(', ') : ''; - return '@page ' + sel - + '{' - + node.declarations.map(this.visit, this).join('') - + '}'; + return this.emit('@page ' + sel, node.position, true) + + this.emit('{') + + this.visitMap(node.declarations) + + this.emit('}'); }; /** @@ -150,10 +206,10 @@ Compiler.prototype.rule = function(node){ var decls = node.declarations; if (!decls.length) return ''; - return node.selectors.join(',') - + '{' - + decls.map(this.visit, this).join('') - + '}'; + return this.emit(node.selectors.join(','), node.position, true) + + this.emit('{') + + this.visitMap(decls) + + this.emit('}'); }; /** @@ -161,6 +217,6 @@ Compiler.prototype.rule = function(node){ */ Compiler.prototype.declaration = function(node){ - return node.property + ':' + node.value + ';'; + return this.emit(node.property + ':' + node.value, node.position) + this.emit(';'); }; From 0f123d1d684f2ac3aaa4b784fb93886e8c9b127f Mon Sep 17 00:00:00 2001 From: Andrey Popp <8mayday@gmail.com> Date: Sun, 3 Nov 2013 03:40:08 +0400 Subject: [PATCH 04/14] Factor out common compiler code --- lib/compiler.js | 80 ++++++++++++++++++++++++++++++++++++++++++++++ lib/compress.js | 83 ++++++------------------------------------------ lib/identity.js | 84 +++++++------------------------------------------ 3 files changed, 101 insertions(+), 146 deletions(-) create mode 100644 lib/compiler.js diff --git a/lib/compiler.js b/lib/compiler.js new file mode 100644 index 0000000..265c56d --- /dev/null +++ b/lib/compiler.js @@ -0,0 +1,80 @@ +var SourceMapGenerator = require('source-map').SourceMapGenerator; + +module.exports = Compiler; + +function Compiler(options) { + options = options || {}; + this.map = new SourceMapGenerator({file: options.filename || 'generated.css'}); + this.position = {line: 1, column: 1}; +} + +/** + * Update current position according to `str` being emitted + */ +Compiler.prototype.updatePosition = function(str) { + var lines = str.match(/\n/g); + if (lines) this.position.line += lines.length; + var i = str.lastIndexOf('\n'); + this.position.column = ~i ? str.length - i : this.position.column + str.length; +} + +/** + * Emit `str` and update current position, use original source `pos` to + * construct source mapping. + */ + +Compiler.prototype.emit = function(str, pos, startOnly) { + if (pos && pos.start) { + this.map.addMapping({ + generated: { + line: this.position.line, + column: Math.max(this.position.column - 1, 0) + }, + source: 'source.css', + original: { + line: pos.start.line, + column: pos.start.column - 1 + } + }); + } + + this.updatePosition(str); + + if (!startOnly && pos && pos.end) { + this.map.addMapping({ + generated: { + line: this.position.line, + column: Math.max(this.position.column - 1, 0) + }, + source: 'source.css', + original: { + line: pos.end.line, + column: pos.end.column - 1 + } + }); + } + + return str; +} + + +/** + * Visit `node`. + */ + +Compiler.prototype.visit = function(node){ + return this[node.type](node); +}; + +/** + * Map visit over array of `nodes`, optionally using a `delim` + */ +Compiler.prototype.mapVisit = function(nodes, delim){ + delim = delim || ''; + var result = ''; + for (var i = 0, length = nodes.length; i < length; i++) { + result += this.visit(nodes[i]); + if (delim && i < length - 1) result += this.emit(delim); + } + return result; +}; diff --git a/lib/compress.js b/lib/compress.js index 36f52cb..f45e757 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1,4 +1,4 @@ -var SourceMapGenerator = require('source-map').SourceMapGenerator; +var CompilerBase = require('./compiler'); /** * Expose compiler. @@ -11,55 +11,10 @@ module.exports = Compiler; */ function Compiler(options) { - options = options || {}; - this.map = new SourceMapGenerator({file: options.filename || 'generated.css'}); - this.position = {line: 1, column: 1}; + CompilerBase.call(this, options); } -Compiler.prototype.updatePosition = function(str) { - var lines = str.match(/\n/g); - if (lines) this.position.line += lines.length; - var i = str.lastIndexOf('\n'); - this.position.column = ~i ? str.length - i : this.position.column + str.length; -} - -/** - * Emit string and update current position - */ - -Compiler.prototype.emit = function(str, pos, startOnly) { - if (pos && pos.start) { - this.map.addMapping({ - generated: { - line: this.position.line, - column: Math.max(this.position.column - 1, 0) - }, - source: 'source.css', - original: { - line: pos.start.line, - column: pos.start.column - 1 - } - }); - } - - this.updatePosition(str); - - if (!startOnly && pos && pos.end) { - this.map.addMapping({ - generated: { - line: this.position.line, - column: Math.max(this.position.column - 1, 0) - }, - source: 'source.css', - original: { - line: pos.end.line, - column: pos.end.column - 1 - } - }); - } - - return str; -} +Compiler.prototype = new CompilerBase(); /** * Compile `node`. @@ -71,24 +26,6 @@ Compiler.prototype.compile = function(node){ .join(''); }; -/** - * Visit `node`. - */ - -Compiler.prototype.visit = function(node){ - return this[node.type](node); -}; - -Compiler.prototype.visitMap = function(nodes, join){ - join = join || ''; - var result = ''; - for (var i = 0, length = nodes.length; i < length; i++) { - result += this.visit(nodes[i]); - if (i < length - 1) result += this.emit(join); - } - return result; -}; - /** * Visit comment node. */ @@ -112,7 +49,7 @@ Compiler.prototype.import = function(node){ Compiler.prototype.media = function(node){ return this.emit('@media ' + node.media, node.position, true) + this.emit('{') - + this.visitMap(node.rules) + + this.mapVisit(node.rules) + this.emit('}'); }; @@ -125,7 +62,7 @@ Compiler.prototype.document = function(node){ return this.emit(doc, node.position, true) + this.emit('{') - + this.visitMap(node.rules) + + this.mapVisit(node.rules) + this.emit('}'); }; @@ -152,7 +89,7 @@ Compiler.prototype.namespace = function(node){ Compiler.prototype.supports = function(node){ return this.emit('@supports ' + node.supports, node.position, true) + this.emit('{') - + this.visitMap(node.rules) + + this.mapVisit(node.rules) + this.emit('}'); }; @@ -166,7 +103,7 @@ Compiler.prototype.keyframes = function(node){ + 'keyframes ' + node.name, node.position, true) + this.emit('{') - + this.visitMap(node.keyframes) + + this.mapVisit(node.keyframes) + this.emit('}'); }; @@ -179,7 +116,7 @@ Compiler.prototype.keyframe = function(node){ return this.emit(node.values.join(','), node.position, true) + this.emit('{') - + this.visitMap(decls) + + this.mapVisit(decls) + this.emit('}'); }; @@ -194,7 +131,7 @@ Compiler.prototype.page = function(node){ return this.emit('@page ' + sel, node.position, true) + this.emit('{') - + this.visitMap(node.declarations) + + this.mapVisit(node.declarations) + this.emit('}'); }; @@ -208,7 +145,7 @@ Compiler.prototype.rule = function(node){ return this.emit(node.selectors.join(','), node.position, true) + this.emit('{') - + this.visitMap(decls) + + this.mapVisit(decls) + this.emit('}'); }; diff --git a/lib/identity.js b/lib/identity.js index 0ce5658..43cc14d 100644 --- a/lib/identity.js +++ b/lib/identity.js @@ -1,4 +1,4 @@ -var SourceMapGenerator = require('source-map').SourceMapGenerator; +var CompilerBase = require('./compiler'); /** * Expose compiler. @@ -12,55 +12,11 @@ module.exports = Compiler; function Compiler(options) { options = options || {}; - this.map = new SourceMapGenerator({file: options.filename || 'generated.css'}); - this.position = {line: 1, column: 0}; + CompilerBase.call(this, options); this.indentation = options.indent; } -Compiler.prototype.updatePosition = function(str) { - var lines = str.match(/\n/g); - if (lines) this.position.line += lines.length; - var i = str.lastIndexOf('\n'); - this.position.column = ~i ? str.length - i : this.position.column + str.length; -} - -/** - * Emit string and update current position - */ - -Compiler.prototype.emit = function(str, pos, startOnly) { - if (pos && pos.start) { - this.map.addMapping({ - generated: { - line: this.position.line, - column: Math.max(this.position.column - 1, 0) - }, - source: 'source.css', - original: { - line: pos.start.line, - column: pos.start.column - 1 - } - }); - } - - this.updatePosition(str); - - if (!startOnly && pos && pos.end) { - this.map.addMapping({ - generated: { - line: this.position.line, - column: Math.max(this.position.column - 1, 0) - }, - source: 'source.css', - original: { - line: pos.end.line, - column: pos.end.column - 1 - } - }); - } - - return str; -} +Compiler.prototype = new CompilerBase; /** * Compile `node`. @@ -70,30 +26,12 @@ Compiler.prototype.compile = function(node){ return this.stylesheet(node); }; -/** - * Visit `node`. - */ - -Compiler.prototype.visit = function(node){ - return this[node.type](node); -}; - -Compiler.prototype.visitMap = function(nodes, join){ - join = join || ''; - var result = ''; - for (var i = 0, length = nodes.length; i < length; i++) { - result += this.visit(nodes[i]); - if (i < length - 1) result += this.emit(join); - } - return result; -}; - /** * Visit stylesheet node. */ Compiler.prototype.stylesheet = function(node){ - return this.visitMap(node.stylesheet.rules, '\n\n'); + return this.mapVisit(node.stylesheet.rules, '\n\n'); }; /** @@ -121,7 +59,7 @@ Compiler.prototype.media = function(node){ + this.emit( ' {\n' + this.indent(1)) - + this.visitMap(node.rules, '\n\n') + + this.mapVisit(node.rules, '\n\n') + this.emit( this.indent(-1) + '\n}'); @@ -139,7 +77,7 @@ Compiler.prototype.document = function(node){ ' ' + ' {\n' + this.indent(1)) - + this.visitMap(node.rules, '\n\n') + + this.mapVisit(node.rules, '\n\n') + this.emit( this.indent(-1) + '\n}'); @@ -170,7 +108,7 @@ Compiler.prototype.supports = function(node){ + this.emit( ' {\n' + this.indent(1)) - + this.visitMap(node.rules, '\n\n') + + this.mapVisit(node.rules, '\n\n') + this.emit( this.indent(-1) + '\n}'); @@ -185,7 +123,7 @@ Compiler.prototype.keyframes = function(node){ + this.emit( ' {\n' + this.indent(1)) - + this.visitMap(node.keyframes, '\n') + + this.mapVisit(node.keyframes, '\n') + this.emit( this.indent(-1) + '}'); @@ -203,7 +141,7 @@ Compiler.prototype.keyframe = function(node){ + this.emit( ' {\n' + this.indent(1)) - + this.visitMap(decls, '\n') + + this.mapVisit(decls, '\n') + this.emit( this.indent(-1) + '\n' @@ -222,7 +160,7 @@ Compiler.prototype.page = function(node){ return this.emit('@page ' + sel, node.position, true) + this.emit('{\n') + this.emit(this.indent(1)) - + this.visitMap(node.declarations, '\n') + + this.mapVisit(node.declarations, '\n') + this.emit(this.indent(-1)) + this.emit('\n}'); }; @@ -239,7 +177,7 @@ Compiler.prototype.rule = function(node){ return this.emit(node.selectors.map(function(s){ return indent + s }).join(',\n'), node.position, true) + this.emit(' {\n') + this.emit(this.indent(1)) - + this.visitMap(decls, '\n') + + this.mapVisit(decls, '\n') + this.emit(this.indent(-1)) + this.emit('\n' + this.indent() + '}'); }; From 60129439661fc796ccb78d84f345d3c3ea4ce24e Mon Sep 17 00:00:00 2001 From: Andrey Popp <8mayday@gmail.com> Date: Sun, 3 Nov 2013 04:17:55 +0400 Subject: [PATCH 05/14] Use position.source for mapping lines --- lib/compiler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compiler.js b/lib/compiler.js index 265c56d..7df63a5 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -30,7 +30,7 @@ Compiler.prototype.emit = function(str, pos, startOnly) { line: this.position.line, column: Math.max(this.position.column - 1, 0) }, - source: 'source.css', + source: pos.source || 'source.css', original: { line: pos.start.line, column: pos.start.column - 1 From 67487c79e77772cbf2e27a117063df975a6e461d Mon Sep 17 00:00:00 2001 From: Andrey Popp <8mayday@gmail.com> Date: Mon, 4 Nov 2013 03:32:43 +0400 Subject: [PATCH 06/14] Compiler returns a sourcemap itself, not generator --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index a3bb2e3..92d51d3 100644 --- a/index.js +++ b/index.js @@ -24,7 +24,7 @@ module.exports = function(node, options){ var code = compiler.compile(node); if (options.sourceMap) - return {code: code, map: compiler.map} + return {code: code, map: compiler.map.toJSON()} else return code; }; From 2b8c53810c01b00e40a7d3bf73088da334f4d69c Mon Sep 17 00:00:00 2001 From: Andrey Popp <8mayday@gmail.com> Date: Mon, 4 Nov 2013 03:33:01 +0400 Subject: [PATCH 07/14] Use filename recorded in position --- lib/compiler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/compiler.js b/lib/compiler.js index 7df63a5..71c087d 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -46,7 +46,7 @@ Compiler.prototype.emit = function(str, pos, startOnly) { line: this.position.line, column: Math.max(this.position.column - 1, 0) }, - source: 'source.css', + source: pos.source || 'source.css', original: { line: pos.end.line, column: pos.end.column - 1 From a05732a10973c38e8361b20392e4391d18c2ee10 Mon Sep 17 00:00:00 2001 From: Andrey Popp <8mayday@gmail.com> Date: Tue, 26 Nov 2013 03:13:07 +0400 Subject: [PATCH 08/14] Separate source map generation into a mixin --- index.js | 5 ++++ lib/compiler.js | 50 ++------------------------------ lib/source-map-support.js | 61 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 48 deletions(-) create mode 100644 lib/source-map-support.js diff --git a/index.js b/index.js index 92d51d3..8f645cf 100644 --- a/index.js +++ b/index.js @@ -22,6 +22,11 @@ module.exports = function(node, options){ ? new Compressed(options) : new Identity(options); + if (options.sourceMap) { + var addSourceMaps = require('./lib/source-map-support'); + addSourceMaps(compiler); + } + var code = compiler.compile(node); if (options.sourceMap) return {code: code, map: compiler.map.toJSON()} diff --git a/lib/compiler.js b/lib/compiler.js index 71c087d..daa1739 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -1,63 +1,17 @@ -var SourceMapGenerator = require('source-map').SourceMapGenerator; module.exports = Compiler; function Compiler(options) { - options = options || {}; - this.map = new SourceMapGenerator({file: options.filename || 'generated.css'}); - this.position = {line: 1, column: 1}; + this.options = options || {}; } /** - * Update current position according to `str` being emitted + * Emit `str` */ -Compiler.prototype.updatePosition = function(str) { - var lines = str.match(/\n/g); - if (lines) this.position.line += lines.length; - var i = str.lastIndexOf('\n'); - this.position.column = ~i ? str.length - i : this.position.column + str.length; -} - -/** - * Emit `str` and update current position, use original source `pos` to - * construct source mapping. - */ - Compiler.prototype.emit = function(str, pos, startOnly) { - if (pos && pos.start) { - this.map.addMapping({ - generated: { - line: this.position.line, - column: Math.max(this.position.column - 1, 0) - }, - source: pos.source || 'source.css', - original: { - line: pos.start.line, - column: pos.start.column - 1 - } - }); - } - - this.updatePosition(str); - - if (!startOnly && pos && pos.end) { - this.map.addMapping({ - generated: { - line: this.position.line, - column: Math.max(this.position.column - 1, 0) - }, - source: pos.source || 'source.css', - original: { - line: pos.end.line, - column: pos.end.column - 1 - } - }); - } - return str; } - /** * Visit `node`. */ diff --git a/lib/source-map-support.js b/lib/source-map-support.js new file mode 100644 index 0000000..919e68e --- /dev/null +++ b/lib/source-map-support.js @@ -0,0 +1,61 @@ +var SourceMapGenerator = require('source-map').SourceMapGenerator; + +module.exports = mixin; + +/** + * Mixin source map support into the `compiler` + */ +function mixin(compiler) { + compiler.map = new SourceMapGenerator({file: compiler.options.filename || 'generated.css'}); + compiler.position = {line: 1, column: 1}; + + for (var k in SourceMapSupport) { + compiler[k] = SourceMapSupport[k]; + } +} + +/** + * Compiler mixin which adds source map support + */ +var SourceMapSupport = { + updatePosition: function(str) { + var lines = str.match(/\n/g); + if (lines) this.position.line += lines.length; + var i = str.lastIndexOf('\n'); + this.position.column = ~i ? str.length - i : this.position.column + str.length; + }, + + emit: function(str, pos, startOnly) { + if (pos && pos.start) { + this.map.addMapping({ + generated: { + line: this.position.line, + column: Math.max(this.position.column - 1, 0) + }, + source: pos.source || 'source.css', + original: { + line: pos.start.line, + column: pos.start.column - 1 + } + }); + } + + this.updatePosition(str); + + if (!startOnly && pos && pos.end) { + this.map.addMapping({ + generated: { + line: this.position.line, + column: Math.max(this.position.column - 1, 0) + }, + source: pos.source || 'source.css', + original: { + line: pos.end.line, + column: pos.end.column - 1 + } + }); + } + + return str; + } +}; From 5e1c2f12b8d5d1995092f184c9cdc7783000eb9d Mon Sep 17 00:00:00 2001 From: TJ Holowaychuk Date: Mon, 25 Nov 2013 18:52:13 -0800 Subject: [PATCH 09/14] refactor --- lib/compiler.js | 32 +++++++--- lib/source-map-support.js | 121 +++++++++++++++++++++++--------------- 2 files changed, 96 insertions(+), 57 deletions(-) diff --git a/lib/compiler.js b/lib/compiler.js index daa1739..6d01a14 100644 --- a/lib/compiler.js +++ b/lib/compiler.js @@ -1,16 +1,29 @@ +/** + * Expose `Compiler`. + */ + module.exports = Compiler; -function Compiler(options) { - this.options = options || {}; +/** + * Initialize a compiler. + * + * @param {Type} name + * @return {Type} + * @api public + */ + +function Compiler(opts) { + this.options = opts || {}; } /** * Emit `str` */ -Compiler.prototype.emit = function(str, pos, startOnly) { + +Compiler.prototype.emit = function(str) { return str; -} +}; /** * Visit `node`. @@ -23,12 +36,15 @@ Compiler.prototype.visit = function(node){ /** * Map visit over array of `nodes`, optionally using a `delim` */ + Compiler.prototype.mapVisit = function(nodes, delim){ + var buf = ''; delim = delim || ''; - var result = ''; + for (var i = 0, length = nodes.length; i < length; i++) { - result += this.visit(nodes[i]); - if (delim && i < length - 1) result += this.emit(delim); + buf += this.visit(nodes[i]); + if (delim && i < length - 1) buf += this.emit(delim); } - return result; + + return buf; }; diff --git a/lib/source-map-support.js b/lib/source-map-support.js index 919e68e..351ec7b 100644 --- a/lib/source-map-support.js +++ b/lib/source-map-support.js @@ -1,61 +1,84 @@ -var SourceMapGenerator = require('source-map').SourceMapGenerator; + +/** + * Module dependencies. + */ + +var SourceMap = require('source-map').SourceMapGenerator; + +/** + * Expose `mixin()`. + */ module.exports = mixin; /** - * Mixin source map support into the `compiler` + * Mixin source map support into `compiler`. + * + * @param {Compiler} compiler + * @api public */ -function mixin(compiler) { - compiler.map = new SourceMapGenerator({file: compiler.options.filename || 'generated.css'}); - compiler.position = {line: 1, column: 1}; - for (var k in SourceMapSupport) { - compiler[k] = SourceMapSupport[k]; - } +function mixin(compiler) { + var file = compiler.options.filename || 'generated.css'; + compiler.map = new SourceMapGenerator({ file: file }); + compiler.position = { line: 1, column: 1 }; + for (var k in exports) compiler[k] = exports[k]; } /** - * Compiler mixin which adds source map support + * Update position. + * + * @param {String} str + * @api private */ -var SourceMapSupport = { - updatePosition: function(str) { - var lines = str.match(/\n/g); - if (lines) this.position.line += lines.length; - var i = str.lastIndexOf('\n'); - this.position.column = ~i ? str.length - i : this.position.column + str.length; - }, - - emit: function(str, pos, startOnly) { - if (pos && pos.start) { - this.map.addMapping({ - generated: { - line: this.position.line, - column: Math.max(this.position.column - 1, 0) - }, - source: pos.source || 'source.css', - original: { - line: pos.start.line, - column: pos.start.column - 1 - } - }); - } - - this.updatePosition(str); - - if (!startOnly && pos && pos.end) { - this.map.addMapping({ - generated: { - line: this.position.line, - column: Math.max(this.position.column - 1, 0) - }, - source: pos.source || 'source.css', - original: { - line: pos.end.line, - column: pos.end.column - 1 - } - }); - } - - return str; + +exports.updatePosition = function(str) { + var lines = str.match(/\n/g); + if (lines) this.position.line += lines.length; + var i = str.lastIndexOf('\n'); + this.position.column = ~i ? str.length - i : this.position.column + str.length; +}; + +/** + * Emit `str`. + * + * @param {String} str + * @param {Number} [pos] + * @param {Boolean} [startOnly] + * @return {String} + * @api private + */ + +exports.emit = function(str, pos, startOnly) { + if (pos && pos.start) { + this.map.addMapping({ + source: pos.source || 'source.css', + generated: { + line: this.position.line, + column: Math.max(this.position.column - 1, 0) + }, + original: { + line: pos.start.line, + column: pos.start.column - 1 + } + }); } + + this.updatePosition(str); + + if (!startOnly && pos && pos.end) { + this.map.addMapping({ + source: pos.source || 'source.css', + generated: { + line: this.position.line, + column: Math.max(this.position.column - 1, 0) + }, + original: { + line: pos.end.line, + column: pos.end.column - 1 + } + }); + } + + return str; }; From 40fdb261168504ea66c2e180eed66065f310c52d Mon Sep 17 00:00:00 2001 From: TJ Holowaychuk Date: Mon, 25 Nov 2013 18:53:25 -0800 Subject: [PATCH 10/14] refactor --- lib/compress.js | 15 ++++++++++++--- lib/identity.js | 15 ++++++++++++--- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/lib/compress.js b/lib/compress.js index f45e757..601bd73 100644 --- a/lib/compress.js +++ b/lib/compress.js @@ -1,4 +1,9 @@ -var CompilerBase = require('./compiler'); + +/** + * Module dependencies. + */ + +var Base = require('./compiler'); /** * Expose compiler. @@ -11,10 +16,14 @@ module.exports = Compiler; */ function Compiler(options) { - CompilerBase.call(this, options); + Base.call(this, options); } -Compiler.prototype = new CompilerBase(); +/** + * Inherit from `Base.prototype`. + */ + +Compiler.prototype.__proto__ = Base.prototype; /** * Compile `node`. diff --git a/lib/identity.js b/lib/identity.js index 43cc14d..cc9fd5c 100644 --- a/lib/identity.js +++ b/lib/identity.js @@ -1,4 +1,9 @@ -var CompilerBase = require('./compiler'); + +/** + * Module dependencies. + */ + +var Base = require('./compiler'); /** * Expose compiler. @@ -12,11 +17,15 @@ module.exports = Compiler; function Compiler(options) { options = options || {}; - CompilerBase.call(this, options); + Base.call(this, options); this.indentation = options.indent; } -Compiler.prototype = new CompilerBase; +/** + * Inherit from `Base.prototype`. + */ + +Compiler.prototype.__proto__ = Base.prototype; /** * Compile `node`. From 2b6eb629a36c81e278c98d88df64ff5ebce65ee8 Mon Sep 17 00:00:00 2001 From: TJ Holowaychuk Date: Mon, 25 Nov 2013 18:56:30 -0800 Subject: [PATCH 11/14] refactor --- index.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/index.js b/index.js index 8f645cf..7075b35 100644 --- a/index.js +++ b/index.js @@ -22,15 +22,15 @@ module.exports = function(node, options){ ? new Compressed(options) : new Identity(options); + // source maps if (options.sourceMap) { - var addSourceMaps = require('./lib/source-map-support'); - addSourceMaps(compiler); + var sourcemaps = require('./lib/source-map-support'); + sourcemaps(compiler); + + var code = compiler.compile(node); + return { code: code, map: compiler.map.toJSON() }; } - var code = compiler.compile(node); - if (options.sourceMap) - return {code: code, map: compiler.map.toJSON()} - else - return code; + return code; }; From e3e3d2d1768f4de69307d61a42c735625829aa89 Mon Sep 17 00:00:00 2001 From: TJ Holowaychuk Date: Mon, 25 Nov 2013 18:57:59 -0800 Subject: [PATCH 12/14] example --- examples/sourcemaps.js | 12 ++++++++++++ index.js | 2 +- lib/source-map-support.js | 2 +- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 examples/sourcemaps.js diff --git a/examples/sourcemaps.js b/examples/sourcemaps.js new file mode 100644 index 0000000..ef1fe4b --- /dev/null +++ b/examples/sourcemaps.js @@ -0,0 +1,12 @@ + +/** + * Module dependencies. + */ + +var parse = require('css-parse') + , stringify = require('..') + , fs = require('fs') + , read = fs.readFileSync + , css = read('examples/media.css', 'utf8'); + +console.log(stringify(parse(css), { compress: false, sourcemap: true })); diff --git a/index.js b/index.js index 7075b35..3c2e539 100644 --- a/index.js +++ b/index.js @@ -23,7 +23,7 @@ module.exports = function(node, options){ : new Identity(options); // source maps - if (options.sourceMap) { + if (options.sourcemap) { var sourcemaps = require('./lib/source-map-support'); sourcemaps(compiler); diff --git a/lib/source-map-support.js b/lib/source-map-support.js index 351ec7b..1e60513 100644 --- a/lib/source-map-support.js +++ b/lib/source-map-support.js @@ -20,7 +20,7 @@ module.exports = mixin; function mixin(compiler) { var file = compiler.options.filename || 'generated.css'; - compiler.map = new SourceMapGenerator({ file: file }); + compiler.map = new SourceMap({ file: file }); compiler.position = { line: 1, column: 1 }; for (var k in exports) compiler[k] = exports[k]; } From e8af108ab4f94c165d2580750156867d393be806 Mon Sep 17 00:00:00 2001 From: Andrey Popp <8mayday@gmail.com> Date: Tue, 26 Nov 2013 11:40:03 +0400 Subject: [PATCH 13/14] Fix compilation w/o source maps --- index.js | 1 + 1 file changed, 1 insertion(+) diff --git a/index.js b/index.js index 3c2e539..103a849 100644 --- a/index.js +++ b/index.js @@ -31,6 +31,7 @@ module.exports = function(node, options){ return { code: code, map: compiler.map.toJSON() }; } + var code = compiler.compile(node); return code; }; From 1efd6ef57fd4a46d500775b1e7291ec853b5755e Mon Sep 17 00:00:00 2001 From: Andrey Popp <8mayday@gmail.com> Date: Tue, 26 Nov 2013 12:16:30 +0400 Subject: [PATCH 14/14] Add basic tests for source mapping --- package.json | 2 +- test/css-stringify.js | 45 +++++++++++++++++++++++++++++++++++++++- test/source-map-case.css | 17 +++++++++++++++ 3 files changed, 62 insertions(+), 2 deletions(-) create mode 100644 test/source-map-case.css diff --git a/package.json b/package.json index 8094e87..1dfe90c 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "devDependencies": { "mocha": "*", "should": "*", - "css-parse": "1.4.0" + "css-parse": "1.6.0" }, "main": "index", "repository": { diff --git a/test/css-stringify.js b/test/css-stringify.js index 8f55ef7..b37ee7f 100644 --- a/test/css-stringify.js +++ b/test/css-stringify.js @@ -8,7 +8,8 @@ var stringify = require('..') , fs = require('fs') , path = require('path') , read = fs.readFileSync - , readdir = fs.readdirSync; + , readdir = fs.readdirSync + , SourceMapConsumer = require('source-map').SourceMapConsumer; describe('stringify(obj)', function(){ readdir('test/cases').forEach(function(file){ @@ -25,3 +26,45 @@ describe('stringify(obj)', function(){ }); }); }); + +describe('stringify(obj, {sourcemap: true})', function(){ + var src = read('test/source-map-case.css', 'utf8'); + var stylesheet = parse(src, { source: 'rules.css', position: true }); + function loc(line, column) { + return { line: line, column: column, source: 'rules.css', name: null } + }; + + var locs = { + tobiSelector: loc(1, 0), + tobiNameName: loc(2, 2), + tobiNameValue: loc(2, 2), + mediaBlock: loc(11, 0), + mediaOnly: loc(12, 2), + comment: loc(17, 0), + }; + + it('should generate source maps alongside when using identity compiler', function(){ + var result = stringify(stylesheet, { sourcemap: true }); + result.should.have.property('code'); + result.should.have.property('map'); + var map = new SourceMapConsumer(result.map); + map.originalPositionFor({ line: 1, column: 0 }).should.eql(locs.tobiSelector); + map.originalPositionFor({ line: 2, column: 2 }).should.eql(locs.tobiNameName); + map.originalPositionFor({ line: 2, column: 8 }).should.eql(locs.tobiNameValue); + map.originalPositionFor({ line: 11, column: 0 }).should.eql(locs.mediaBlock); + map.originalPositionFor({ line: 12, column: 2 }).should.eql(locs.mediaOnly); + map.originalPositionFor({ line: 17, column: 0 }).should.eql(locs.comment); + }); + + it('should generate source maps alongside when using compress compiler', function(){ + var result = stringify(stylesheet, { compress: true, sourcemap: true }); + result.should.have.property('code'); + result.should.have.property('map'); + var map = new SourceMapConsumer(result.map); + map.originalPositionFor({ line: 1, column: 0 }).should.eql(locs.tobiSelector); + map.originalPositionFor({ line: 1, column: 5 }).should.eql(locs.tobiNameName); + map.originalPositionFor({ line: 1, column: 10 }).should.eql(locs.tobiNameValue); + map.originalPositionFor({ line: 1, column: 50 }).should.eql(locs.mediaBlock); + map.originalPositionFor({ line: 1, column: 64 }).should.eql(locs.mediaOnly); + }); +}); diff --git a/test/source-map-case.css b/test/source-map-case.css new file mode 100644 index 0000000..47598d7 --- /dev/null +++ b/test/source-map-case.css @@ -0,0 +1,17 @@ +tobi { + name: 'tobi'; + age: 2; +} + +loki { + name: 'loki'; + age: 1; +} + +@media screen { + screen-only { + display: block; + } +} + +/* comment */