diff --git a/.gitignore b/.gitignore index a0db3828..6b5c221c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ node_modules .idea test/browser/less.js test/browser/test-runner-*.htm +test/sourcemaps/*.map +test/sourcemaps/*.css diff --git a/.jshintignore b/.jshintignore new file mode 100644 index 00000000..ccc70d75 --- /dev/null +++ b/.jshintignore @@ -0,0 +1,5 @@ +benchmark/ +build/ +dist/ +node_modules/ +test/browser/ diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 00000000..2f830954 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,9 @@ +{ + "evil": true, + "boss": true, + "expr": true, + "laxbreak": true, + "node": true, + "unused": "vars", + "noarg": true +} diff --git a/Makefile b/Makefile index db27cf04..c9920c5e 100644 --- a/Makefile +++ b/Makefile @@ -35,6 +35,7 @@ less: @@cat ${HEADER} | sed s/@VERSION/${VERSION}/ > ${DIST} @@echo "(function (window, undefined) {" >> ${DIST} @@cat build/require.js\ + build/browser-header.js\ ${SRC}/parser.js\ ${SRC}/functions.js\ ${SRC}/colors.js\ @@ -60,10 +61,19 @@ browser-test: browser-prepare browser-test-server: browser-prepare phantomjs test/browser/phantom-runner.js --no-tests +jshint: + node_modules/.bin/jshint --config ./.jshintrc . + +test-sourcemaps: + node bin/lessc --source-map --source-map-inline test/less/import.less test/sourcemaps/import.css + node bin/lessc --source-map --source-map-inline test/less/sourcemaps/basic.less test/sourcemaps/basic.css + node node_modules/http-server/bin/http-server test/sourcemaps -p 8083 + rhino: @@mkdir -p dist @@touch ${RHINO} @@cat build/require-rhino.js\ + build/rhino-header.js\ ${SRC}/parser.js\ ${SRC}/env.js\ ${SRC}/visitor.js\ diff --git a/bin/lessc b/bin/lessc index 01de8320..c830488b 100755 --- a/bin/lessc +++ b/bin/lessc @@ -138,6 +138,21 @@ args = args.filter(function (arg) { options.dumpLineNumbers = match[2]; } break; + case 'source-map': + if (!match[2]) { + options.sourceMap = true; + } else { + options.sourceMap = match[2]; + } + break; + case 'source-map-rootpath': + if (checkArgFunc(arg, match[2])) { + options.sourceMapRootpath = match[2]; + } + break; + case 'source-map-inline': + options.outputSourceFiles = true; + break; case 'rp': case 'rootpath': if (checkArgFunc(arg, match[2])) { @@ -175,12 +190,24 @@ if (input && input != '-') { var output = args[2]; var outputbase = args[2]; if (output) { + options.sourceMapOutputFilename = output; output = path.resolve(process.cwd(), output); if (warningMessages) { sys.puts(warningMessages); } } +options.sourceMapBasepath = process.cwd(); + +if (options.sourceMap === true) { + if (!output) { + sys.puts("the sourcemap option only has an optional filename if the css filename is given"); + return; + } + options.sourceMapFullFilename = options.sourceMapOutputFilename + ".map"; + options.sourceMap = path.basename(options.sourceMapFullFilename); +} + if (! input) { sys.puts("lessc: no input files"); sys.puts(""); @@ -211,6 +238,12 @@ if (options.depends) { sys.print(outputbase + ": "); } +var writeSourceMap = function(output) { + var filename = options.sourceMapFullFilename || options.sourceMap; + ensureDirectory(filename); + fs.writeFileSync(filename, output, 'utf8'); +}; + var parseLessFile = function (e, data) { if (e) { sys.puts("lessc: " + e.message); @@ -240,6 +273,13 @@ var parseLessFile = function (e, data) { ieCompat: options.ieCompat, compress: options.compress, cleancss: options.cleancss, + sourceMap: Boolean(options.sourceMap), + sourceMapFilename: options.sourceMap, + sourceMapOutputFilename: options.sourceMapOutputFilename, + sourceMapBasepath: options.sourceMapBasepath, + sourceMapRootpath: options.sourceMapRootpath || "", + outputSourceFiles: options.outputSourceFiles, + writeSourceMap: writeSourceMap, maxLineLen: options.maxLineLen, strictMath: options.strictMath, strictUnits: options.strictUnits diff --git a/build/browser-header.js b/build/browser-header.js new file mode 100644 index 00000000..e0022b71 --- /dev/null +++ b/build/browser-header.js @@ -0,0 +1,4 @@ +if (typeof(window.less) === 'undefined') { window.less = {}; } +less = window.less; +tree = window.less.tree = {}; +less.mode = 'browser'; diff --git a/build/rhino-header.js b/build/rhino-header.js new file mode 100644 index 00000000..891f0943 --- /dev/null +++ b/build/rhino-header.js @@ -0,0 +1,4 @@ +if (typeof(window) === 'undefined') { less = {} } +else { less = window.less = {} } +tree = less.tree = {}; +less.mode = 'rhino'; \ No newline at end of file diff --git a/lib/less/browser.js b/lib/less/browser.js index bb800f5d..844a4af1 100644 --- a/lib/less/browser.js +++ b/lib/less/browser.js @@ -1,6 +1,7 @@ // // browser.js - client-side engine // +/*global less */ var isFileProtocol = /^(file|chrome(-extension)?|resource|qrc|app):/.test(location.protocol); @@ -43,7 +44,7 @@ less.watch = function () { less.env = 'development'; initRunningMode(); } - return this.watchMode = true + return this.watchMode = true; }; less.unwatch = function () {clearInterval(less.watchTimer); return this.watchMode = false; }; @@ -105,12 +106,12 @@ less.modifyVars = function(record) { newVars += ((name.slice(0,1) === '@')? '' : '@') + name +': '+ ((record[name].slice(-1) === ';')? record[name] : record[name] +';'); } - less.refresh(false, newVars) + less.refresh(false, newVars); }; less.refresh = function (reload, newVars) { var startTime, endTime; - startTime = endTime = new(Date); + startTime = endTime = new Date(); loadStyleSheets(function (e, root, _, sheet, env) { if (e) { @@ -122,9 +123,9 @@ less.refresh = function (reload, newVars) { log("parsed " + sheet.href + " successfully."); createCSS(root.toCSS(less), sheet, env.lastModified); } - log("css for " + sheet.href + " generated in " + (new(Date) - endTime) + 'ms'); - (env.remaining === 0) && log("css generated in " + (new(Date) - startTime) + 'ms'); - endTime = new(Date); + log("css for " + sheet.href + " generated in " + (new Date() - endTime) + 'ms'); + (env.remaining === 0) && log("css generated in " + (new Date() - startTime) + 'ms'); + endTime = new Date(); }, reload, newVars); loadStyles(newVars); @@ -145,6 +146,7 @@ function loadStyles(newVars) { lessText += "\n" + newVars; } + /*jshint loopfunc:true */ // use closure to store current value of i var callback = (function(style) { return function (e, cssAST) { @@ -158,7 +160,7 @@ function loadStyles(newVars) { } else { style.innerHTML = css; } - } + }; })(style); new(less.Parser)(env).parse(lessText, callback); } @@ -337,20 +339,20 @@ function loadFile(originalHref, currentFileInfo, callback, env, newVars) { if (newVars) { lessText += "\n" + newVars; } - callback(null, lessText, href, newFileInfo, { lastModified: new Date() }) + callback(null, lessText, href, newFileInfo, { lastModified: new Date() }); } catch (e) { callback(e, null, href); } return; } - xhr(href, env.mime, function (data, lastModified) { + doXHR(href, env.mime, function (data, lastModified) { // per file cache fileCache[href] = data; // Use remote copy (re-parse) try { - callback(null, data, href, newFileInfo, { lastModified: lastModified }) + callback(null, data, href, newFileInfo, { lastModified: lastModified }); } catch (e) { callback(e, null, href); } @@ -406,7 +408,7 @@ function createCSS(styles, sheet, lastModified) { // If there is no oldCss, just append; otherwise, only append if we need // to replace oldCss with an updated stylesheet - if (oldCss == null || keepOldCss === false) { + if (oldCss === null || keepOldCss === false) { var nextEl = sheet && sheet.nextSibling || null; (nextEl || document.getElementsByTagName('head')[0]).parentNode.insertBefore(css, nextEl); } @@ -427,7 +429,7 @@ function createCSS(styles, sheet, lastModified) { } } -function xhr(url, type, callback, errback) { +function doXHR(url, type, callback, errback) { var xhr = getXMLHttpRequest(); var async = isFileProtocol ? less.fileAsync : less.async; @@ -467,10 +469,11 @@ function xhr(url, type, callback, errback) { function getXMLHttpRequest() { if (window.XMLHttpRequest) { - return new(XMLHttpRequest); + return new XMLHttpRequest(); } else { try { - return new(ActiveXObject)("MSXML2.XMLHTTP.3.0"); + /*global ActiveXObject */ + return new ActiveXObject("MSXML2.XMLHTTP.3.0"); } catch (e) { log("browser doesn't support AJAX."); return null; @@ -483,13 +486,13 @@ function removeNode(node) { } function log(str) { - if (less.env == 'development' && typeof(console) !== "undefined") { console.log('less: ' + str) } + if (less.env == 'development' && typeof(console) !== "undefined") { console.log('less: ' + str); } } function error(e, rootHref) { var id = 'less-error-message:' + extractId(rootHref || ""); var template = '
  • {content}
  • '; - var elem = document.createElement('div'), timer, content, error = []; + var elem = document.createElement('div'), timer, content, errors = []; var filename = e.filename || rootHref; var filenameNoPath = filename.match(/([^\/]+(\?.*)?)$/)[1]; @@ -500,8 +503,8 @@ function error(e, rootHref) { '' + '

    in ' + filenameNoPath + " "; var errorline = function (e, i, classname) { - if (e.extract[i] != undefined) { - error.push(template.replace(/\{line\}/, (parseInt(e.line) || 0) + (i - 1)) + if (e.extract[i] !== undefined) { + errors.push(template.replace(/\{line\}/, (parseInt(e.line, 10) || 0) + (i - 1)) .replace(/\{class\}/, classname) .replace(/\{content\}/, e.extract[i])); } @@ -512,7 +515,7 @@ function error(e, rootHref) { errorline(e, 1, 'line'); errorline(e, 2, ''); content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':

    ' + - ''; + ''; } else if (e.stack) { content += '
    ' + e.stack.split('\n').slice(1).join('
    '); } diff --git a/lib/less/env.js b/lib/less/env.js index feb79baa..dd4d5a5f 100644 --- a/lib/less/env.js +++ b/lib/less/env.js @@ -58,7 +58,8 @@ 'ieCompat', // whether to enforce IE compatibility (IE8 data-uri) 'strictMath', // whether math has to be within parenthesis 'strictUnits', // whether units need to evaluate correctly - 'cleancss' // whether to compress with clean-css + 'cleancss', // whether to compress with clean-css + 'sourceMap' // whether to output a source map ]; tree.evalEnv = function(options, frames) { @@ -98,5 +99,6 @@ destination[propertiesToCopy[i]] = original[propertiesToCopy[i]]; } } - } -})(require('./tree')); \ No newline at end of file + }; + +})(require('./tree')); diff --git a/lib/less/extend-visitor.js b/lib/less/extend-visitor.js index cd9d9458..aead49f2 100644 --- a/lib/less/extend-visitor.js +++ b/lib/less/extend-visitor.js @@ -1,4 +1,6 @@ (function (tree) { + /*jshint loopfunc:true */ + tree.extendFinderVisitor = function() { this._visitor = new tree.visitor(this); this.contexts = []; @@ -246,7 +248,7 @@ haystackElement = hackstackSelector.elements[hackstackElementIndex]; // if we allow elements before our match we can add a potential match every time. otherwise only at the first element. - if (extend.allowBefore || (haystackSelectorIndex == 0 && hackstackElementIndex == 0)) { + if (extend.allowBefore || (haystackSelectorIndex === 0 && hackstackElementIndex === 0)) { potentialMatches.push({pathIndex: haystackSelectorIndex, index: hackstackElementIndex, matched: 0, initialCombinator: haystackElement.combinator}); } @@ -257,7 +259,7 @@ // then each selector in haystackSelectorPath has a space before it added in the toCSS phase. so we need to work out // what the resulting combinator will be targetCombinator = haystackElement.combinator.value; - if (targetCombinator == '' && hackstackElementIndex === 0) { + if (targetCombinator === '' && hackstackElementIndex === 0) { targetCombinator = ' '; } @@ -333,7 +335,8 @@ firstElement = new tree.Element( match.initialCombinator, replacementSelector.elements[0].value, - replacementSelector.elements[0].index + replacementSelector.elements[0].index, + replacementSelector.elements[0].currentFileInfo ); if (match.pathIndex > currentSelectorPathIndex && currentSelectorPathElementIndex > 0) { @@ -388,4 +391,4 @@ } }; -})(require('./tree')); \ No newline at end of file +})(require('./tree')); diff --git a/lib/less/functions.js b/lib/less/functions.js index cddfdb20..2806bfde 100644 --- a/lib/less/functions.js +++ b/lib/less/functions.js @@ -26,10 +26,10 @@ tree.functions = { function hue(h) { h = h < 0 ? h + 1 : (h > 1 ? h - 1 : h); - if (h * 6 < 1) return m1 + (m2 - m1) * h * 6; - else if (h * 2 < 1) return m2; - else if (h * 3 < 2) return m1 + (m2 - m1) * (2/3 - h) * 6; - else return m1; + if (h * 6 < 1) { return m1 + (m2 - m1) * h * 6; } + else if (h * 2 < 1) { return m2; } + else if (h * 3 < 2) { return m1 + (m2 - m1) * (2/3 - h) * 6; } + else { return m1; } } }, @@ -223,6 +223,7 @@ tree.functions = { str = quoted.value; for (var i = 0; i < args.length; i++) { + /*jshint loopfunc:true */ str = str.replace(/%[sda]/i, function(token) { var value = token.match(/s/i) ? args[i].value : args[i].toCSS(); return token.match(/[A-Z]$/) ? encodeURIComponent(value) : value; @@ -259,6 +260,7 @@ tree.functions = { }, _math: function (fn, unit, n) { if (n instanceof tree.Dimension) { + /*jshint eqnull:true */ return new(tree.Dimension)(fn(parseFloat(n.value)), unit == null ? n.unit : unit); } else if (typeof(n) === 'number') { return fn(n); @@ -463,10 +465,10 @@ tree.functions = { // use base 64 unless it's an ASCII or UTF-8 format var charset = mime.charsets.lookup(mimetype); useBase64 = ['US-ASCII', 'UTF-8'].indexOf(charset) < 0; - if (useBase64) mimetype += ';base64'; + if (useBase64) { mimetype += ';base64'; } } else { - useBase64 = /;base64$/.test(mimetype) + useBase64 = /;base64$/.test(mimetype); } var buf = fs.readFileSync(filePath); @@ -533,7 +535,7 @@ tree.functions = { gradientType = "radial"; gradientDirectionSvg = 'cx="50%" cy="50%" r="75%"'; rectangleDimension = 'x="-50" y="-50" width="101" height="101"'; - break + break; default: throw { type: "Argument", message: "svg-gradient direction must be 'to bottom', 'to right', 'to bottom right', 'to top right' or 'ellipse at center'" }; } @@ -607,6 +609,7 @@ var mathFunctions = [{name:"ceil"}, {name:"floor"}, {name: "sqrt"}, {name:"abs"} {name:"atan", unit: "rad"}, {name:"asin", unit: "rad"}, {name:"acos", unit: "rad"}], createMathFunction = function(name, unit) { return function(n) { + /*jshint eqnull:true */ if (unit != null) { n = n.unify(); } diff --git a/lib/less/index.js b/lib/less/index.js index 5c6a81ac..cb95ca88 100644 --- a/lib/less/index.js +++ b/lib/less/index.js @@ -24,7 +24,7 @@ var less = { catch (err) { callback(err); } }); } else { - ee = new(require('events').EventEmitter); + ee = new (require('events').EventEmitter)(); process.nextTick(function () { parser.parse(input, function (e, root) { @@ -42,10 +42,10 @@ var less = { var message = ""; var extract = ctx.extract; var error = []; - var stylize = options.color ? require('./lessc_helper').stylize : function (str) { return str }; + var stylize = options.color ? require('./lessc_helper').stylize : function (str) { return str; }; // only output a stack if it isn't a less error - if (ctx.stack && !ctx.type) { return stylize(ctx.stack, 'red') } + if (ctx.stack && !ctx.type) { return stylize(ctx.stack, 'red'); } if (!ctx.hasOwnProperty('index') || !extract) { return ctx.stack || ctx.message; @@ -132,7 +132,7 @@ less.Parser.fileLoader = function (file, currentFileInfo, callback, env) { newFileInfo.filename = pathname; callback(null, data, pathname, newFileInfo); - }; + } var isUrl = isUrlRe.test( file ); if (isUrl || isUrlRe.test(currentFileInfo.currentDirectory)) { @@ -146,12 +146,7 @@ less.Parser.fileLoader = function (file, currentFileInfo, callback, env) { } var urlStr = isUrl ? file : url.resolve(currentFileInfo.currentDirectory, file), - urlObj = url.parse(urlStr), - req = { - host: urlObj.hostname, - port: urlObj.port || 80, - path: urlObj.pathname + (urlObj.search||'') - }; + urlObj = url.parse(urlStr); request.get(urlStr, function (error, res, body) { if (res.statusCode === 404) { @@ -205,7 +200,7 @@ less.Parser.fileLoader = function (file, currentFileInfo, callback, env) { }); } } -} +}; require('./env'); require('./functions'); @@ -215,5 +210,6 @@ require('./import-visitor.js'); require('./extend-visitor.js'); require('./join-selector-visitor.js'); require('./to-css-visitor.js'); +require('./source-map-output.js'); for (var k in less) { exports[k] = less[k]; } diff --git a/lib/less/lessc_helper.js b/lib/less/lessc_helper.js index 6c9bb051..1a2bba15 100644 --- a/lib/less/lessc_helper.js +++ b/lib/less/lessc_helper.js @@ -51,6 +51,9 @@ var lessc_helper = { sys.puts(" that will output the information within a fake"); sys.puts(" media query which is compatible with the SASS"); sys.puts(" format, and 'all' which will do both."); + sys.puts(" --source-map[=FILENAME] Outputs a v3 sourcemap to the filename (or output filename.map)"); + sys.puts(" --source-map-rootpath=X adds this path onto the sourcemap filename and less file paths"); + sys.puts(" --source-map-inline puts the less files into the map instead of referencing them"); sys.puts(" -rp, --rootpath=URL Set rootpath for url rewriting in relative imports and urls."); sys.puts(" Works with or without the relative-urls option."); sys.puts(" -ru, --relative-urls re-write relative urls to the base less file."); @@ -66,4 +69,4 @@ var lessc_helper = { }; // Exports helper functions -for (var h in lessc_helper) { exports[h] = lessc_helper[h] } +for (var h in lessc_helper) { exports[h] = lessc_helper[h]; } diff --git a/lib/less/parser.js b/lib/less/parser.js index 81d4473e..1dd9a26e 100644 --- a/lib/less/parser.js +++ b/lib/less/parser.js @@ -1,23 +1,10 @@ -var less, tree, charset; +var less, tree; -if (typeof environment === "object" && ({}).toString.call(environment) === "[object Environment]") { - // Rhino - // Details on how to detect Rhino: https://github.com/ringo/ringojs/issues/88 - if (typeof(window) === 'undefined') { less = {} } - else { less = window.less = {} } - tree = less.tree = {}; - less.mode = 'rhino'; -} else if (typeof(window) === 'undefined') { - // Node.js - less = exports, +// Node.js does not have a header file added which defines less +if (less === undefined) { + less = exports; tree = require('./tree'); less.mode = 'node'; -} else { - // Browser - if (typeof(window.less) === 'undefined') { window.less = {} } - less = window.less, - tree = window.less.tree = {}; - less.mode = 'browser'; } // // less.js - parser @@ -63,8 +50,6 @@ less.Parser = function Parser(env) { current, // index of current chunk, in `input` parser; - var that = this; - // Top parser on an import tree must be sure there is one "env" // which will then be passed around by reference. if (!(env instanceof tree.parseEnv)) { @@ -117,7 +102,7 @@ less.Parser = function Parser(env) { fileParsedFunc(e, root, fullPath); }); } - }, env) + }, env); } } }; @@ -140,7 +125,7 @@ less.Parser = function Parser(env) { // Parse from a token, regexp or string, and move forward if match // function $(tok) { - var match, args, length, index, k; + var match, length; // // Non-terminal @@ -189,13 +174,13 @@ less.Parser = function Parser(env) { mem = i += length; while (i < endIndex) { - if (! isWhitespace(input.charAt(i))) { break } + if (! isWhitespace(input.charAt(i))) { break; } i++; } chunks[j] = chunks[j].slice(length + (i - mem)); current = i; - if (chunks[j].length === 0 && j < chunks.length - 1) { j++ } + if (chunks[j].length === 0 && j < chunks.length - 1) { j++; } return oldi !== i || oldj !== j; } @@ -235,13 +220,23 @@ less.Parser = function Parser(env) { } } - function getLocation(index, input) { - for (var n = index, column = -1; - n >= 0 && input.charAt(n) !== '\n'; - n--) { column++ } + function getLocation(index, inputStream) { + var n = index + 1, + line = null, + column = -1; - return { line: typeof(index) === 'number' ? (input.slice(0, index).match(/\n/g) || "").length : null, - column: column }; + while (--n >= 0 && inputStream.charAt(n) !== '\n') { + column++; + } + + if (typeof index === 'number') { + line = (inputStream.slice(0, index).match(/\n/g) || "").length; + } + + return { + line: line, + column: column + }; } function getDebugInfo(index, inputStream, env) { @@ -261,6 +256,7 @@ less.Parser = function Parser(env) { loc = getLocation(e.index, input), line = loc.line, col = loc.column, + callLine = e.call && getLocation(e.call, input).line, lines = input.split('\n'); this.type = e.type || 'Syntax'; @@ -268,8 +264,8 @@ less.Parser = function Parser(env) { this.filename = e.filename || env.currentFileInfo.filename; this.index = e.index; this.line = typeof(line) === 'number' ? line + 1 : null; - this.callLine = e.call && (getLocation(e.call, input).line + 1); - this.callExtract = lines[getLocation(e.call, input).line]; + this.callLine = callLine + 1; + this.callExtract = lines[callLine]; this.stack = e.stack; this.column = col; this.extract = [ @@ -301,7 +297,7 @@ less.Parser = function Parser(env) { // call `callback` when done. // parse: function (str, callback) { - var root, start, end, zone, line, lines, buff = [], c, error = null; + var root, line, lines, error = null; i = j = current = furthest = 0; input = str.replace(/\r\n/g, '\n'); @@ -309,6 +305,8 @@ less.Parser = function Parser(env) { // Remove potential UTF Byte Order Mark input = input.replace(/^\uFEFF/, ''); + parser.imports.contents[env.currentFileInfo.filename] = input; + // Split the input into chunks. chunks = (function (chunks) { var j = 0, @@ -353,16 +351,42 @@ less.Parser = function Parser(env) { } switch (c) { - case '{': if (! inParam) { level ++; chunk.push(c); break } - case '}': if (! inParam) { level --; chunk.push(c); chunks[++j] = chunk = []; break } - case '(': if (! inParam) { inParam = true; chunk.push(c); break } - case ')': if ( inParam) { inParam = false; chunk.push(c); break } - default: chunk.push(c); + case '{': + if (!inParam) { + level++; + chunk.push(c); + break; + } + /* falls through */ + case '}': + if (!inParam) { + level--; + chunk.push(c); + chunks[++j] = chunk = []; + break; + } + /* falls through */ + case '(': + if (!inParam) { + inParam = true; + chunk.push(c); + break; + } + /* falls through */ + case ')': + if (inParam) { + inParam = false; + chunk.push(c); + break; + } + /* falls through */ + default: + chunk.push(c); } i++; } - if (level != 0) { + if (level !== 0) { error = new(LessError)({ index: i-1, type: 'Parse', @@ -371,7 +395,7 @@ less.Parser = function Parser(env) { }, env); } - return chunks.map(function (c) { return c.join('') }); + return chunks.map(function (c) { return c.join(''); }); })([[]]); if (error) { @@ -391,11 +415,10 @@ less.Parser = function Parser(env) { } root.toCSS = (function (evaluate) { - var line, lines, column; - return function (options, variables) { options = options || {}; - var importError, + var evaldRoot, + css, evalEnv = new tree.evalEnv(options); // @@ -427,7 +450,7 @@ less.Parser = function Parser(env) { } try { - var evaldRoot = evaluate.call(this, evalEnv); + evaldRoot = evaluate.call(this, evalEnv); new(tree.joinSelectorVisitor)() .run(evaldRoot); @@ -438,7 +461,21 @@ less.Parser = function Parser(env) { new(tree.toCSSVisitor)({compress: Boolean(options.compress)}) .run(evaldRoot); - var css = evaldRoot.toCSS({ + if (options.sourceMap) { + evaldRoot = new tree.sourceMapOutput( + { + writeSourceMap: options.writeSourceMap, + rootNode: evaldRoot, + contentsMap: parser.imports.contents, + sourceMapFilename: options.sourceMapFilename, + outputFilename: options.sourceMapOutputFilename, + sourceMapBasepath: options.sourceMapBasepath, + sourceMapRootpath: options.sourceMapRootpath, + outputSourceFiles: options.outputSourceFiles + }); + } + + css = evaldRoot.toCSS({ compress: Boolean(options.compress), dumpLineNumbers: env.dumpLineNumbers, strictUnits: Boolean(options.strictUnits)}); @@ -466,10 +503,9 @@ less.Parser = function Parser(env) { // and the part which didn't), so we can color them differently. if (i < input.length - 1) { i = furthest; + var loc = getLocation(i, input); lines = input.split('\n'); - line = (input.slice(0, i).match(/\n/g) || "").length + 1; - - for (var n = i, column = -1; n >= 0 && input.charAt(n) !== '\n'; n--) { column++ } + line = loc.line + 1; error = { type: "Parse", @@ -477,7 +513,7 @@ less.Parser = function Parser(env) { index: i, filename: env.currentFileInfo.filename, line: line, - column: column, + column: loc.column, extract: [ lines[line - 2], lines[line - 1], @@ -571,7 +607,7 @@ less.Parser = function Parser(env) { comment: function () { var comment; - if (input.charAt(i) !== '/') return; + if (input.charAt(i) !== '/') { return; } if (input.charAt(i + 1) === '/') { return new(tree.Comment)($(/^\/\/.*/), true, i, env.currentFileInfo); @@ -602,8 +638,8 @@ less.Parser = function Parser(env) { quoted: function () { var str, j = i, e, index = i; - if (input.charAt(j) === '~') { j++, e = true } // Escaped strings - if (input.charAt(j) !== '"' && input.charAt(j) !== "'") return; + if (input.charAt(j) === '~') { j++, e = true; } // Escaped strings + if (input.charAt(j) !== '"' && input.charAt(j) !== "'") { return; } e && $('~'); @@ -643,13 +679,13 @@ less.Parser = function Parser(env) { call: function () { var name, nameLC, args, alpha_ret, index = i; - if (! (name = /^([\w-]+|%|progid:[\w\.]+)\(/.exec(chunks[j]))) return; + if (! (name = /^([\w-]+|%|progid:[\w\.]+)\(/.exec(chunks[j]))) { return; } name = name[1]; nameLC = name.toLowerCase(); - if (nameLC === 'url') { return null } - else { i += name.length } + if (nameLC === 'url') { return null; } + else { i += name.length; } if (nameLC === 'alpha') { alpha_ret = $(this.alpha); @@ -673,7 +709,9 @@ less.Parser = function Parser(env) { while (arg = $(this.entities.assignment) || $(this.expression)) { args.push(arg); - if (! $(',')) { break } + if (! $(',')) { + break; + } } return args; }, @@ -707,12 +745,16 @@ less.Parser = function Parser(env) { url: function () { var value; - if (input.charAt(i) !== 'u' || !$(/^url\(/)) return; + if (input.charAt(i) !== 'u' || !$(/^url\(/)) { + return; + } + value = $(this.entities.quoted) || $(this.entities.variable) || $(/^(?:(?:\\[\(\)'"])|[^\(\)'"])+/) || ""; expect(')'); + /*jshint eqnull:true */ return new(tree.URL)((value.value != null || value instanceof tree.Variable) ? value : new(tree.Anonymous)(value), env.currentFileInfo); }, @@ -735,7 +777,7 @@ less.Parser = function Parser(env) { // A variable entity useing the protective {} e.g. @{var} variableCurly: function () { - var name, curly, index = i; + var curly, index = i; if (input.charAt(i) === '@' && (curly = $(/^@\{([\w-]+)\}/))) { return new(tree.Variable)("@" + curly[1], index, env.currentFileInfo); @@ -765,7 +807,9 @@ less.Parser = function Parser(env) { dimension: function () { var value, c = input.charCodeAt(i); //Is the first char of the dimension 0-9, '.', '+' or '-' - if ((c > 57 || c < 43) || c === 47 || c == 44) return; + if ((c > 57 || c < 43) || c === 47 || c == 44) { + return; + } if (value = $(/^([+-]?\d*\.?\d+)(%|[a-z]+)?/)) { return new(tree.Dimension)(value[1], value[2]); @@ -815,7 +859,7 @@ less.Parser = function Parser(env) { variable: function () { var name; - if (input.charAt(i) === '@' && (name = $(/^(@[\w-]+)\s*:/))) { return name[1] } + if (input.charAt(i) === '@' && (name = $(/^(@[\w-]+)\s*:/))) { return name[1]; } }, // @@ -841,7 +885,7 @@ less.Parser = function Parser(env) { extendList.push(new(tree.Extend)(new(tree.Selector)(elements), option, index)); - } while($(",")) + } while($(",")); expect(/^\)/); @@ -875,14 +919,14 @@ less.Parser = function Parser(env) { // selector for now. // call: function () { - var elements = [], e, c, args, delim, arg, index = i, s = input.charAt(i), important = false; + var elements = [], e, c, args, index = i, s = input.charAt(i), important = false; - if (s !== '.' && s !== '#') { return } + if (s !== '.' && s !== '#') { return; } save(); // stop us absorbing part of an invalid selector while (e = $(/^[#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/)) { - elements.push(new(tree.Element)(c, e, i)); + elements.push(new(tree.Element)(c, e, i, env.currentFileInfo)); c = $('>'); } if ($('(')) { @@ -937,7 +981,7 @@ less.Parser = function Parser(env) { if (isCall) { // Variable if (arg.value.length == 1) { - var val = arg.value[0]; + val = arg.value[0]; } } else { val = arg; @@ -986,7 +1030,7 @@ less.Parser = function Parser(env) { isSemiColonSeperated = true; if (expressions.length > 1) { - value = new (tree.Value)(expressions); + value = new(tree.Value)(expressions); } argsSemiColon.push({ name:name, value:value }); @@ -1019,9 +1063,11 @@ less.Parser = function Parser(env) { // the `{...}` block. // definition: function () { - var name, params = [], match, ruleset, param, value, cond, variadic = false; + var name, params = [], match, ruleset, cond, variadic = false; if ((input.charAt(i) !== '.' && input.charAt(i) !== '#') || - peek(/^[^{]*\}/)) return; + peek(/^[^{]*\}/)) { + return; + } save(); @@ -1083,7 +1129,7 @@ less.Parser = function Parser(env) { alpha: function () { var value; - if (! $(/^\(opacity=/i)) return; + if (! $(/^\(opacity=/i)) { return; } if (value = $(/^\d+/) || $(this.entities.variable)) { expect(')'); return new(tree.Alpha)(value); @@ -1103,7 +1149,7 @@ less.Parser = function Parser(env) { // and an element name, such as a tag a class, or `*`. // element: function () { - var e, t, c, v; + var e, c, v; c = $(this.combinator); @@ -1119,7 +1165,7 @@ less.Parser = function Parser(env) { } } - if (e) { return new(tree.Element)(c, e, i) } + if (e) { return new(tree.Element)(c, e, i, env.currentFileInfo); } }, // @@ -1136,7 +1182,7 @@ less.Parser = function Parser(env) { if (c === '>' || c === '+' || c === '~' || c === '|') { i++; - while (input.charAt(i).match(/\s/)) { i++ } + while (input.charAt(i).match(/\s/)) { i++; } return new(tree.Combinator)(c); } else if (input.charAt(i - 1).match(/\s/)) { return new(tree.Combinator)(" "); @@ -1160,7 +1206,7 @@ less.Parser = function Parser(env) { // Selectors are made out of one or more Elements, see above. // selector: function (isLess) { - var sel, e, elements = [], c, extend, extendList = [], when, condition; + var e, elements = [], c, extend, extendList = [], when, condition; while ((isLess && (extend = $(this.extend))) || (isLess && (when = $(/^when/))) || (e = $(this.element))) { if (when) { @@ -1174,19 +1220,21 @@ less.Parser = function Parser(env) { error("Extend can only be used at the end of selector"); } c = input.charAt(i); - elements.push(e) + elements.push(e); e = null; } - if (c === '{' || c === '}' || c === ';' || c === ',' || c === ')') { break } + if (c === '{' || c === '}' || c === ';' || c === ',' || c === ')') { + break; + } } if (elements.length > 0) { return new(tree.Selector)(elements, extendList, condition, i, env.currentFileInfo); } if (extendList.length) { error("Extend must be used to extend a selector, it cannot be used on its own"); } }, attribute: function () { - var attr = '', key, val, op; + var key, val, op; - if (! $('[')) return; + if (! $('[')) { return; } if (!(key = $(this.entities.variableCurly))) { key = expect(/^(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\.)+/); @@ -1220,8 +1268,9 @@ less.Parser = function Parser(env) { save(); - if (env.dumpLineNumbers) + if (env.dumpLineNumbers) { debugInfo = getDebugInfo(i, input, env); + } while (s = $(this.lessSelector)) { selectors.push(s); @@ -1235,8 +1284,9 @@ less.Parser = function Parser(env) { if (selectors.length > 0 && (rules = $(this.block))) { var ruleset = new(tree.Ruleset)(selectors, rules, env.strictImports); - if (env.dumpLineNumbers) + if (env.dumpLineNumbers) { ruleset.debugInfo = debugInfo; + } return ruleset; } else { // Backtrack @@ -1248,7 +1298,7 @@ less.Parser = function Parser(env) { var name, value, c = input.charAt(i), important, merge = false; save(); - if (c === '.' || c === '#' || c === '&') { return } + if (c === '.' || c === '#' || c === '&') { return; } if (name = $(this.variable) || $(this.ruleProperty)) { // prefer to try to parse first if its a variable or we are compressing @@ -1333,7 +1383,7 @@ less.Parser = function Parser(env) { break; } options[optionName] = value; - if (! $(',')) { break } + if (! $(',')) { break; } } } while (o); expect(')'); @@ -1364,7 +1414,7 @@ less.Parser = function Parser(env) { } else { return null; } - } else { return null } + } else { return null; } } } while (e); @@ -1379,10 +1429,10 @@ less.Parser = function Parser(env) { do { if (e = $(this.mediaFeature)) { features.push(e); - if (! $(',')) { break } + if (! $(',')) { break; } } else if (e = $(this.entities.variable)) { features.push(e); - if (! $(',')) { break } + if (! $(',')) { break; } } } while (e); @@ -1392,16 +1442,18 @@ less.Parser = function Parser(env) { media: function () { var features, rules, media, debugInfo; - if (env.dumpLineNumbers) + if (env.dumpLineNumbers) { debugInfo = getDebugInfo(i, input, env); + } if ($(/^@media/)) { features = $(this.mediaFeatures); if (rules = $(this.block)) { media = new(tree.Media)(rules, features, i, env.currentFileInfo); - if(env.dumpLineNumbers) + if (env.dumpLineNumbers) { media.debugInfo = debugInfo; + } return media; } } @@ -1413,10 +1465,10 @@ less.Parser = function Parser(env) { // @charset "utf-8"; // directive: function () { - var name, value, rules, identifier, e, nodes, nonVendorSpecificName, + var name, value, rules, nonVendorSpecificName, hasBlock, hasIdentifier, hasExpression; - if (input.charAt(i) !== '@') return; + if (input.charAt(i) !== '@') { return; } if (value = $(this['import']) || $(this.media)) { return value; @@ -1426,7 +1478,7 @@ less.Parser = function Parser(env) { name = $(/^@[a-z-]+/); - if (!name) return; + if (!name) { return; } nonVendorSpecificName = name; if (name.charAt(1) == '-' && name.indexOf('-', 2) > 0) { @@ -1499,11 +1551,11 @@ less.Parser = function Parser(env) { // and before the `;`. // value: function () { - var e, expressions = [], important; + var e, expressions = []; while (e = $(this.expression)) { expressions.push(e); - if (! $(',')) { break } + if (! $(',')) { break; } } if (expressions.length > 0) { @@ -1528,7 +1580,7 @@ less.Parser = function Parser(env) { } }, multiplication: function () { - var m, a, op, operation, isSpaced, expression = []; + var m, a, op, operation, isSpaced; if (m = $(this.operand)) { isSpaced = isWhitespace(input.charAt(i - 1)); while (!peek(/^\/[*\/]/) && (op = ($('/') || $('*')))) { @@ -1571,7 +1623,7 @@ less.Parser = function Parser(env) { condition: function () { var a, b, c, op, index = i, negate = false; - if ($(/^not/)) { negate = true } + if ($(/^not/)) { negate = true; } expect('('); if (a = $(this.addition) || $(this.entities.keyword) || $(this.entities.quoted)) { if (op = $(/^(?:>=|=<|[<=>])/)) { @@ -1595,7 +1647,7 @@ less.Parser = function Parser(env) { operand: function () { var negate, p = input.charAt(i + 1); - if (input.charAt(i) === '-' && (p === '@' || p === '(')) { negate = $('-') } + if (input.charAt(i) === '-' && (p === '@' || p === '(')) { negate = $('-'); } var o = $(this.sub) || $(this.entities.dimension) || $(this.entities.color) || $(this.entities.variable) || $(this.entities.call); @@ -1616,7 +1668,7 @@ less.Parser = function Parser(env) { // @var * 2 // expression: function () { - var e, delim, entities = [], d; + var e, delim, entities = []; while (e = $(this.addition) || $(this.entity)) { entities.push(e); diff --git a/lib/less/rhino.js b/lib/less/rhino.js index fe917a2b..5feb2e74 100644 --- a/lib/less/rhino.js +++ b/lib/less/rhino.js @@ -1,3 +1,5 @@ +/*jshint rhino:true, unused: false */ +/*global name:true, less, loadStyleSheet */ var name; function loadStyleSheet(sheet, callback, reload, remaining) { @@ -61,7 +63,7 @@ function writeFile(filename, content) { print('No files present in the fileset; Check your pattern match in build.xml'); quit(1); } - path = name.split("/");path.pop();path=path.join("/") + path = name.split("/");path.pop();path=path.join("/"); var input = readFile(name); @@ -109,7 +111,7 @@ function error(e, filename) { var errorline = function (e, i, classname) { if (e.extract[i]) { content += - String(parseInt(e.line) + (i - 1)) + + String(parseInt(e.line, 10) + (i - 1)) + ":" + e.extract[i] + "\n"; } }; @@ -123,4 +125,4 @@ function error(e, filename) { errorline(e, 2); } print(content); -} \ No newline at end of file +} diff --git a/lib/less/source-map-output.js b/lib/less/source-map-output.js new file mode 100644 index 00000000..8988eb23 --- /dev/null +++ b/lib/less/source-map-output.js @@ -0,0 +1,84 @@ +(function (tree) { + var sourceMap = require("source-map"); + + tree.sourceMapOutput = function (options) { + this._css = []; + this._rootNode = options.rootNode; + this._writeSourceMap = options.writeSourceMap; + this._contentsMap = options.contentsMap; + this._sourceMapFilename = options.sourceMapFilename; + this._outputFilename = options.outputFilename; + this._sourceMapBasepath = options.sourceMapBasepath; + this._sourceMapRootpath = options.sourceMapRootpath; + this._outputSourceFiles = options.outputSourceFiles; + + if (this._sourceMapRootpath && this._sourceMapRootpath.charAt(this._sourceMapRootpath.length-1) !== '/') { + this._sourceMapRootpath += '/'; + } + + this._lineNumber = 0; + this._column = 0; + }; + + tree.sourceMapOutput.prototype.normalizeFilename = function(filename) { + if (this._sourceMapBasepath && filename.indexOf(this._sourceMapBasepath) === 0) { + filename = filename.substring(this._sourceMapBasepath.length); + if (filename.charAt(0) === '\\' || filename.charAt(0) === '/') { + filename = filename.substring(1); + } + } + return this._sourceMapRootpath + filename.replace(/\\/g, '/'); + }; + + tree.sourceMapOutput.prototype.add = function(chunk, fileInfo, index) { + + if (!chunk) { + //TODO what is calling this with undefined? + return; + } + + var lines, + columns; + + if (fileInfo) { + var inputSource = this._contentsMap[fileInfo.filename].substring(0, index); + lines = inputSource.split("\n"); + columns = lines[lines.length-1]; + this._sourceMapGenerator.addMapping({ generated: { line: this._lineNumber + 1, column: this._column}, + original: { line: lines.length, column: columns.length}, + source: this.normalizeFilename(fileInfo.filename)}); + } + lines = chunk.split("\n"); + columns = lines[lines.length-1]; + + if (lines.length === 1) { + this._column += columns.length; + } else { + this._lineNumber += lines.length - 1; + this._column = columns.length; + } + + this._css.push(chunk); + }; + + tree.sourceMapOutput.prototype.toCSS = function(env) { + this._sourceMapGenerator = new sourceMap.SourceMapGenerator({ file: this._outputFilename, sourceRoot: null }); + + if (this._outputSourceFiles) { + for(var filename in this._contentsMap) { + this._sourceMapGenerator.setSourceContent(this.normalizeFilename(filename), this._contentsMap[filename]); + } + } + + this._rootNode.genCSS(env, this); + + this._writeSourceMap(JSON.stringify(this._sourceMapGenerator.toJSON())); + + if (this._sourceMapFilename) { + this._css.push("/*# sourceMappingURL=" + this._sourceMapRootpath + this._sourceMapFilename + " */"); + } + + return this._css.join(''); + }; + +})(require('./tree')); \ No newline at end of file diff --git a/lib/less/to-css-visitor.js b/lib/less/to-css-visitor.js index 0af0f157..a9988c6f 100644 --- a/lib/less/to-css-visitor.js +++ b/lib/less/to-css-visitor.js @@ -21,6 +21,10 @@ return []; }, + visitExtend: function (extendNode, visitArgs) { + return []; + }, + visitComment: function (commentNode, visitArgs) { if (commentNode.isSilent(this._env)) { return []; @@ -28,7 +32,20 @@ return commentNode; }, + visitMedia: function(mediaNode, visitArgs) { + mediaNode.accept(this._visitor); + visitArgs.visitDeeper = false; + + if (!mediaNode.rules.length) { + return []; + } + return mediaNode; + }, + visitDirective: function(directiveNode, visitArgs) { + if (directiveNode.currentFileInfo.reference && !directiveNode.isReferenced) { + return []; + } if (directiveNode.name === "@charset") { // Only output the debug info together with subsequent @charset definitions // a comment (or @media statement) before the actual @charset directive would @@ -67,6 +84,9 @@ rulesetNode.paths = rulesetNode.paths .filter(function(p) { var i; + if (p[0].elements[0].combinator.value === ' ') { + p[0].elements[0].combinator = new(tree.Combinator)(''); + } for(i = 0; i < p.length; i++) { if (p[i].getIsReferenced() && p[i].getIsOutput()) { return true; @@ -89,20 +109,90 @@ } // accept the visitor to remove rules and refactor itself // then we can decide now whether we want it or not - if (rulesetNode.rules.length > 0 && rulesetNode.paths.length > 0) { + if (rulesetNode.rules.length > 0) { rulesetNode.accept(this._visitor); } visitArgs.visitDeeper = false; + this._mergeRules(rulesetNode.rules); + this._removeDuplicateRules(rulesetNode.rules); + // now decide whether we keep the ruleset - if (rulesets.length > 0 && rulesetNode.rules.length > 0 && rulesetNode.paths.length > 0) { + if (rulesetNode.rules.length > 0 && rulesetNode.paths.length > 0) { + rulesets.splice(0, 0, rulesetNode); + } + } else { + rulesetNode.accept(this._visitor); + visitArgs.visitDeeper = false; + if (rulesetNode.firstRoot || rulesetNode.rules.length > 0) { rulesets.splice(0, 0, rulesetNode); } } - if (rulesets.length === 0) { - rulesets = rulesetNode; + if (rulesets.length === 1) { + return rulesets[0]; } return rulesets; + }, + + _removeDuplicateRules: function(rules) { + // remove duplicates + var ruleCache = {}, + ruleList, rule, i; + for(i = rules.length - 1; i >= 0 ; i--) { + rule = rules[i]; + if (rule instanceof tree.Rule) { + if (!ruleCache[rule.name]) { + ruleCache[rule.name] = rule; + } else { + ruleList = ruleCache[rule.name]; + if (ruleList instanceof tree.Rule) { + ruleList = ruleCache[rule.name] = [ruleCache[rule.name].toCSS(this._env)]; + } + var ruleCSS = rule.toCSS(this._env); + if (ruleList.indexOf(ruleCSS) !== -1) { + rules.splice(i, 1); + } else { + ruleList.push(ruleCSS); + } + } + } + } + }, + + _mergeRules: function (rules) { + var groups = {}, + parts, + rule, + key; + + for (var i = 0; i < rules.length; i++) { + rule = rules[i]; + + if ((rule instanceof tree.Rule) && rule.merge) { + key = [rule.name, + rule.important ? "!" : ""].join(","); + + if (!groups[key]) { + parts = groups[key] = []; + } else { + rules.splice(i--, 1); + } + + parts.push(rule); + } + } + + Object.keys(groups).map(function (k) { + parts = groups[k]; + + if (parts.length > 1) { + rule = parts[0]; + + rule.value = new (tree.Value)(parts.map(function (p) { + return p.value; + })); + } + }); } }; diff --git a/lib/less/tree.js b/lib/less/tree.js index 9aee4613..d44d2339 100644 --- a/lib/less/tree.js +++ b/lib/less/tree.js @@ -1,6 +1,6 @@ (function (tree) { -tree.debugInfo = function(env, ctx) { +tree.debugInfo = function(env, ctx, lineSeperator) { var result=""; if (env.dumpLineNumbers && !env.compress) { switch(env.dumpLineNumbers) { @@ -11,7 +11,7 @@ tree.debugInfo = function(env, ctx) { result = tree.debugInfo.asMediaQuery(ctx); break; case 'all': - result = tree.debugInfo.asComment(ctx)+tree.debugInfo.asMediaQuery(ctx); + result = tree.debugInfo.asComment(ctx) + (lineSeperator || "") + tree.debugInfo.asMediaQuery(ctx); break; } } @@ -24,22 +24,52 @@ tree.debugInfo.asComment = function(ctx) { tree.debugInfo.asMediaQuery = function(ctx) { return '@media -sass-debug-info{filename{font-family:' + - ('file://' + ctx.debugInfo.fileName).replace(/([.:/\\])/g, function(a){if(a=='\\') a = '\/'; return '\\' + a}) + + ('file://' + ctx.debugInfo.fileName).replace(/([.:/\\])/g, function (a) { + if (a == '\\') { + a = '\/'; + } + return '\\' + a; + }) + '}line{font-family:\\00003' + ctx.debugInfo.lineNumber + '}}\n'; }; tree.find = function (obj, fun) { for (var i = 0, r; i < obj.length; i++) { - if (r = fun.call(obj, obj[i])) { return r } + if (r = fun.call(obj, obj[i])) { return r; } } return null; }; + tree.jsify = function (obj) { if (Array.isArray(obj.value) && (obj.value.length > 1)) { - return '[' + obj.value.map(function (v) { return v.toCSS(false) }).join(', ') + ']'; + return '[' + obj.value.map(function (v) { return v.toCSS(false); }).join(', ') + ']'; } else { return obj.toCSS(false); } }; +tree.toCSS = function (env) { + var strs = []; + this.genCSS(env, { + add: function(chunk, node) { + strs.push(chunk); + } + }); + return strs.join(''); +}; + +tree.outputRuleset = function (env, output, rules) { + output.add((env.compress ? '{' : ' {\n')); + env.tabLevel = (env.tabLevel || 0) + 1; + var tabRuleStr = env.compress ? '' : Array(env.tabLevel + 1).join(" "), + tabSetStr = env.compress ? '' : Array(env.tabLevel).join(" "); + for(var i = 0; i < rules.length; i++) { + output.add(tabRuleStr); + rules[i].genCSS(env, output); + output.add(env.compress ? '' : '\n'); + } + env.tabLevel--; + output.add(tabSetStr + "}"); +}; + })(require('./tree')); diff --git a/lib/less/tree/alpha.js b/lib/less/tree/alpha.js index cb8db113..e827fd08 100644 --- a/lib/less/tree/alpha.js +++ b/lib/less/tree/alpha.js @@ -9,13 +9,21 @@ tree.Alpha.prototype = { this.value = visitor.visit(this.value); }, eval: function (env) { - if (this.value.eval) { this.value = this.value.eval(env); } + if (this.value.eval) { return new tree.Alpha(this.value.eval(env)); } return this; }, - toCSS: function () { - return "alpha(opacity=" + - (this.value.toCSS ? this.value.toCSS() : this.value) + ")"; - } + genCSS: function (env, output) { + output.add("alpha(opacity="); + + if (this.value.genCSS) { + this.value.genCSS(env, output); + } else { + output.add(this.value); + } + + output.add(")"); + }, + toCSS: tree.toCSS }; })(require('../tree')); diff --git a/lib/less/tree/anonymous.js b/lib/less/tree/anonymous.js index a009cf27..216a43db 100644 --- a/lib/less/tree/anonymous.js +++ b/lib/less/tree/anonymous.js @@ -5,10 +5,7 @@ tree.Anonymous = function (string) { }; tree.Anonymous.prototype = { type: "Anonymous", - toCSS: function () { - return this.value; - }, - eval: function () { return this }, + eval: function () { return this; }, compare: function (x) { if (!x.toCSS) { return -1; @@ -22,7 +19,11 @@ tree.Anonymous.prototype = { } return left < right ? -1 : 1; - } + }, + genCSS: function (env, output) { + output.add(this.value); + }, + toCSS: tree.toCSS }; })(require('../tree')); diff --git a/lib/less/tree/assignment.js b/lib/less/tree/assignment.js index b0e2c555..61d22fca 100644 --- a/lib/less/tree/assignment.js +++ b/lib/less/tree/assignment.js @@ -9,15 +9,21 @@ tree.Assignment.prototype = { accept: function (visitor) { this.value = visitor.visit(this.value); }, - toCSS: function () { - return this.key + '=' + (this.value.toCSS ? this.value.toCSS() : this.value); - }, eval: function (env) { if (this.value.eval) { return new(tree.Assignment)(this.key, this.value.eval(env)); } return this; - } + }, + genCSS: function (env, output) { + output.add(this.key + '='); + if (this.value.genCSS) { + this.value.genCSS(env, output); + } else { + output.add(this.value); + } + }, + toCSS: tree.toCSS }; -})(require('../tree')); \ No newline at end of file +})(require('../tree')); diff --git a/lib/less/tree/call.js b/lib/less/tree/call.js index 20a1fc0e..6d262b81 100644 --- a/lib/less/tree/call.js +++ b/lib/less/tree/call.js @@ -36,6 +36,7 @@ tree.Call.prototype = { try { func = new tree.functionCall(env, this.currentFileInfo); result = func[nameLC].apply(func, args); + /*jshint eqnull:true */ if (result != null) { return result; } @@ -46,15 +47,24 @@ tree.Call.prototype = { index: this.index, filename: this.currentFileInfo.filename }; } } - - // 2. - return new(tree.Anonymous)(this.name + - "(" + args.map(function (a) { return a.toCSS(env); }).join(', ') + ")"); + + return new tree.Call(this.name, args, this.index, this.currentFileInfo); }, - toCSS: function (env) { - return this.eval(env).toCSS(); - } + genCSS: function (env, output) { + output.add(this.name + "(", this.currentFileInfo, this.index); + + for(var i = 0; i < this.args.length; i++) { + this.args[i].genCSS(env, output); + if (i + 1 < this.args.length) { + output.add(", "); + } + } + + output.add(")"); + }, + + toCSS: tree.toCSS }; })(require('../tree')); diff --git a/lib/less/tree/color.js b/lib/less/tree/color.js index ae8878ec..c5ad64fc 100644 --- a/lib/less/tree/color.js +++ b/lib/less/tree/color.js @@ -24,17 +24,19 @@ tree.Color = function (rgb, a) { }; tree.Color.prototype = { type: "Color", - eval: function () { return this }, + eval: function () { return this; }, luma: function () { return (0.2126 * this.rgb[0] / 255) + (0.7152 * this.rgb[1] / 255) + (0.0722 * this.rgb[2] / 255); }, - // - // If we have some transparency, the only way to represent it - // is via `rgba`. Otherwise, we use the hex representation, - // which has better compatibility with older browsers. - // Values are capped between `0` and `255`, rounded and zero-padded. - // + genCSS: function (env, output) { + output.add(this.toCSS(env)); + }, toCSS: function (env, doNotCompress) { var compress = env && env.compress && !doNotCompress; + + // If we have some transparency, the only way to represent it + // is via `rgba`. Otherwise, we use the hex representation, + // which has better compatibility with older browsers. + // Values are capped between `0` and `255`, rounded and zero-padded. if (this.alpha < 1.0) { return "rgba(" + this.rgb.map(function (c) { return Math.round(c); @@ -46,7 +48,7 @@ tree.Color.prototype = { var splitcolor = color.split(''); // Convert color to short format - if (splitcolor[1] == splitcolor[2] && splitcolor[3] == splitcolor[4] && splitcolor[5] == splitcolor[6]) { + if (splitcolor[1] === splitcolor[2] && splitcolor[3] === splitcolor[4] && splitcolor[5] === splitcolor[6]) { color = '#' + splitcolor[1] + splitcolor[3] + splitcolor[5]; } } diff --git a/lib/less/tree/comment.js b/lib/less/tree/comment.js index b9724633..c39606e0 100644 --- a/lib/less/tree/comment.js +++ b/lib/less/tree/comment.js @@ -7,13 +7,13 @@ tree.Comment = function (value, silent, index, currentFileInfo) { }; tree.Comment.prototype = { type: "Comment", - toCSS: function (env) { - var debugInfo = ""; + genCSS: function (env, output) { if (this.debugInfo) { - debugInfo = tree.debugInfo(env, this); + output.add(tree.debugInfo(env, this), this.currentFileInfo, this.index); } - return debugInfo + this.value; + output.add(this.value.trim()); //TODO shouldn't need to trim, we shouldn't grab the \n }, + toCSS: tree.toCSS, isSilent: function(env) { var isReference = (this.currentFileInfo && this.currentFileInfo.reference && !this.isReferenced), isCompressed = env.compress && !this.value.match(/^\/\*!/); diff --git a/lib/less/tree/condition.js b/lib/less/tree/condition.js index 8608b69d..47b1d008 100644 --- a/lib/less/tree/condition.js +++ b/lib/less/tree/condition.js @@ -19,7 +19,7 @@ tree.Condition.prototype = { var i = this.index, result; - var result = (function (op) { + result = (function (op) { switch (op) { case 'and': return a && b; diff --git a/lib/less/tree/dimension.js b/lib/less/tree/dimension.js index 9850e6ff..7d5044d9 100644 --- a/lib/less/tree/dimension.js +++ b/lib/less/tree/dimension.js @@ -20,7 +20,7 @@ tree.Dimension.prototype = { toColor: function () { return new(tree.Color)([this.value, this.value, this.value]); }, - toCSS: function (env) { + genCSS: function (env, output) { if ((env && env.strictUnits) && !this.unit.isSingular()) { throw new Error("Multiple units in dimension. Correct the units or use the unit function. Bad unit: "+this.unit.toString()); } @@ -36,7 +36,8 @@ tree.Dimension.prototype = { if (env && env.compress) { // Zero values doesn't need a unit if (value === 0 && !this.unit.isAngle()) { - return strValue; + output.add(strValue); + return; } // Float values doesn't need a leading zero @@ -45,13 +46,16 @@ tree.Dimension.prototype = { } } - return strValue + this.unit.toCSS(env); + output.add(strValue); + this.unit.genCSS(env, output); }, + toCSS: tree.toCSS, // In an operation between two Dimensions, // we default to the first Dimension's unit, // so `1px + 2` will yield `3px`. operate: function (env, op, other) { + /*jshint noempty:false */ var value = tree.operate(env, op, this.value, other.value), unit = this.unit.clone(); @@ -59,7 +63,7 @@ tree.Dimension.prototype = { if (unit.numerator.length === 0 && unit.denominator.length === 0) { unit.numerator = other.unit.numerator.slice(0); unit.denominator = other.unit.denominator.slice(0); - } else if (other.unit.numerator.length == 0 && unit.denominator.length == 0) { + } else if (other.unit.numerator.length === 0 && unit.denominator.length === 0) { // do nothing } else { other = other.convertTo(this.unit.usedUnits()); @@ -104,202 +108,206 @@ tree.Dimension.prototype = { }, unify: function () { - return this.convertTo({ length: 'm', duration: 's', angle: 'rad' }); + return this.convertTo({ length: 'm', duration: 's', angle: 'rad' }); }, convertTo: function (conversions) { - var value = this.value, unit = this.unit.clone(), - i, groupName, group, conversion, targetUnit, derivedConversions = {}; + var value = this.value, unit = this.unit.clone(), + i, groupName, group, targetUnit, derivedConversions = {}, applyUnit; - if (typeof conversions === 'string') { - for(i in tree.UnitConversions) { - if (tree.UnitConversions[i].hasOwnProperty(conversions)) { - derivedConversions = {}; - derivedConversions[i] = conversions; - } - } - conversions = derivedConversions; - } - - for (groupName in conversions) { - if (conversions.hasOwnProperty(groupName)) { - targetUnit = conversions[groupName]; - group = tree.UnitConversions[groupName]; - - unit.map(function (atomicUnit, denominator) { + if (typeof conversions === 'string') { + for(i in tree.UnitConversions) { + if (tree.UnitConversions[i].hasOwnProperty(conversions)) { + derivedConversions = {}; + derivedConversions[i] = conversions; + } + } + conversions = derivedConversions; + } + applyUnit = function (atomicUnit, denominator) { + /*jshint loopfunc:true */ if (group.hasOwnProperty(atomicUnit)) { - if (denominator) { - value = value / (group[atomicUnit] / group[targetUnit]); - } else { - value = value * (group[atomicUnit] / group[targetUnit]); - } + if (denominator) { + value = value / (group[atomicUnit] / group[targetUnit]); + } else { + value = value * (group[atomicUnit] / group[targetUnit]); + } - return targetUnit; + return targetUnit; } return atomicUnit; - }); + }; + + for (groupName in conversions) { + if (conversions.hasOwnProperty(groupName)) { + targetUnit = conversions[groupName]; + group = tree.UnitConversions[groupName]; + + unit.map(applyUnit); + } } - } - unit.cancel(); + unit.cancel(); - return new(tree.Dimension)(value, unit); + return new(tree.Dimension)(value, unit); } }; // http://www.w3.org/TR/css3-values/#absolute-lengths tree.UnitConversions = { - length: { - 'm': 1, - 'cm': 0.01, - 'mm': 0.001, - 'in': 0.0254, - 'pt': 0.0254 / 72, - 'pc': 0.0254 / 72 * 12 - }, - duration: { - 's': 1, - 'ms': 0.001 - }, - angle: { - 'rad': 1/(2*Math.PI), - 'deg': 1/360, - 'grad': 1/400, - 'turn': 1 - } + length: { + 'm': 1, + 'cm': 0.01, + 'mm': 0.001, + 'in': 0.0254, + 'pt': 0.0254 / 72, + 'pc': 0.0254 / 72 * 12 + }, + duration: { + 's': 1, + 'ms': 0.001 + }, + angle: { + 'rad': 1/(2*Math.PI), + 'deg': 1/360, + 'grad': 1/400, + 'turn': 1 + } }; tree.Unit = function (numerator, denominator, backupUnit) { - this.numerator = numerator ? numerator.slice(0).sort() : []; - this.denominator = denominator ? denominator.slice(0).sort() : []; - this.backupUnit = backupUnit; + this.numerator = numerator ? numerator.slice(0).sort() : []; + this.denominator = denominator ? denominator.slice(0).sort() : []; + this.backupUnit = backupUnit; }; tree.Unit.prototype = { - type: "Unit", - clone: function () { - return new tree.Unit(this.numerator.slice(0), this.denominator.slice(0), this.backupUnit); - }, + type: "Unit", + clone: function () { + return new tree.Unit(this.numerator.slice(0), this.denominator.slice(0), this.backupUnit); + }, + genCSS: function (env, output) { + if (this.numerator.length >= 1) { + output.add(this.numerator[0]); + } else + if (this.denominator.length >= 1) { + output.add(this.denominator[0]); + } else + if ((!env || !env.strictUnits) && this.backupUnit) { + output.add(this.backupUnit); + } + }, + toCSS: tree.toCSS, - toCSS: function (env) { - if (this.numerator.length >= 1) { - return this.numerator[0]; - } - if (this.denominator.length >= 1) { - return this.denominator[0]; - } - if ((!env || !env.strictUnits) && this.backupUnit) { - return this.backupUnit; - } - return ""; - }, - - toString: function () { + toString: function () { var i, returnStr = this.numerator.join("*"); for (i = 0; i < this.denominator.length; i++) { returnStr += "/" + this.denominator[i]; } return returnStr; - }, - - compare: function (other) { - return this.is(other.toString()) ? 0 : -1; - }, + }, - is: function (unitString) { - return this.toString() === unitString; - }, + compare: function (other) { + return this.is(other.toString()) ? 0 : -1; + }, - isAngle: function () { - return tree.UnitConversions.angle.hasOwnProperty(this.toCSS()); - }, + is: function (unitString) { + return this.toString() === unitString; + }, - isEmpty: function () { - return this.numerator.length == 0 && this.denominator.length == 0; - }, + isAngle: function () { + return tree.UnitConversions.angle.hasOwnProperty(this.toCSS()); + }, - isSingular: function() { - return this.numerator.length <= 1 && this.denominator.length == 0; - }, + isEmpty: function () { + return this.numerator.length === 0 && this.denominator.length === 0; + }, - map: function(callback) { - var i; + isSingular: function() { + return this.numerator.length <= 1 && this.denominator.length === 0; + }, - for (i = 0; i < this.numerator.length; i++) { - this.numerator[i] = callback(this.numerator[i], false); - } + map: function(callback) { + var i; - for (i = 0; i < this.denominator.length; i++) { - this.denominator[i] = callback(this.denominator[i], true); - } - }, - - usedUnits: function() { - var group, groupName, result = {}; - - for (groupName in tree.UnitConversions) { - if (tree.UnitConversions.hasOwnProperty(groupName)) { - group = tree.UnitConversions[groupName]; - - this.map(function (atomicUnit) { - if (group.hasOwnProperty(atomicUnit) && !result[groupName]) { - result[groupName] = atomicUnit; - } - - return atomicUnit; - }); - } - } - - return result; - }, - - cancel: function () { - var counter = {}, atomicUnit, i, backup; - - for (i = 0; i < this.numerator.length; i++) { - atomicUnit = this.numerator[i]; - if (!backup) { - backup = atomicUnit; + for (i = 0; i < this.numerator.length; i++) { + this.numerator[i] = callback(this.numerator[i], false); } - counter[atomicUnit] = (counter[atomicUnit] || 0) + 1; - } - for (i = 0; i < this.denominator.length; i++) { - atomicUnit = this.denominator[i]; - if (!backup) { - backup = atomicUnit; + for (i = 0; i < this.denominator.length; i++) { + this.denominator[i] = callback(this.denominator[i], true); } - counter[atomicUnit] = (counter[atomicUnit] || 0) - 1; - } + }, - this.numerator = []; - this.denominator = []; + usedUnits: function() { + var group, result = {}, mapUnit; - for (atomicUnit in counter) { - if (counter.hasOwnProperty(atomicUnit)) { - var count = counter[atomicUnit]; + mapUnit = function (atomicUnit) { + /*jshint loopfunc:true */ + if (group.hasOwnProperty(atomicUnit) && !result[groupName]) { + result[groupName] = atomicUnit; + } - if (count > 0) { - for (i = 0; i < count; i++) { - this.numerator.push(atomicUnit); - } - } else if (count < 0) { - for (i = 0; i < -count; i++) { - this.denominator.push(atomicUnit); - } + return atomicUnit; + }; + + for (var groupName in tree.UnitConversions) { + if (tree.UnitConversions.hasOwnProperty(groupName)) { + group = tree.UnitConversions[groupName]; + + this.map(mapUnit); + } } - } - } - if (this.numerator.length === 0 && this.denominator.length === 0 && backup) { - this.backupUnit = backup; - } + return result; + }, - this.numerator.sort(); - this.denominator.sort(); - } + cancel: function () { + var counter = {}, atomicUnit, i, backup; + + for (i = 0; i < this.numerator.length; i++) { + atomicUnit = this.numerator[i]; + if (!backup) { + backup = atomicUnit; + } + counter[atomicUnit] = (counter[atomicUnit] || 0) + 1; + } + + for (i = 0; i < this.denominator.length; i++) { + atomicUnit = this.denominator[i]; + if (!backup) { + backup = atomicUnit; + } + counter[atomicUnit] = (counter[atomicUnit] || 0) - 1; + } + + this.numerator = []; + this.denominator = []; + + for (atomicUnit in counter) { + if (counter.hasOwnProperty(atomicUnit)) { + var count = counter[atomicUnit]; + + if (count > 0) { + for (i = 0; i < count; i++) { + this.numerator.push(atomicUnit); + } + } else if (count < 0) { + for (i = 0; i < -count; i++) { + this.denominator.push(atomicUnit); + } + } + } + } + + if (this.numerator.length === 0 && this.denominator.length === 0 && backup) { + this.backupUnit = backup; + } + + this.numerator.sort(); + this.denominator.sort(); + } }; })(require('../tree')); diff --git a/lib/less/tree/directive.js b/lib/less/tree/directive.js index 23c6016a..64082593 100644 --- a/lib/less/tree/directive.js +++ b/lib/less/tree/directive.js @@ -18,24 +18,17 @@ tree.Directive.prototype = { this.rules = visitor.visit(this.rules); this.value = visitor.visit(this.value); }, - toCSS: function (env) { - - if (this.currentFileInfo.reference && !this.isReferenced) { - return ""; - } - + genCSS: function (env, output) { + output.add(this.name, this.currentFileInfo, this.index); if (this.rules) { - var css = ""; - for(var i = 0; i < this.rules.length; i++) { - //this.rules[i].root = true; - css += this.rules[i].toCSS(env).trim() + "\n"; - } - css = css.trim().replace(/\n/g, '\n '); - return this.name + (env.compress ? '{' : ' {\n ') + css + (env.compress ? '}': '\n}\n'); + tree.outputRuleset(env, output, this.rules); } else { - return this.name + ' ' + this.value.toCSS() + ';\n'; + output.add(' '); + this.value.genCSS(env, output); + output.add(';'); } }, + toCSS: tree.toCSS, eval: function (env) { var evaldDirective = this; if (this.rules) { diff --git a/lib/less/tree/element.js b/lib/less/tree/element.js index 55d47903..1c25aeec 100644 --- a/lib/less/tree/element.js +++ b/lib/less/tree/element.js @@ -1,6 +1,6 @@ (function (tree) { -tree.Element = function (combinator, value, index) { +tree.Element = function (combinator, value, index, currentFileInfo) { this.combinator = combinator instanceof tree.Combinator ? combinator : new(tree.Combinator)(combinator); @@ -12,6 +12,7 @@ tree.Element = function (combinator, value, index) { this.value = ""; } this.index = index; + this.currentFileInfo = currentFileInfo; }; tree.Element.prototype = { type: "Element", @@ -22,11 +23,15 @@ tree.Element.prototype = { eval: function (env) { return new(tree.Element)(this.combinator, this.value.eval ? this.value.eval(env) : this.value, - this.index); + this.index, + this.currentFileInfo); + }, + genCSS: function (env, output) { + output.add(this.toCSS(env), this.currentFileInfo, this.index); }, toCSS: function (env) { var value = (this.value.toCSS ? this.value.toCSS(env) : this.value); - if (value == '' && this.combinator.value.charAt(0) == '&') { + if (value === '' && this.combinator.value.charAt(0) === '&') { return ''; } else { return this.combinator.toCSS(env || {}) + value; @@ -48,6 +53,9 @@ tree.Attribute.prototype = { return new(tree.Attribute)(this.key.eval ? this.key.eval(env) : this.key, this.op, (this.value && this.value.eval) ? this.value.eval(env) : this.value); }, + genCSS: function (env, output) { + output.add(this.toCSS(env)); + }, toCSS: function (env) { var value = this.key.toCSS ? this.key.toCSS(env) : this.key; @@ -69,17 +77,28 @@ tree.Combinator = function (value) { }; tree.Combinator.prototype = { type: "Combinator", - toCSS: function (env) { - return { - '' : '', - ' ' : ' ', - ':' : ' :', - '+' : env.compress ? '+' : ' + ', - '~' : env.compress ? '~' : ' ~ ', - '>' : env.compress ? '>' : ' > ', - '|' : '|' - }[this.value]; - } + _outputMap: { + '' : '', + ' ' : ' ', + ':' : ' :', + '+' : ' + ', + '~' : ' ~ ', + '>' : ' > ', + '|' : '|' + }, + _outputMapCompressed: { + '' : '', + ' ' : ' ', + ':' : ' :', + '+' : '+', + '~' : '~', + '>' : '>', + '|' : '|' + }, + genCSS: function (env, output) { + output.add((env.compress ? this._outputMapCompressed : this._outputMap)[this.value]); + }, + toCSS: tree.toCSS }; })(require('../tree')); diff --git a/lib/less/tree/expression.js b/lib/less/tree/expression.js index 790c2b6f..23969297 100644 --- a/lib/less/tree/expression.js +++ b/lib/less/tree/expression.js @@ -33,11 +33,15 @@ tree.Expression.prototype = { } return returnValue; }, - toCSS: function (env) { - return this.value.map(function (e) { - return e.toCSS ? e.toCSS(env) : ''; - }).join(' '); + genCSS: function (env, output) { + for(var i = 0; i < this.value.length; i++) { + this.value[i].genCSS(env, output); + if (i + 1 < this.value.length) { + output.add(" "); + } + } }, + toCSS: tree.toCSS, throwAwayComments: function () { this.value = this.value.filter(function(v) { return !(v instanceof tree.Comment); diff --git a/lib/less/tree/import.js b/lib/less/tree/import.js index f81b09c0..e4cfd9e2 100644 --- a/lib/less/tree/import.js +++ b/lib/less/tree/import.js @@ -12,8 +12,6 @@ // the file has been fetched, and parsed. // tree.Import = function (path, features, options, index, currentFileInfo) { - var that = this; - this.options = options; this.index = index; this.path = path; @@ -48,15 +46,18 @@ tree.Import.prototype = { this.root = visitor.visit(this.root); } }, - toCSS: function (env) { - var features = this.features ? ' ' + this.features.toCSS(env) : ''; - + genCSS: function (env, output) { if (this.css) { - return "@import " + this.path.toCSS() + features + ';\n'; - } else { - return ""; + output.add("@import ", this.currentFileInfo, this.index); + this.path.genCSS(env, output); + if (this.features) { + output.add(" "); + this.features.genCSS(env, output); + } + output.add(';'); } }, + toCSS: tree.toCSS, getPath: function () { if (this.path instanceof tree.Quoted) { var path = this.path.value; diff --git a/lib/less/tree/javascript.js b/lib/less/tree/javascript.js index eaa9c0fe..428a56d6 100644 --- a/lib/less/tree/javascript.js +++ b/lib/less/tree/javascript.js @@ -24,6 +24,7 @@ tree.JavaScript.prototype = { } for (var k in env.frames[0].variables()) { + /*jshint loopfunc:true */ context[k.slice(1)] = { value: env.frames[0].variables()[k].value, toJS: function () { diff --git a/lib/less/tree/keyword.js b/lib/less/tree/keyword.js index 3184fce2..8cfbb09c 100644 --- a/lib/less/tree/keyword.js +++ b/lib/less/tree/keyword.js @@ -1,10 +1,13 @@ (function (tree) { -tree.Keyword = function (value) { this.value = value }; +tree.Keyword = function (value) { this.value = value; }; tree.Keyword.prototype = { type: "Keyword", eval: function () { return this; }, - toCSS: function () { return this.value; }, + genCSS: function (env, output) { + output.add(this.value); + }, + toCSS: tree.toCSS, compare: function (other) { if (other instanceof tree.Keyword) { return other.value === this.value ? 0 : 1; diff --git a/lib/less/tree/media.js b/lib/less/tree/media.js index 57d68a2d..c5cdbd78 100644 --- a/lib/less/tree/media.js +++ b/lib/less/tree/media.js @@ -16,24 +16,12 @@ tree.Media.prototype = { this.features = visitor.visit(this.features); this.rules = visitor.visit(this.rules); }, - toCSS: function (env) { - var features = this.features.toCSS(env); - - var content = ""; - - for(var i = 0; i < this.rules.length; i++) { - content += this.rules[i].toCSS(env).trim() + "\n"; - } - - content = content.trim().replace(/\n/g, '\n '); - - if (content.match(/\S/)) { - return '@media ' + features + (env.compress ? '{' : ' {\n ') + content + - (env.compress ? '}': '\n}\n'); - } else { - return ""; - } + genCSS: function (env, output) { + output.add('@media ', this.currentFileInfo, this.index); + this.features.genCSS(env, output); + tree.outputRuleset(env, output, this.rules); }, + toCSS: tree.toCSS, eval: function (env) { if (!env.mediaBlocks) { env.mediaBlocks = []; @@ -69,13 +57,13 @@ tree.Media.prototype = { env.mediaPath.pop(); return env.mediaPath.length === 0 ? media.evalTop(env) : - media.evalNested(env) + media.evalNested(env); }, variable: function (name) { return tree.Ruleset.prototype.variable.call(this.rules[0], name); }, find: function () { return tree.Ruleset.prototype.find.apply(this.rules[0], arguments); }, rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.rules[0]); }, emptySelectors: function() { - var el = new(tree.Element)('', '&', 0); + var el = new(tree.Element)('', '&', this.index, this.currentFileInfo); return [new(tree.Selector)([el], null, null, this.index, this.currentFileInfo)]; }, markReferenced: function () { diff --git a/lib/less/tree/mixin.js b/lib/less/tree/mixin.js index b7e7f3e8..cd8bf173 100644 --- a/lib/less/tree/mixin.js +++ b/lib/less/tree/mixin.js @@ -88,7 +88,7 @@ tree.mixin.Call.prototype = { tree.mixin.Definition = function (name, params, rules, condition, variadic) { this.name = name; - this.selectors = [new(tree.Selector)([new(tree.Element)(null, name)])]; + this.selectors = [new(tree.Selector)([new(tree.Element)(null, name, this.index, this.currentFileInfo)])]; this.params = params; this.condition = condition; this.variadic = variadic; @@ -96,8 +96,8 @@ tree.mixin.Definition = function (name, params, rules, condition, variadic) { this.rules = rules; this._lookups = {}; this.required = params.reduce(function (count, p) { - if (!p.name || (p.name && !p.value)) { return count + 1 } - else { return count } + if (!p.name || (p.name && !p.value)) { return count + 1; } + else { return count; } }, 0); this.parent = tree.Ruleset.prototype; this.frames = []; @@ -109,13 +109,13 @@ tree.mixin.Definition.prototype = { this.rules = visitor.visit(this.rules); this.condition = visitor.visit(this.condition); }, - toCSS: function () { return ""; }, variable: function (name) { return this.parent.variable.call(this, name); }, variables: function () { return this.parent.variables.call(this); }, find: function () { return this.parent.find.apply(this, arguments); }, rulesets: function () { return this.parent.rulesets.apply(this); }, evalParams: function (env, mixinEnv, args, evaldArguments) { + /*jshint boss:true */ var frame = new(tree.Ruleset)(null, []), varargs, arg, params = this.params.slice(0), @@ -151,7 +151,7 @@ tree.mixin.Definition.prototype = { } argIndex = 0; for (i = 0; i < params.length; i++) { - if (evaldArguments[i]) continue; + if (evaldArguments[i]) { continue; } arg = args && args[argIndex]; @@ -193,7 +193,7 @@ tree.mixin.Definition.prototype = { var _arguments = [], mixinFrames = this.frames.concat(env.frames), frame = this.evalParams(env, new(tree.evalEnv)(env, mixinFrames), args, _arguments), - context, rules, start, ruleset; + rules, ruleset; frame.rules.unshift(new(tree.Rule)('@arguments', new(tree.Expression)(_arguments).eval(env))); @@ -216,12 +216,12 @@ tree.mixin.Definition.prototype = { return true; }, matchArgs: function (args, env) { - var argsLength = (args && args.length) || 0, len, frame; + var argsLength = (args && args.length) || 0, len; if (! this.variadic) { - if (argsLength < this.required) { return false } - if (argsLength > this.params.length) { return false } - if ((this.required > 0) && (argsLength > this.params.length)) { return false } + if (argsLength < this.required) { return false; } + if (argsLength > this.params.length) { return false; } + if ((this.required > 0) && (argsLength > this.params.length)) { return false; } } len = Math.min(argsLength, this.arity); diff --git a/lib/less/tree/negative.js b/lib/less/tree/negative.js index 5971d70b..12dbcc63 100644 --- a/lib/less/tree/negative.js +++ b/lib/less/tree/negative.js @@ -8,9 +8,11 @@ tree.Negative.prototype = { accept: function (visitor) { this.value = visitor.visit(this.value); }, - toCSS: function (env) { - return '-' + this.value.toCSS(env); + genCSS: function (env, output) { + output.add('-'); + this.value.genCSS(env, output); }, + toCSS: tree.toCSS, eval: function (env) { if (env.isMathOn()) { return (new(tree.Operation)('*', [new(tree.Dimension)(-1), this.value])).eval(env); diff --git a/lib/less/tree/operation.js b/lib/less/tree/operation.js index d9d4af3e..ec806e29 100644 --- a/lib/less/tree/operation.js +++ b/lib/less/tree/operation.js @@ -34,10 +34,18 @@ tree.Operation.prototype = { return new(tree.Operation)(this.op, [a, b], this.isSpaced); } }, - toCSS: function (env) { - var separator = this.isSpaced ? " " : ""; - return this.operands[0].toCSS() + separator + this.op + separator + this.operands[1].toCSS(); - } + genCSS: function (env, output) { + this.operands[0].genCSS(env, output); + if (this.isSpaced) { + output.add(" "); + } + output.add(this.op); + if (this.isSpaced) { + output.add(" "); + } + this.operands[1].genCSS(env, output); + }, + toCSS: tree.toCSS }; tree.operate = function (env, op, a, b) { diff --git a/lib/less/tree/paren.js b/lib/less/tree/paren.js index df6fa95c..e27b8d66 100644 --- a/lib/less/tree/paren.js +++ b/lib/less/tree/paren.js @@ -9,9 +9,12 @@ tree.Paren.prototype = { accept: function (visitor) { this.value = visitor.visit(this.value); }, - toCSS: function (env) { - return '(' + this.value.toCSS(env).trim() + ')'; + genCSS: function (env, output) { + output.add('('); + this.value.genCSS(env, output); + output.add(')'); }, + toCSS: tree.toCSS, eval: function (env) { return new(tree.Paren)(this.value.eval(env)); } diff --git a/lib/less/tree/quoted.js b/lib/less/tree/quoted.js index 9ca4b010..29fc22b0 100644 --- a/lib/less/tree/quoted.js +++ b/lib/less/tree/quoted.js @@ -9,13 +9,16 @@ tree.Quoted = function (str, content, escaped, index, currentFileInfo) { }; tree.Quoted.prototype = { type: "Quoted", - toCSS: function () { - if (this.escaped) { - return this.value; - } else { - return this.quote + this.value + this.quote; + genCSS: function (env, output) { + if (!this.escaped) { + output.add(this.quote, this.currentFileInfo, this.index); + } + output.add(this.value); + if (!this.escaped) { + output.add(this.quote); } }, + toCSS: tree.toCSS, eval: function (env) { var that = this; var value = this.value.replace(/`([^`]+)`/g, function (_, exp) { diff --git a/lib/less/tree/rule.js b/lib/less/tree/rule.js index ea419f9a..20562094 100644 --- a/lib/less/tree/rule.js +++ b/lib/less/tree/rule.js @@ -16,21 +16,19 @@ tree.Rule.prototype = { accept: function (visitor) { this.value = visitor.visit(this.value); }, - toCSS: function (env) { - if (this.variable) { return ""; } - else { - try { - return this.name + (env.compress ? ':' : ': ') + - this.value.toCSS(env) + - this.important + (this.inline ? "" : ";"); - } - catch(e) { - e.index = this.index; - e.filename = this.currentFileInfo.filename; - throw e; - } + genCSS: function (env, output) { + output.add(this.name + (env.compress ? ':' : ': '), this.currentFileInfo, this.index); + try { + this.value.genCSS(env, output); } + catch(e) { + e.index = this.index; + e.filename = this.currentFileInfo.filename; + throw e; + } + output.add(this.important + ((this.inline || (env.lastRule && env.compress)) ? "" : ";"), this.currentFileInfo, this.index); }, + toCSS: tree.toCSS, eval: function (env) { var strictMathBypass = false; if (this.name === "font" && !env.strictMath) { diff --git a/lib/less/tree/ruleset.js b/lib/less/tree/ruleset.js index 9f7f0574..25a92e38 100644 --- a/lib/less/tree/ruleset.js +++ b/lib/less/tree/ruleset.js @@ -9,13 +9,21 @@ tree.Ruleset = function (selectors, rules, strictImports) { tree.Ruleset.prototype = { type: "Ruleset", accept: function (visitor) { - this.selectors = visitor.visit(this.selectors); + if (this.paths) { + for(var i = 0; i < this.paths.length; i++) { + this.paths[i] = visitor.visit(this.paths[i]); + } + } else { + this.selectors = visitor.visit(this.selectors); + } this.rules = visitor.visit(this.rules); }, eval: function (env) { - var selectors = this.selectors && this.selectors.map(function (s) { return s.eval(env) }); + var selectors = this.selectors && this.selectors.map(function (s) { return s.eval(env); }); var ruleset = new(tree.Ruleset)(selectors, this.rules.slice(0), this.strictImports); var rules; + var rule; + var i; ruleset.originalRuleset = this; ruleset.root = this.root; @@ -42,7 +50,7 @@ tree.Ruleset.prototype = { // Store the frames around mixin definitions, // so they can be evaluated like closures when the time comes. - for (var i = 0; i < ruleset.rules.length; i++) { + for (i = 0; i < ruleset.rules.length; i++) { if (ruleset.rules[i] instanceof tree.mixin.Definition) { ruleset.rules[i].frames = env.frames.slice(0); } @@ -51,8 +59,9 @@ tree.Ruleset.prototype = { var mediaBlockCount = (env.mediaBlocks && env.mediaBlocks.length) || 0; // Evaluate mixin calls. - for (var i = 0; i < ruleset.rules.length; i++) { + for (i = 0; i < ruleset.rules.length; i++) { if (ruleset.rules[i] instanceof tree.mixin.Call) { + /*jshint loopfunc:true */ rules = ruleset.rules[i].eval(env).filter(function(r) { if ((r instanceof tree.Rule) && r.variable) { // do not pollute the scope if the variable is @@ -69,7 +78,7 @@ tree.Ruleset.prototype = { } // Evaluate everything else - for (var i = 0, rule; i < ruleset.rules.length; i++) { + for (i = 0; i < ruleset.rules.length; i++) { rule = ruleset.rules[i]; if (! (rule instanceof tree.mixin.Definition)) { @@ -82,7 +91,7 @@ tree.Ruleset.prototype = { env.selectors.shift(); if (env.mediaBlocks) { - for(var i = mediaBlockCount; i < env.mediaBlocks.length; i++) { + for (i = mediaBlockCount; i < env.mediaBlocks.length; i++) { env.mediaBlocks[i].bubbleSelectors(selectors); } } @@ -132,7 +141,7 @@ tree.Ruleset.prototype = { this._lookups = {}; }, variables: function () { - if (this._variables) { return this._variables } + if (this._variables) { return this._variables; } else { return this._variables = this.rules.reduce(function (hash, r) { if (r instanceof tree.Rule && r.variable === true) { @@ -152,10 +161,10 @@ tree.Ruleset.prototype = { }, find: function (selector, self) { self = self || this; - var rules = [], rule, match, + var rules = [], match, key = selector.toCSS(); - if (key in this._lookups) { return this._lookups[key] } + if (key in this._lookups) { return this._lookups[key]; } this.rulesets().forEach(function (rule) { if (rule !== self) { @@ -174,91 +183,99 @@ tree.Ruleset.prototype = { }); return this._lookups[key] = rules; }, - // - // Entry point for code generation - // - // `context` holds an array of arrays. - // - toCSS: function (env) { - var css = [], // The CSS output - rules = [], // node.Rule instances - _rules = [], // - rulesets = [], // node.Ruleset instances - selector, // The fully rendered selector + genCSS: function (env, output) { + var i, j, + ruleNodes = [], + rulesetNodes = [], debugInfo, // Line number debugging - rule; + rule, + firstRuleset = true, + path; - this.mergeRules(); + env.tabLevel = (env.tabLevel || 0); - // Compile rules and rulesets - for (var i = 0; i < this.rules.length; i++) { - rule = this.rules[i]; - - if (rule.rules || (rule instanceof tree.Media) || rule instanceof tree.Directive) { - rulesets.push(rule.toCSS(env)); - } else if (rule instanceof tree.Comment) { - if (this.root) { - rulesets.push(rule.toCSS(env)); - } else { - rules.push(rule.toCSS(env)); - } - } else { - if (rule.toCSS) { - rules.push(rule.toCSS(env)); - } else if (rule.value) { - rules.push(rule.value.toString()); - } - } - } - - // Remove last semicolon - if (env.compress && rules.length) { - rule = rules[rules.length - 1]; - if (rule.charAt(rule.length - 1) === ';') { - rules[rules.length - 1] = rule.substring(0, rule.length - 1); - } + if (!this.root) { + env.tabLevel++; } - rulesets = rulesets.join(''); + var tabRuleStr = env.compress ? '' : Array(env.tabLevel + 1).join(" "), + tabSetStr = env.compress ? '' : Array(env.tabLevel).join(" "); + + for (i = 0; i < this.rules.length; i++) { + rule = this.rules[i]; + if (rule.rules || (rule instanceof tree.Media) || rule instanceof tree.Directive || (this.root && rule instanceof tree.Comment)) { + rulesetNodes.push(rule); + } else { + ruleNodes.push(rule); + } + } // If this is the root node, we don't render // a selector, or {}. - // Otherwise, only output if this ruleset has rules. - if (this.root) { - css.push(rules.join(env.compress ? '' : '\n')); - } else { - if (rules.length > 0) { - debugInfo = tree.debugInfo(env, this); - selector = this.paths - .map(function (p) { - return p.map(function (s) { - return s.toCSS(env); - }).join('').trim(); - }).join(env.compress ? ',' : ',\n'); + if (!this.root) { + debugInfo = tree.debugInfo(env, this, tabSetStr); - if (selector) { - // Remove duplicates - for (var i = rules.length - 1; i >= 0; i--) { - if (rules[i].slice(0, 2) === "/*" || _rules.indexOf(rules[i]) === -1) { - _rules.unshift(rules[i]); - } - } - rules = _rules; + if (debugInfo) { + output.add(debugInfo); + output.add(tabSetStr); + } - css.push(debugInfo + selector + - (env.compress ? '{' : ' {\n ') + - rules.join(env.compress ? '' : '\n ') + - (env.compress ? '}' : '\n}\n')); + for(i = 0; i < this.paths.length; i++) { + path = this.paths[i]; + env.firstSelector = true; + for(j = 0; j < path.length; j++) { + output.add(path[j].genCSS(env, output)); + env.firstSelector = false; + } + if (i + 1 < this.paths.length) { + output.add(env.compress ? ',' : (',\n' + tabSetStr)); } } + + output.add((env.compress ? '{' : ' {\n') + tabRuleStr); } - css.push(rulesets); - return css.join('') + (env.compress ? '\n' : ''); + // Compile rules and rulesets + for (i = 0; i < ruleNodes.length; i++) { + rule = ruleNodes[i]; + + if (i + 1 === ruleNodes.length) { + env.lastRule = true; + } + + if (rule.toCSS) { + output.add(rule.genCSS(env, output)); + } else if (rule.value) { + output.add(rule.value.toString()); + } + + if (!env.lastRule) { + output.add(env.compress ? '' : ('\n' + tabRuleStr)); + } else { + env.lastRule = false; + } + } + + if (!this.root) { + output.add((env.compress ? '}' : '\n' + tabSetStr + '}')); + env.tabLevel--; + } + + for (i = 0; i < rulesetNodes.length; i++) { + if (ruleNodes.length && firstRuleset) { + output.add("\n" + (this.root ? tabRuleStr : tabSetStr)); + } + if (!firstRuleset) { + output.add('\n' + (this.root ? tabRuleStr : tabSetStr)); + } + firstRuleset = false; + output.add(rulesetNodes[i].genCSS(env, output)); + } + + output.add(!env.compress && this.firstRoot ? '\n' : ''); }, - toCSSRoot: function (env) { - }, + toCSS: tree.toCSS, markReferenced: function () { for (var s = 0; s < this.selectors.length; s++) { @@ -289,7 +306,7 @@ tree.Ruleset.prototype = { if (!hasParentSelector) { if (context.length > 0) { - for(i = 0; i < context.length; i++) { + for (i = 0; i < context.length; i++) { paths.push(context[i].concat(selector)); } } @@ -333,22 +350,22 @@ tree.Ruleset.prototype = { } // loop through our current selectors - for(j = 0; j < newSelectors.length; j++) { + for (j = 0; j < newSelectors.length; j++) { sel = newSelectors[j]; // if we don't have any parent paths, the & might be in a mixin so that it can be used // whether there are parents or not - if (context.length == 0) { + if (context.length === 0) { // the combinator used on el should now be applied to the next element instead so that // it is not lost if (sel.length > 0) { sel[0].elements = sel[0].elements.slice(0); - sel[0].elements.push(new(tree.Element)(el.combinator, '', 0)); //new Element(el.Combinator, "")); + sel[0].elements.push(new(tree.Element)(el.combinator, '', 0, el.index, el.currentFileInfo)); } selectorsMultiplied.push(sel); } else { // and the parent selectors - for(k = 0; k < context.length; k++) { + for (k = 0; k < context.length; k++) { parentSel = context[k]; // We need to put the current selectors // then join the last selector's elements on to the parents selectors @@ -380,7 +397,7 @@ tree.Ruleset.prototype = { newJoinedSelectorEmpty = false; // join the elements so far with the first part of the parent - newJoinedSelector.elements.push(new(tree.Element)(el.combinator, parentSel[0].elements[0].value, 0)); + newJoinedSelector.elements.push(new(tree.Element)(el.combinator, parentSel[0].elements[0].value, el.index, el.currentFileInfo)); newJoinedSelector.elements = newJoinedSelector.elements.concat(parentSel[0].elements.slice(1)); } @@ -410,7 +427,7 @@ tree.Ruleset.prototype = { this.mergeElementsOnToSelectors(currentElements, newSelectors); } - for(i = 0; i < newSelectors.length; i++) { + for (i = 0; i < newSelectors.length; i++) { if (newSelectors[i].length > 0) { paths.push(newSelectors[i]); } @@ -418,14 +435,14 @@ tree.Ruleset.prototype = { }, mergeElementsOnToSelectors: function(elements, selectors) { - var i, sel, extendList; + var i, sel; - if (selectors.length == 0) { + if (selectors.length === 0) { selectors.push([ new(tree.Selector)(elements) ]); return; } - for(i = 0; i < selectors.length; i++) { + for (i = 0; i < selectors.length; i++) { sel = selectors[i]; // if the previous thing in sel is a parent this needs to join on to it @@ -436,42 +453,6 @@ tree.Ruleset.prototype = { sel.push(new(tree.Selector)(elements)); } } - }, - - mergeRules: function () { - var groups = {}, - parts, - rule, - key; - - for (var i = 0; i < this.rules.length; i++) { - rule = this.rules[i]; - - if ((rule instanceof tree.Rule) && rule.merge) { - key = [rule.name, - rule.important ? "!" : ""].join(","); - - if (!groups[key]) { - parts = groups[key] = []; - } else { - this.rules.splice(i--, 1); - } - - parts.push(rule); - } - } - - Object.keys(groups).map(function (k) { - parts = groups[k]; - - if (parts.length > 1) { - rule = parts[0]; - - rule.value = new (tree.Value)(parts.map(function (p) { - return p.value; - })); - } - }); } }; })(require('../tree')); diff --git a/lib/less/tree/selector.js b/lib/less/tree/selector.js index 0fc8582d..ac38b993 100644 --- a/lib/less/tree/selector.js +++ b/lib/less/tree/selector.js @@ -18,6 +18,7 @@ tree.Selector.prototype = { this.condition = visitor.visit(this.condition); }, createDerived: function(elements, extendList, evaldCondition) { + /*jshint eqnull:true */ evaldCondition = evaldCondition != null ? evaldCondition : this.evaldCondition; var newSelector = new(tree.Selector)(elements, extendList || this.extendList, this.condition, this.index, this.currentFileInfo, this.isReferenced); newSelector.evaldCondition = evaldCondition; @@ -53,25 +54,20 @@ tree.Selector.prototype = { return extend.eval(env); }), evaldCondition); }, - toCSS: function (env) { - if (this._css) { return this._css } - - if (this.elements[0].combinator.value === "") { - this._css = ' '; - } else { - this._css = ''; + genCSS: function (env, output) { + var i, element; + if ((!env || !env.firstSelector) && this.elements[0].combinator.value === "") { + output.add(' ', this.currentFileInfo, this.index); } - - this._css += this.elements.map(function (e) { - if (typeof(e) === 'string') { - return ' ' + e.trim(); - } else { - return e.toCSS(env); + if (!this._css) { + //TODO caching? speed comparison? + for(i = 0; i < this.elements.length; i++) { + element = this.elements[i]; + element.genCSS(env, output); } - }).join(''); - - return this._css; + } }, + toCSS: tree.toCSS, markReferenced: function () { this.isReferenced = true; }, diff --git a/lib/less/tree/unicode-descriptor.js b/lib/less/tree/unicode-descriptor.js index 3f725127..7bf98ea5 100644 --- a/lib/less/tree/unicode-descriptor.js +++ b/lib/less/tree/unicode-descriptor.js @@ -5,10 +5,11 @@ tree.UnicodeDescriptor = function (value) { }; tree.UnicodeDescriptor.prototype = { type: "UnicodeDescriptor", - toCSS: function (env) { - return this.value; + genCSS: function (env, output) { + output.add(this.value); }, - eval: function () { return this } + toCSS: tree.toCSS, + eval: function () { return this; } }; })(require('../tree')); diff --git a/lib/less/tree/url.js b/lib/less/tree/url.js index 82ef8d7a..7ba562fa 100644 --- a/lib/less/tree/url.js +++ b/lib/less/tree/url.js @@ -9,9 +9,12 @@ tree.URL.prototype = { accept: function (visitor) { this.value = visitor.visit(this.value); }, - toCSS: function () { - return "url(" + this.value.toCSS() + ")"; + genCSS: function (env, output) { + output.add("url("); + this.value.genCSS(env, output); + output.add(")"); }, + toCSS: tree.toCSS, eval: function (ctx) { var val = this.value.eval(ctx), rootpath; diff --git a/lib/less/tree/value.js b/lib/less/tree/value.js index 5aae88eb..7f110f65 100644 --- a/lib/less/tree/value.js +++ b/lib/less/tree/value.js @@ -17,11 +17,16 @@ tree.Value.prototype = { })); } }, - toCSS: function (env) { - return this.value.map(function (e) { - return e.toCSS(env); - }).join(env.compress ? ',' : ', '); - } + genCSS: function (env, output) { + var i; + for(i = 0; i < this.value.length; i++) { + this.value[i].genCSS(env, output); + if (i+1 < this.value.length) { + output.add(env.compress ? ',' : ', '); + } + } + }, + toCSS: tree.toCSS }; })(require('../tree')); diff --git a/lib/less/tree/variable.js b/lib/less/tree/variable.js index ff57494c..8f146932 100644 --- a/lib/less/tree/variable.js +++ b/lib/less/tree/variable.js @@ -1,12 +1,16 @@ (function (tree) { -tree.Variable = function (name, index, currentFileInfo) { this.name = name, this.index = index, this.currentFileInfo = currentFileInfo }; +tree.Variable = function (name, index, currentFileInfo) { + this.name = name; + this.index = index; + this.currentFileInfo = currentFileInfo; +}; tree.Variable.prototype = { type: "Variable", eval: function (env) { var variable, v, name = this.name; - if (name.indexOf('@@') == 0) { + if (name.indexOf('@@') === 0) { name = '@' + new(tree.Variable)(name.slice(1)).eval(env).value; } diff --git a/lib/less/visitor.js b/lib/less/visitor.js index 5a4cd402..52f734f1 100644 --- a/lib/less/visitor.js +++ b/lib/less/visitor.js @@ -35,12 +35,11 @@ return node; }, visitArray: function(nodes) { - var i, newNodes = [], visitor = this; + var i, newNodes = []; for(i = 0; i < nodes.length; i++) { var evald = this.visit(nodes[i]); if (evald instanceof Array) { evald = this.flatten(evald); - //evald.forEach(this.doAccept, this); newNodes = newNodes.concat(evald); } else { newNodes.push(evald); diff --git a/package.json b/package.json index c07e9651..0f3c4c24 100644 --- a/package.json +++ b/package.json @@ -28,16 +28,20 @@ "node": ">=0.4.2" }, "scripts": { + "pretest": "make jshint", "test": "make test" }, "optionalDependencies": { "mime": "1.2.x", "request": ">=2.12.0", "mkdirp": "~0.3.4", - "clean-css": "1.0.x" + "clean-css": "1.0.x", + "source-map": "0.1.x" }, "devDependencies": { - "diff": "~1.0" + "diff": "~1.0", + "jshint": "~2.1.4", + "http-server": "~0.5.5" }, "keywords": [ "compile less", diff --git a/test/browser-test-prepare.js b/test/browser-test-prepare.js index 1a1556f6..95a99703 100644 --- a/test/browser-test-prepare.js +++ b/test/browser-test-prepare.js @@ -1,13 +1,12 @@ var path = require('path'), - fs = require('fs'), - sys = require('util'); + fs = require('fs'); var readDirFilesSync = function(dir, regex, callback) { fs.readdirSync(dir).forEach(function (file) { if (! regex.test(file)) { return; } callback(file); }); -} +}; var createTestRunnerPage = function(dir, exclude, testSuiteName, dir2) { var output = '\n'; @@ -33,7 +32,7 @@ var removeFiles = function(dir, regex) { console.log("Failed to delete " + file); }); }); -} +}; removeFiles("test/browser", /test-runner-[a-zA-Z-]*\.htm$/); createTestRunnerPage("", /javascript|urls/, "main"); diff --git a/test/browser/css/rootpath-relative/urls.css b/test/browser/css/rootpath-relative/urls.css index 817d5818..20b08339 100644 --- a/test/browser/css/rootpath-relative/urls.css +++ b/test/browser/css/rootpath-relative/urls.css @@ -1,5 +1,4 @@ @import "https://www.github.com/cloudhead/imports/modify-this.css"; - @import "https://www.github.com/cloudhead/imports/modify-again.css"; .modify { my-url: url("https://www.github.com/cloudhead/imports/a.png"); diff --git a/test/browser/css/urls.css b/test/browser/css/urls.css index ddb46b15..7001990e 100644 --- a/test/browser/css/urls.css +++ b/test/browser/css/urls.css @@ -1,5 +1,4 @@ @import "http://localhost:8081/browser/less/modify-this.css"; - @import "http://localhost:8081/browser/less/modify-again.css"; .modify { my-url: url("http://localhost:8081/browser/less/a.png"); diff --git a/test/css/comments.css b/test/css/comments.css index 2306c6a8..b85f5b4f 100644 --- a/test/css/comments.css +++ b/test/css/comments.css @@ -32,7 +32,6 @@ color: red; /* A C-style comment */ /* A C-style comment */ - background-color: orange; font-size: 12px; /* lost comment */ @@ -59,7 +58,12 @@ #last { color: #0000ff; } -/* *//* { *//* *//* *//* */#div { +/* */ +/* { */ +/* */ +/* */ +/* */ +#div { color: #A33; } /* } */ diff --git a/test/css/compression/compression.css b/test/css/compression/compression.css index 3fdd6a21..e3582bf3 100644 --- a/test/css/compression/compression.css +++ b/test/css/compression/compression.css @@ -1,5 +1,4 @@ #colours{color1:#fea;color2:#fea;color3:rgba(255,238,170,0.1);string:"#ffeeaa";/*! but not this type Note preserved whitespace - */ -} + */} dimensions{val:.1px;val:0;val:4cm;val:.2;val:5;angles-must-have-unit:0deg;width:auto\9} \ No newline at end of file diff --git a/test/css/extend-selector.css b/test/css/extend-selector.css index dc03d1cd..4a525746 100644 --- a/test/css/extend-selector.css +++ b/test/css/extend-selector.css @@ -52,9 +52,9 @@ div.ext7, .c.replace + .replace .replace, .replace.replace .c, .c.replace + .replace .c, -.rep_ace .rep_ace .rep_ace, +.rep_ace.rep_ace .rep_ace, .c.rep_ace + .rep_ace .rep_ace, -.rep_ace .rep_ace .c, +.rep_ace.rep_ace .c, .c.rep_ace + .rep_ace .c { prop: copy-paste-replace; } diff --git a/test/css/import-inline.css b/test/css/import-inline.css index 1c17ab2a..f198d3c1 100644 --- a/test/css/import-inline.css +++ b/test/css/import-inline.css @@ -1,3 +1,5 @@ -this isn't very valid CSS. @media (min-width: 600px) { +this isn't very valid CSS. +@media (min-width: 600px) { #css { color: yellow; } + } diff --git a/test/css/import.css b/test/css/import.css index 616657ac..a3749181 100644 --- a/test/css/import.css +++ b/test/css/import.css @@ -1,7 +1,5 @@ @import url(http://fonts.googleapis.com/css?family=Open+Sans); - @import url(/absolute/something.css) screen and (color) and (max-width: 600px); - @import url("//ha.com/file.css") (min-width: 100px); #import-test { height: 10px; diff --git a/test/css/media.css b/test/css/media.css index aeba6e77..d90c1e8d 100644 --- a/test/css/media.css +++ b/test/css/media.css @@ -118,7 +118,8 @@ margin: 1cm; } @page :first { - size: 8.5in 11in;@top-left { + size: 8.5in 11in; + @top-left { margin: 1cm; } @top-left-corner { diff --git a/test/css/static-urls/urls.css b/test/css/static-urls/urls.css index b5a690e9..565ccb44 100644 --- a/test/css/static-urls/urls.css +++ b/test/css/static-urls/urls.css @@ -1,5 +1,4 @@ @import "folder (1)/../css/background.css"; - @import "folder (1)/import-test-d.css"; @font-face { src: url("/fonts/garamond-pro.ttf"); diff --git a/test/css/urls.css b/test/css/urls.css index 21b33bca..ef812605 100644 --- a/test/css/urls.css +++ b/test/css/urls.css @@ -1,7 +1,5 @@ @import "import/../css/background.css"; - @import "import/import-test-d.css"; - @import "file.css"; @font-face { src: url("/fonts/garamond-pro.ttf"); diff --git a/test/less-test.js b/test/less-test.js index ae4755db..bc00bc6c 100644 --- a/test/less-test.js +++ b/test/less-test.js @@ -9,6 +9,8 @@ var globals = Object.keys(global); var oneTestOnly = process.argv[2]; +var isVerbose = process.env.npm_config_loglevel === 'verbose'; + var totalTests = 0, failedTests = 0, passedTests = 0; @@ -20,7 +22,7 @@ less.tree.functions.increment = function (a) { return new(less.tree.Dimension)(a.value + 1); }; less.tree.functions._color = function (str) { - if (str.value === "evil red") { return new(less.tree.Color)("600") } + if (str.value === "evil red") { return new(less.tree.Color)("600"); } }; sys.puts("\n" + stylize("LESS", 'underline') + "\n"); @@ -39,6 +41,8 @@ runTestSet({strictMath: true, dumpLineNumbers: 'all'}, "debug/", null, runTestSet({strictMath: true, relativeUrls: false, rootpath: "folder (1)/"}, "static-urls/"); runTestSet({strictMath: true, compress: true}, "compression/"); runTestSet({}, "legacy/"); +runTestSet({strictMath: true, strictUnits: true, sourceMap: true }, "sourcemaps/", + testSourcemap, null, null, function(filename) { return path.join('test/sourcemaps', filename) + '.json'; }); testNoOptions(); @@ -53,6 +57,24 @@ function getErrorPathReplacementFunction(dir) { }; } +function testSourcemap(name, err, compiledLess, doReplacements, sourcemap) { + fs.readFile(path.join('test/', name) + '.json', 'utf8', function (e, expectedSourcemap) { + sys.print("- " + name + ": "); + if (sourcemap === expectedSourcemap) { + ok('OK'); + } else if (err) { + fail("ERROR: " + (err && err.message)); + if (isVerbose) { + console.error(); + console.error(err.stack); + } + } else { + difference("FAIL", expectedSourcemap, sourcemap); + } + sys.puts(""); + }); +} + function testErrors(name, err, compiledLess, doReplacements) { fs.readFile(path.join('test/less/', name) + '.txt', 'utf8', function (e, expectedErr) { sys.print("- " + name + ": "); @@ -94,14 +116,14 @@ function checkGlobalLeaks() { }); } -function runTestSet(options, foldername, verifyFunction, nameModifier, doReplacements) { +function runTestSet(options, foldername, verifyFunction, nameModifier, doReplacements, getFilename) { foldername = foldername || ""; if(!doReplacements) doReplacements = globalReplacements; fs.readdirSync(path.join('test/less/', foldername)).forEach(function (file) { - if (! /\.less/.test(file)) { return } + if (! /\.less/.test(file)) { return; } var name = foldername + path.basename(file, '.less'); @@ -109,20 +131,34 @@ function runTestSet(options, foldername, verifyFunction, nameModifier, doReplace totalTests++; + if (options.sourceMap) { + var sourceMapOutput; + options.writeSourceMap = function(output) { + sourceMapOutput = output; + }; + options.sourceMapOutputFilename = name + ".css"; + options.sourceMapBasepath = path.join(process.cwd(), "test/less"); + options.sourceMapRootpath = "testweb/"; + } + toCSS(options, path.join('test/less/', foldername + file), function (err, less) { if (verifyFunction) { - return verifyFunction(name, err, less, doReplacements); + return verifyFunction(name, err, less, doReplacements, sourceMapOutput); } var css_name = name; - if(nameModifier) css_name=nameModifier(name); + if(nameModifier) { css_name = nameModifier(name); } fs.readFile(path.join('test/css', css_name) + '.css', 'utf8', function (e, css) { - sys.print("- " + css_name + ": ") + sys.print("- " + css_name + ": "); css = css && doReplacements(css, 'test/less/' + foldername); if (less === css) { ok('OK'); } else if (err) { fail("ERROR: " + (err && err.message)); + if (isVerbose) { + console.error(); + console.error(err.stack); + } } else { difference("FAIL", css, less); } @@ -136,7 +172,8 @@ function diff(left, right) { sys.puts(""); require('diff').diffLines(left, right).forEach(function(item) { if(item.added || item.removed) { - sys.print(stylize(item.value, item.added ? 'green' : 'red')); + var text = item.value.replace("\n", String.fromCharCode(182) + "\n"); + sys.print(stylize(text, item.added ? 'green' : 'red')); } else { sys.print(item.value); } @@ -186,10 +223,10 @@ function endTest() { } function toCSS(options, path, callback) { - var tree, css; + var css; options = options || {}; fs.readFile(path, 'utf8', function (e, str) { - if (e) { return callback(e) } + if (e) { return callback(e); } options.paths = [require('path').dirname(path)]; options.filename = require('path').resolve(process.cwd(), path); @@ -214,7 +251,7 @@ function testNoOptions() { totalTests++; try { sys.print("- Integration - creating parser without options: "); - new(less.Parser); + new(less.Parser)(); } catch(e) { fail(stylize("FAIL\n", "red")); return; diff --git a/test/less/import/invalid-css.less b/test/less/import/invalid-css.less index b7cad97b..ed585d63 100644 --- a/test/less/import/invalid-css.less +++ b/test/less/import/invalid-css.less @@ -1 +1 @@ -this isn't very valid CSS. \ No newline at end of file +this isn't very valid CSS. \ No newline at end of file diff --git a/test/less/sourcemaps/basic.less b/test/less/sourcemaps/basic.less new file mode 100644 index 00000000..a3f4bbc5 --- /dev/null +++ b/test/less/sourcemaps/basic.less @@ -0,0 +1,26 @@ +@var: black; + +.a() { + color: red; +} + +.b { + color: green; + .a(); + color: blue; + background: @var; +} + +.a, .b { + background: green; + .c, .d { + background: gray; + & + & { + color: red; + } + } +} + +.extend:extend(.a all) { + color: pink; +} \ No newline at end of file diff --git a/test/sourcemaps/basic.json b/test/sourcemaps/basic.json new file mode 100644 index 00000000..50e568d5 --- /dev/null +++ b/test/sourcemaps/basic.json @@ -0,0 +1 @@ +{"version":3,"file":"sourcemaps/basic.css","sources":["testweb/sourcemaps/basic.less"],"names":[],"mappings":"AAMG;EACD,YAAA;EAJA,UAAA;EAWA,iBAAA;EALA,WAAA;EACA,mBAAA;;AAJC,EASC;AATD,EASM;EACL,gBAAA;;AACE,EAFF,GAEM,KAFN;AAEE,EAFF,GAEM,KAFD;AAEH,EAFG,GAEC,KAFN;AAEE,EAFG,GAEC,KAFD;EAGH,UAAA;;AALJ;AAAK;AAUA;EATL,iBAAA;;AADA,EAEE;AAFG,EAEH;AAFF,EAEO;AAFF,EAEE;AAQF,OARH;AAQG,OARE;EACL,gBAAA;;AACE,EAFF,GAEM,KAFN;AAEE,EAFF,GAEM,KAFN;AAEE,EAFF,GAEM,KAFD;AAEH,EAFF,GAEM,KAFD;AAEH,EAFF,GAEM,KAFN;AAEE,EAFF,GAEM,KAFN;AAEE,EAFF,GAEM,KAFD;AAEH,EAFF,GAEM,KAFD;AAEH,EAFG,GAEC,KAFN;AAEE,EAFG,GAEC,KAFN;AAEE,EAFG,GAEC,KAFD;AAEH,EAFG,GAEC,KAFD;AAEH,EAFG,GAEC,KAFN;AAEE,EAFG,GAEC,KAFN;AAEE,EAFG,GAEC,KAFD;AAEH,EAFG,GAEC,KAFD;AAQF,OARH,GAQG,UARH;AAQG,OARH,GAEM,KAFN;AAQG,OARH,GAQG,UARE;AAQF,OARH,GAEM,KAFD;AAEH,EAFF,GAQG,UARH;AAEE,EAFF,GAQG,UARE;AAQF,OARE,GAQF,UARH;AAQG,OARE,GAEC,KAFN;AAQG,OARE,GAQF,UARE;AAQF,OARE,GAEC,KAFD;AAEH,EAFG,GAQF,UARH;AAEE,EAFG,GAQF,UARE;EAGH,UAAA;;AAKC;EACL,WAAA"} \ No newline at end of file diff --git a/test/sourcemaps/index.html b/test/sourcemaps/index.html new file mode 100644 index 00000000..205bd44f --- /dev/null +++ b/test/sourcemaps/index.html @@ -0,0 +1,16 @@ + + + + + + +
    id import-test
    +
    id import-test
    +
    class mixin
    +
    class a
    +
    class b
    +
    class b
    class c
    +
    class a
    class d
    +
    class extend
    class c
    + + \ No newline at end of file