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..0d5dbf2a --- /dev/null +++ b/.jshintrc @@ -0,0 +1,11 @@ +{ + "evil": true, + "boss": true, + "expr": true, + "laxbreak": true, + "latedef": true, + "node": true, + "undef": true, + "unused": "vars", + "noarg": true +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c66c964..0eb931f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +# 1.5.0 Beta 1 + +2013-09-01 + + - sourcemap support + - support for import inline option to include css that you do NOT want less to parse e.g. `@import (inline) "file.css";` + - better support for modifyVars (refresh styles with new variables, using a file cache), is now more resiliant + - support for import reference option to reference external css, but not output it. Any mixin calls or extend's will be output. + - support for guards on selectors (currently only if you have a single selector) + - Added min/max functions + - fix bad spaces between namespace operators + - do not compress comment if it begins with an exclamation mark + - change to not throw exceptions in toCSS - always return an error object + - allow property merging through the +: syntax + - Fix the saturate function to pass through when using the CSS syntax + - Added svg-gradient function + - Added no-js option to lessc (in browser, use javascriptEnabled: false) which disallows JavaScript in less files + - switched from the little supported and buggy cssmin (previously ycssmin) to clean-css + - Browser: added logLevel option to control logging (2 = everything, 1 = errors only, 0 = no logging) + - Browser: added errorReporting option which can be "html" (default) or "console" or a function + - A few bug fixes for media queries and extends + # 1.4.2 2013-07-20 @@ -22,7 +44,7 @@ - fix passing of strict maths option # 1.4.0 Beta 4 - + 2013-05-04 - change strictMaths to strictMath. Enable this with --strict-math=on in lessc and strictMath:true in JavaScript. @@ -55,7 +77,7 @@ - significant bug fixes to our debug options - other parameters can be used as defaults in mixins e.g. .a(@a, @b:@a) - an error is shown if properties are used outside of a ruleset - - added extract function which picks a value out of a list, e.g. extract(12 13 14, 3) => 3 + - added extract function which picks a value out of a list, e.g. extract(12 13 14, 3) => 14 - added luma, hsvhue, hsvsaturation, hsvvalue functions - added pow, pi, mod, tan, sin, cos, atan, asin, acos and sqrt math functions - added convert function, e.g. convert(1rad, deg) => value in degrees diff --git a/Makefile b/Makefile index b6c74172..ed898a9e 100644 --- a/Makefile +++ b/Makefile @@ -35,15 +35,17 @@ 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\ - ${SRC}/tree/*.js\ ${SRC}/tree.js\ + ${SRC}/tree/*.js\ ${SRC}/env.js\ ${SRC}/visitor.js\ ${SRC}/import-visitor.js\ ${SRC}/join-selector-visitor.js\ + ${SRC}/to-css-visitor.js\ ${SRC}/extend-visitor.js\ ${SRC}/browser.js\ build/amd.js >> ${DIST} @@ -59,15 +61,25 @@ 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\ ${SRC}/import-visitor.js\ ${SRC}/join-selector-visitor.js\ + ${SRC}/to-css-visitor.js\ ${SRC}/extend-visitor.js\ ${SRC}/functions.js\ ${SRC}/colors.js\ diff --git a/bin/lessc b/bin/lessc index ec058215..c830488b 100755 --- a/bin/lessc +++ b/bin/lessc @@ -11,7 +11,7 @@ var args = process.argv.slice(1); var options = { depends: false, compress: false, - yuicompress: false, + cleancss: false, max_line_len: -1, optimization: 1, silent: false, @@ -52,6 +52,8 @@ var checkBooleanArg = function(arg) { return Boolean(onOff[2]); }; +var warningMessages = ""; + args = args.filter(function (arg) { var match; @@ -95,7 +97,11 @@ args = args.filter(function (arg) { options.depends = true; break; case 'yui-compress': - options.yuicompress = true; + warningMessages += "yui-compress option has been removed. assuming clean-css."; + options.cleancss = true; + break; + case 'clean-css': + options.cleancss = true; break; case 'max-line-len': if (checkArgFunc(arg, match[2])) { @@ -111,6 +117,9 @@ args = args.filter(function (arg) { case 'no-ie-compat': options.ieCompat = false; break; + case 'no-js': + options.javascriptEnabled = false; + break; case 'include-path': if (checkArgFunc(arg, match[2])) { options.paths = match[2].split(os.type().match(/Windows/) ? ';' : ':') @@ -129,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])) { @@ -166,7 +190,22 @@ 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) { @@ -199,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); @@ -227,7 +272,14 @@ var parseLessFile = function (e, data) { verbose: options.verbose, ieCompat: options.ieCompat, compress: options.compress, - yuicompress: options.yuicompress, + 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..eaff5bfc --- /dev/null +++ b/build/browser-header.js @@ -0,0 +1,4 @@ +if (typeof(window.less) === 'undefined' || typeof(window.less.nodeType) !== '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/dist/less-1.5.0.js b/dist/less-1.5.0.js new file mode 100644 index 00000000..dcb52cab --- /dev/null +++ b/dist/less-1.5.0.js @@ -0,0 +1,6660 @@ +/* + * LESS - Leaner CSS v1.5.0 + * http://lesscss.org + * + * Copyright (c) 2009-2013, Alexis Sellier + * Licensed under the Apache 2.0 License. + * + * @licence + */ +(function (window, undefined) { +// +// Stub out `require` in the browser +// +function require(arg) { + return window.less[arg.split('/')[1]]; +}; + +if (typeof(window.less) === 'undefined' || typeof(window.less.nodeType) !== 'undefined') { window.less = {}; } +less = window.less; +tree = window.less.tree = {}; +less.mode = 'browser'; +var less, tree; + +// Node.js does not have a header file added which defines less +if (less === undefined) { + less = exports; + tree = require('./tree'); + less.mode = 'node'; +} +// +// less.js - parser +// +// A relatively straight-forward predictive parser. +// There is no tokenization/lexing stage, the input is parsed +// in one sweep. +// +// To make the parser fast enough to run in the browser, several +// optimization had to be made: +// +// - Matching and slicing on a huge input is often cause of slowdowns. +// The solution is to chunkify the input into smaller strings. +// The chunks are stored in the `chunks` var, +// `j` holds the current chunk index, and `current` holds +// the index of the current chunk in relation to `input`. +// This gives us an almost 4x speed-up. +// +// - In many cases, we don't need to match individual tokens; +// for example, if a value doesn't hold any variables, operations +// or dynamic references, the parser can effectively 'skip' it, +// treating it as a literal. +// An example would be '1px solid #000' - which evaluates to itself, +// we don't need to know what the individual components are. +// The drawback, of course is that you don't get the benefits of +// syntax-checking on the CSS. This gives us a 50% speed-up in the parser, +// and a smaller speed-up in the code-gen. +// +// +// Token matching is done with the `$` function, which either takes +// a terminal string or regexp, or a non-terminal function to call. +// It also takes care of moving all the indices forwards. +// +// +less.Parser = function Parser(env) { + var input, // LeSS input string + i, // current index in `input` + j, // current chunk + temp, // temporarily holds a chunk's state, for backtracking + memo, // temporarily holds `i`, when backtracking + furthest, // furthest index the parser has gone to + chunks, // chunkified input + current, // index of current chunk, in `input` + parser; + + // 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)) { + env = new tree.parseEnv(env); + } + + var imports = this.imports = { + paths: env.paths || [], // Search paths, when importing + queue: [], // Files which haven't been imported yet + files: env.files, // Holds the imported parse trees + contents: env.contents, // Holds the imported file contents + mime: env.mime, // MIME type of .less files + error: null, // Error in parsing/evaluating an import + push: function (path, currentFileInfo, importOptions, callback) { + var parserImports = this; + this.queue.push(path); + + var fileParsedFunc = function (e, root, fullPath) { + parserImports.queue.splice(parserImports.queue.indexOf(path), 1); // Remove the path from the queue + + var importedPreviously = fullPath in parserImports.files; + + parserImports.files[fullPath] = root; // Store the root + + if (e && !parserImports.error) { parserImports.error = e; } + + callback(e, root, importedPreviously); + }; + + if (less.Parser.importer) { + less.Parser.importer(path, currentFileInfo, fileParsedFunc, env); + } else { + less.Parser.fileLoader(path, currentFileInfo, function(e, contents, fullPath, newFileInfo) { + if (e) {fileParsedFunc(e); return;} + + var newEnv = new tree.parseEnv(env); + + newEnv.currentFileInfo = newFileInfo; + newEnv.processImports = false; + newEnv.contents[fullPath] = contents; + + if (currentFileInfo.reference || importOptions.reference) { + newFileInfo.reference = true; + } + + if (importOptions.inline) { + fileParsedFunc(null, contents, fullPath); + } else { + new(less.Parser)(newEnv).parse(contents, function (e, root) { + fileParsedFunc(e, root, fullPath); + }); + } + }, env); + } + } + }; + + function save() { temp = chunks[j], memo = i, current = i; } + function restore() { chunks[j] = temp, i = memo, current = i; } + + function sync() { + if (i > current) { + chunks[j] = chunks[j].slice(i - current); + current = i; + } + } + function isWhitespace(c) { + // Could change to \s? + var code = c.charCodeAt(0); + return code === 32 || code === 10 || code === 9; + } + // + // Parse from a token, regexp or string, and move forward if match + // + function $(tok) { + var match, length; + + // + // Non-terminal + // + if (tok instanceof Function) { + return tok.call(parser.parsers); + // + // Terminal + // + // Either match a single character in the input, + // or match a regexp in the current chunk (chunk[j]). + // + } else if (typeof(tok) === 'string') { + match = input.charAt(i) === tok ? tok : null; + length = 1; + sync (); + } else { + sync (); + + if (match = tok.exec(chunks[j])) { + length = match[0].length; + } else { + return null; + } + } + + // The match is confirmed, add the match length to `i`, + // and consume any extra white-space characters (' ' || '\n') + // which come after that. The reason for this is that LeSS's + // grammar is mostly white-space insensitive. + // + if (match) { + skipWhitespace(length); + + if(typeof(match) === 'string') { + return match; + } else { + return match.length === 1 ? match[0] : match; + } + } + } + + function skipWhitespace(length) { + var oldi = i, oldj = j, + endIndex = i + chunks[j].length, + mem = i += length; + + while (i < endIndex) { + 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++; } + + return oldi !== i || oldj !== j; + } + + function expect(arg, msg) { + var result = $(arg); + if (! result) { + error(msg || (typeof(arg) === 'string' ? "expected '" + arg + "' got '" + input.charAt(i) + "'" + : "unexpected token")); + } else { + return result; + } + } + + function error(msg, type) { + var e = new Error(msg); + e.index = i; + e.type = type || 'Syntax'; + throw e; + } + + // Same as $(), but don't change the state of the parser, + // just return the match. + function peek(tok) { + if (typeof(tok) === 'string') { + return input.charAt(i) === tok; + } else { + return tok.test(chunks[j]); + } + } + + function getInput(e, env) { + if (e.filename && env.currentFileInfo.filename && (e.filename !== env.currentFileInfo.filename)) { + return parser.imports.contents[e.filename]; + } else { + return input; + } + } + + function getLocation(index, inputStream) { + var n = index + 1, + line = null, + column = -1; + + 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) { + var filename = env.currentFileInfo.filename; + if(less.mode !== 'browser' && less.mode !== 'rhino') { + filename = require('path').resolve(filename); + } + + return { + lineNumber: getLocation(index, inputStream).line + 1, + fileName: filename + }; + } + + function LessError(e, env) { + var input = getInput(e, 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'; + this.message = e.message; + this.filename = e.filename || env.currentFileInfo.filename; + this.index = e.index; + this.line = typeof(line) === 'number' ? line + 1 : null; + this.callLine = callLine + 1; + this.callExtract = lines[callLine]; + this.stack = e.stack; + this.column = col; + this.extract = [ + lines[line - 1], + lines[line], + lines[line + 1] + ]; + } + + LessError.prototype = new Error(); + LessError.prototype.constructor = LessError; + + this.env = env = env || {}; + + // The optimization level dictates the thoroughness of the parser, + // the lower the number, the less nodes it will create in the tree. + // This could matter for debugging, or if you want to access + // the individual nodes in the tree. + this.optimization = ('optimization' in this.env) ? this.env.optimization : 1; + + // + // The Parser + // + return parser = { + + imports: imports, + // + // Parse an input string into an abstract syntax tree, + // call `callback` when done. + // + parse: function (str, callback) { + var root, line, lines, error = null; + + i = j = current = furthest = 0; + input = str.replace(/\r\n/g, '\n'); + + // 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, + skip = /(?:@\{[\w-]+\}|[^"'`\{\}\/\(\)\\])+/g, + comment = /\/\*(?:[^*]|\*+[^\/*])*\*+\/|\/\/.*/g, + string = /"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'|`((?:[^`]|\\.)*)`/g, + level = 0, + match, + chunk = chunks[0], + inParam; + + for (var i = 0, c, cc; i < input.length;) { + skip.lastIndex = i; + if (match = skip.exec(input)) { + if (match.index === i) { + i += match[0].length; + chunk.push(match[0]); + } + } + c = input.charAt(i); + comment.lastIndex = string.lastIndex = i; + + if (match = string.exec(input)) { + if (match.index === i) { + i += match[0].length; + chunk.push(match[0]); + continue; + } + } + + if (!inParam && c === '/') { + cc = input.charAt(i + 1); + if (cc === '/' || cc === '*') { + if (match = comment.exec(input)) { + if (match.index === i) { + i += match[0].length; + chunk.push(match[0]); + continue; + } + } + } + } + + switch (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) { + error = new(LessError)({ + index: i-1, + type: 'Parse', + message: (level > 0) ? "missing closing `}`" : "missing opening `{`", + filename: env.currentFileInfo.filename + }, env); + } + + return chunks.map(function (c) { return c.join(''); }); + })([[]]); + + if (error) { + return callback(new(LessError)(error, env)); + } + + // Start with the primary rule. + // The whole syntax tree is held under a Ruleset node, + // with the `root` property set to true, so no `{}` are + // output. The callback is called when the input is parsed. + try { + root = new(tree.Ruleset)([], $(this.parsers.primary)); + root.root = true; + root.firstRoot = true; + } catch (e) { + return callback(new(LessError)(e, env)); + } + + root.toCSS = (function (evaluate) { + return function (options, variables) { + options = options || {}; + var evaldRoot, + css, + evalEnv = new tree.evalEnv(options); + + // + // Allows setting variables with a hash, so: + // + // `{ color: new(tree.Color)('#f01') }` will become: + // + // new(tree.Rule)('@color', + // new(tree.Value)([ + // new(tree.Expression)([ + // new(tree.Color)('#f01') + // ]) + // ]) + // ) + // + if (typeof(variables) === 'object' && !Array.isArray(variables)) { + variables = Object.keys(variables).map(function (k) { + var value = variables[k]; + + if (! (value instanceof tree.Value)) { + if (! (value instanceof tree.Expression)) { + value = new(tree.Expression)([value]); + } + value = new(tree.Value)([value]); + } + return new(tree.Rule)('@' + k, value, false, null, 0); + }); + evalEnv.frames = [new(tree.Ruleset)(null, variables)]; + } + + try { + evaldRoot = evaluate.call(this, evalEnv); + + new(tree.joinSelectorVisitor)() + .run(evaldRoot); + + new(tree.processExtendsVisitor)() + .run(evaldRoot); + + new(tree.toCSSVisitor)({compress: Boolean(options.compress)}) + .run(evaldRoot); + + 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)}); + } catch (e) { + throw new(LessError)(e, env); + } + + if (options.cleancss && less.mode === 'node') { + return require('clean-css').process(css); + } else if (options.compress) { + return css.replace(/(^(\s)+)|((\s)+$)/g, ""); + } else { + return css; + } + }; + })(root.eval); + + // If `i` is smaller than the `input.length - 1`, + // it means the parser wasn't able to parse the whole + // string, so we've got a parsing error. + // + // We try to extract a \n delimited string, + // showing the line where the parse error occured. + // We split it up into two parts (the part which parsed, + // 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 = loc.line + 1; + + error = { + type: "Parse", + message: "Unrecognised input", + index: i, + filename: env.currentFileInfo.filename, + line: line, + column: loc.column, + extract: [ + lines[line - 2], + lines[line - 1], + lines[line] + ] + }; + } + + var finish = function (e) { + e = error || e || parser.imports.error; + + if (e) { + if (!(e instanceof LessError)) { + e = new(LessError)(e, env); + } + + callback(e); + } + else { + callback(null, root); + } + }; + + if (env.processImports !== false) { + new tree.importVisitor(this.imports, finish) + .run(root); + } else { + finish(); + } + }, + + // + // Here in, the parsing rules/functions + // + // The basic structure of the syntax tree generated is as follows: + // + // Ruleset -> Rule -> Value -> Expression -> Entity + // + // Here's some LESS code: + // + // .class { + // color: #fff; + // border: 1px solid #000; + // width: @w + 4px; + // > .child {...} + // } + // + // And here's what the parse tree might look like: + // + // Ruleset (Selector '.class', [ + // Rule ("color", Value ([Expression [Color #fff]])) + // Rule ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]])) + // Rule ("width", Value ([Expression [Operation "+" [Variable "@w"][Dimension 4px]]])) + // Ruleset (Selector [Element '>', '.child'], [...]) + // ]) + // + // In general, most rules will try to parse a token with the `$()` function, and if the return + // value is truly, will return a new node, of the relevant type. Sometimes, we need to check + // first, before parsing, that's when we use `peek()`. + // + parsers: { + // + // The `primary` rule is the *entry* and *exit* point of the parser. + // The rules here can appear at any level of the parse tree. + // + // The recursive nature of the grammar is an interplay between the `block` + // rule, which represents `{ ... }`, the `ruleset` rule, and this `primary` rule, + // as represented by this simplified grammar: + // + // primary → (ruleset | rule)+ + // ruleset → selector+ block + // block → '{' primary '}' + // + // Only at one point is the primary rule not called from the + // block rule: at the root level. + // + primary: function () { + var node, root = []; + + while ((node = $(this.extendRule) || $(this.mixin.definition) || $(this.rule) || $(this.ruleset) || + $(this.mixin.call) || $(this.comment) || $(this.directive)) + || $(/^[\s\n]+/) || $(/^;+/)) { + node && root.push(node); + } + return root; + }, + + // We create a Comment node for CSS comments `/* */`, + // but keep the LeSS comments `//` silent, by just skipping + // over them. + comment: function () { + var comment; + + if (input.charAt(i) !== '/') { return; } + + if (input.charAt(i + 1) === '/') { + return new(tree.Comment)($(/^\/\/.*/), true, i, env.currentFileInfo); + } else if (comment = $(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/)) { + return new(tree.Comment)(comment, false, i, env.currentFileInfo); + } + }, + + comments: function () { + var comment, comments = []; + + while(comment = $(this.comment)) { + comments.push(comment); + } + + return comments; + }, + + // + // Entities are tokens which can be found inside an Expression + // + entities: { + // + // A string, which supports escaping " and ' + // + // "milky way" 'he\'s the one!' + // + 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; } + + e && $('~'); + + if (str = $(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/)) { + return new(tree.Quoted)(str[0], str[1] || str[2], e, index, env.currentFileInfo); + } + }, + + // + // A catch-all word, such as: + // + // black border-collapse + // + keyword: function () { + var k; + + if (k = $(/^[_A-Za-z-][_A-Za-z0-9-]*/)) { + if (tree.colors.hasOwnProperty(k)) { + // detect named color + return new(tree.Color)(tree.colors[k].slice(1)); + } else { + return new(tree.Keyword)(k); + } + } + }, + + // + // A function call + // + // rgb(255, 0, 255) + // + // We also try to catch IE's `alpha()`, but let the `alpha` parser + // deal with the details. + // + // The arguments are parsed with the `entities.arguments` parser. + // + call: function () { + var name, nameLC, args, alpha_ret, index = i; + + 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 === 'alpha') { + alpha_ret = $(this.alpha); + if(typeof alpha_ret !== 'undefined') { + return alpha_ret; + } + } + + $('('); // Parse the '(' and consume whitespace. + + args = $(this.entities.arguments); + + if (! $(')')) { + return; + } + + if (name) { return new(tree.Call)(name, args, index, env.currentFileInfo); } + }, + arguments: function () { + var args = [], arg; + + while (arg = $(this.entities.assignment) || $(this.expression)) { + args.push(arg); + if (! $(',')) { + break; + } + } + return args; + }, + literal: function () { + return $(this.entities.dimension) || + $(this.entities.color) || + $(this.entities.quoted) || + $(this.entities.unicodeDescriptor); + }, + + // Assignments are argument entities for calls. + // They are present in ie filter properties as shown below. + // + // filter: progid:DXImageTransform.Microsoft.Alpha( *opacity=50* ) + // + + assignment: function () { + var key, value; + if ((key = $(/^\w+(?=\s?=)/i)) && $('=') && (value = $(this.entity))) { + return new(tree.Assignment)(key, value); + } + }, + + // + // Parse url() tokens + // + // We use a specific rule for urls, because they don't really behave like + // standard function calls. The difference is that the argument doesn't have + // to be enclosed within a string, so it can't be parsed as an Expression. + // + url: function () { + var value; + + 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); + }, + + // + // A Variable entity, such as `@fink`, in + // + // width: @fink + 2px + // + // We use a different parser for variable definitions, + // see `parsers.variable`. + // + variable: function () { + var name, index = i; + + if (input.charAt(i) === '@' && (name = $(/^@@?[\w-]+/))) { + return new(tree.Variable)(name, index, env.currentFileInfo); + } + }, + + // A variable entity useing the protective {} e.g. @{var} + variableCurly: function () { + var curly, index = i; + + if (input.charAt(i) === '@' && (curly = $(/^@\{([\w-]+)\}/))) { + return new(tree.Variable)("@" + curly[1], index, env.currentFileInfo); + } + }, + + // + // A Hexadecimal color + // + // #4F3C2F + // + // `rgb` and `hsl` colors are parsed through the `entities.call` parser. + // + color: function () { + var rgb; + + if (input.charAt(i) === '#' && (rgb = $(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/))) { + return new(tree.Color)(rgb[1]); + } + }, + + // + // A Dimension, that is, a number and a unit + // + // 0.5em 95% + // + 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 (value = $(/^([+-]?\d*\.?\d+)(%|[a-z]+)?/)) { + return new(tree.Dimension)(value[1], value[2]); + } + }, + + // + // A unicode descriptor, as is used in unicode-range + // + // U+0?? or U+00A1-00A9 + // + unicodeDescriptor: function () { + var ud; + + if (ud = $(/^U\+[0-9a-fA-F?]+(\-[0-9a-fA-F?]+)?/)) { + return new(tree.UnicodeDescriptor)(ud[0]); + } + }, + + // + // JavaScript code to be evaluated + // + // `window.location.href` + // + javascript: function () { + var str, j = i, e; + + if (input.charAt(j) === '~') { j++; e = true; } // Escaped strings + if (input.charAt(j) !== '`') { return; } + if (env.javascriptEnabled !== undefined && !env.javascriptEnabled) { + error("You are using JavaScript, which has been disabled."); + } + + if (e) { $('~'); } + + if (str = $(/^`([^`]*)`/)) { + return new(tree.JavaScript)(str[1], i, e); + } + } + }, + + // + // The variable part of a variable definition. Used in the `rule` parser + // + // @fink: + // + variable: function () { + var name; + + if (input.charAt(i) === '@' && (name = $(/^(@[\w-]+)\s*:/))) { return name[1]; } + }, + + // + // extend syntax - used to extend selectors + // + extend: function(isRule) { + var elements, e, index = i, option, extendList = []; + + if (!$(isRule ? /^&:extend\(/ : /^:extend\(/)) { return; } + + do { + option = null; + elements = []; + while (true) { + option = $(/^(all)(?=\s*(\)|,))/); + if (option) { break; } + e = $(this.element); + if (!e) { break; } + elements.push(e); + } + + option = option && option[1]; + + extendList.push(new(tree.Extend)(new(tree.Selector)(elements), option, index)); + + } while($(",")); + + expect(/^\)/); + + if (isRule) { + expect(/^;/); + } + + return extendList; + }, + + // + // extendRule - used in a rule to extend all the parent selectors + // + extendRule: function() { + return this.extend(true); + }, + + // + // Mixins + // + mixin: { + // + // A Mixin call, with an optional argument list + // + // #mixins > .square(#fff); + // .rounded(4px, black); + // .button; + // + // The `while` loop is there because mixins can be + // namespaced, but we only support the child and descendant + // selector for now. + // + call: function () { + var elements = [], e, c, args, index = i, s = input.charAt(i), important = false; + + 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, env.currentFileInfo)); + c = $('>'); + } + if ($('(')) { + args = this.mixin.args.call(this, true).args; + expect(')'); + } + + args = args || []; + + if ($(this.important)) { + important = true; + } + + if (elements.length > 0 && ($(';') || peek('}'))) { + return new(tree.mixin.Call)(elements, args, index, env.currentFileInfo, important); + } + + restore(); + }, + args: function (isCall) { + var expressions = [], argsSemiColon = [], isSemiColonSeperated, argsComma = [], expressionContainsNamed, name, nameLoop, value, arg, + returner = {args:null, variadic: false}; + while (true) { + if (isCall) { + arg = $(this.expression); + } else { + $(this.comments); + if (input.charAt(i) === '.' && $(/^\.{3}/)) { + returner.variadic = true; + if ($(";") && !isSemiColonSeperated) { + isSemiColonSeperated = true; + } + (isSemiColonSeperated ? argsSemiColon : argsComma) + .push({ variadic: true }); + break; + } + arg = $(this.entities.variable) || $(this.entities.literal) + || $(this.entities.keyword); + } + + if (!arg) { + break; + } + + nameLoop = null; + if (arg.throwAwayComments) { + arg.throwAwayComments(); + } + value = arg; + var val = null; + + if (isCall) { + // Variable + if (arg.value.length == 1) { + val = arg.value[0]; + } + } else { + val = arg; + } + + if (val && val instanceof tree.Variable) { + if ($(':')) { + if (expressions.length > 0) { + if (isSemiColonSeperated) { + error("Cannot mix ; and , as delimiter types"); + } + expressionContainsNamed = true; + } + value = expect(this.expression); + nameLoop = (name = val.name); + } else if (!isCall && $(/^\.{3}/)) { + returner.variadic = true; + if ($(";") && !isSemiColonSeperated) { + isSemiColonSeperated = true; + } + (isSemiColonSeperated ? argsSemiColon : argsComma) + .push({ name: arg.name, variadic: true }); + break; + } else if (!isCall) { + name = nameLoop = val.name; + value = null; + } + } + + if (value) { + expressions.push(value); + } + + argsComma.push({ name:nameLoop, value:value }); + + if ($(',')) { + continue; + } + + if ($(';') || isSemiColonSeperated) { + + if (expressionContainsNamed) { + error("Cannot mix ; and , as delimiter types"); + } + + isSemiColonSeperated = true; + + if (expressions.length > 1) { + value = new(tree.Value)(expressions); + } + argsSemiColon.push({ name:name, value:value }); + + name = null; + expressions = []; + expressionContainsNamed = false; + } + } + + returner.args = isSemiColonSeperated ? argsSemiColon : argsComma; + return returner; + }, + // + // A Mixin definition, with a list of parameters + // + // .rounded (@radius: 2px, @color) { + // ... + // } + // + // Until we have a finer grained state-machine, we have to + // do a look-ahead, to make sure we don't have a mixin call. + // See the `rule` function for more information. + // + // We start by matching `.rounded (`, and then proceed on to + // the argument list, which has optional default values. + // We store the parameters in `params`, with a `value` key, + // if there is a value, such as in the case of `@radius`. + // + // Once we've got our params list, and a closing `)`, we parse + // the `{...}` block. + // + definition: function () { + var name, params = [], match, ruleset, cond, variadic = false; + if ((input.charAt(i) !== '.' && input.charAt(i) !== '#') || + peek(/^[^{]*\}/)) { + return; + } + + save(); + + if (match = $(/^([#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/)) { + name = match[1]; + + var argInfo = this.mixin.args.call(this, false); + params = argInfo.args; + variadic = argInfo.variadic; + + // .mixincall("@{a}"); + // looks a bit like a mixin definition.. so we have to be nice and restore + if (!$(')')) { + furthest = i; + restore(); + } + + $(this.comments); + + if ($(/^when/)) { // Guard + cond = expect(this.conditions, 'expected condition'); + } + + ruleset = $(this.block); + + if (ruleset) { + return new(tree.mixin.Definition)(name, params, ruleset, cond, variadic); + } else { + restore(); + } + } + } + }, + + // + // Entities are the smallest recognized token, + // and can be found inside a rule's value. + // + entity: function () { + return $(this.entities.literal) || $(this.entities.variable) || $(this.entities.url) || + $(this.entities.call) || $(this.entities.keyword) ||$(this.entities.javascript) || + $(this.comment); + }, + + // + // A Rule terminator. Note that we use `peek()` to check for '}', + // because the `block` rule will be expecting it, but we still need to make sure + // it's there, if ';' was ommitted. + // + end: function () { + return $(';') || peek('}'); + }, + + // + // IE's alpha function + // + // alpha(opacity=88) + // + alpha: function () { + var value; + + if (! $(/^\(opacity=/i)) { return; } + if (value = $(/^\d+/) || $(this.entities.variable)) { + expect(')'); + return new(tree.Alpha)(value); + } + }, + + // + // A Selector Element + // + // div + // + h1 + // #socks + // input[type="text"] + // + // Elements are the building blocks for Selectors, + // they are made out of a `Combinator` (see combinator rule), + // and an element name, such as a tag a class, or `*`. + // + element: function () { + var e, c, v; + + c = $(this.combinator); + + e = $(/^(?:\d+\.\d+|\d+)%/) || $(/^(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/) || + $('*') || $('&') || $(this.attribute) || $(/^\([^()@]+\)/) || $(/^[\.#](?=@)/) || $(this.entities.variableCurly); + + if (! e) { + if ($('(')) { + if ((v = ($(this.selector))) && + $(')')) { + e = new(tree.Paren)(v); + } + } + } + + if (e) { return new(tree.Element)(c, e, i, env.currentFileInfo); } + }, + + // + // Combinators combine elements together, in a Selector. + // + // Because our parser isn't white-space sensitive, special care + // has to be taken, when parsing the descendant combinator, ` `, + // as it's an empty space. We have to check the previous character + // in the input, to see if it's a ` ` character. More info on how + // we deal with this in *combinator.js*. + // + combinator: function () { + var c = input.charAt(i); + + if (c === '>' || c === '+' || c === '~' || c === '|') { + 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)(" "); + } else { + return new(tree.Combinator)(null); + } + }, + // + // A CSS selector (see selector below) + // with less extensions e.g. the ability to extend and guard + // + lessSelector: function () { + return this.selector(true); + }, + // + // A CSS Selector + // + // .class > div + h1 + // li a:hover + // + // Selectors are made out of one or more Elements, see above. + // + selector: function (isLess) { + var e, elements = [], c, extend, extendList = [], when, condition; + + while ((isLess && (extend = $(this.extend))) || (isLess && (when = $(/^when/))) || (e = $(this.element))) { + if (when) { + condition = expect(this.conditions, 'expected condition'); + } else if (condition) { + error("CSS guard can only be used at the end of selector"); + } else if (extend) { + extendList.push.apply(extendList, extend); + } else { + if (extendList.length) { + error("Extend can only be used at the end of selector"); + } + c = input.charAt(i); + elements.push(e); + e = null; + } + 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 key, val, op; + + if (! $('[')) { return; } + + if (!(key = $(this.entities.variableCurly))) { + key = expect(/^(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\.)+/); + } + + if ((op = $(/^[|~*$^]?=/))) { + val = $(this.entities.quoted) || $(/^[\w-]+/) || $(this.entities.variableCurly); + } + + expect(']'); + + return new(tree.Attribute)(key, op, val); + }, + + // + // The `block` rule is used by `ruleset` and `mixin.definition`. + // It's a wrapper around the `primary` rule, with added `{}`. + // + block: function () { + var content; + if ($('{') && (content = $(this.primary)) && $('}')) { + return content; + } + }, + + // + // div, .class, body > p {...} + // + ruleset: function () { + var selectors = [], s, rules, debugInfo; + + save(); + + if (env.dumpLineNumbers) { + debugInfo = getDebugInfo(i, input, env); + } + + while (s = $(this.lessSelector)) { + selectors.push(s); + $(this.comments); + if (! $(',')) { break; } + if (s.condition) { + error("Guards are only currently allowed on a single selector"); + } + $(this.comments); + } + + if (selectors.length > 0 && (rules = $(this.block))) { + var ruleset = new(tree.Ruleset)(selectors, rules, env.strictImports); + if (env.dumpLineNumbers) { + ruleset.debugInfo = debugInfo; + } + return ruleset; + } else { + // Backtrack + furthest = i; + restore(); + } + }, + rule: function (tryAnonymous) { + var name, value, c = input.charAt(i), important, merge = false; + save(); + + 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 + // but always fallback on the other one + value = !tryAnonymous && (env.compress || (name.charAt(0) === '@')) ? + ($(this.value) || $(this.anonymousValue)) : + ($(this.anonymousValue) || $(this.value)); + + + important = $(this.important); + if (name[name.length-1] === "+") { + merge = true; + name = name.substr(0, name.length - 1); + } + + if (value && $(this.end)) { + return new (tree.Rule)(name, value, important, merge, memo, env.currentFileInfo); + } else { + furthest = i; + restore(); + if (value && !tryAnonymous) { + return this.rule(true); + } + } + } + }, + anonymousValue: function () { + var match; + if (match = /^([^@+\/'"*`(;{}-]*);/.exec(chunks[j])) { + i += match[0].length - 1; + return new(tree.Anonymous)(match[1]); + } + }, + + // + // An @import directive + // + // @import "lib"; + // + // Depending on our environemnt, importing is done differently: + // In the browser, it's an XHR request, in Node, it would be a + // file-system operation. The function used for importing is + // stored in `import`, which we pass to the Import constructor. + // + "import": function () { + var path, features, index = i; + + save(); + + var dir = $(/^@import?\s+/); + + var options = (dir ? $(this.importOptions) : null) || {}; + + if (dir && (path = $(this.entities.quoted) || $(this.entities.url))) { + features = $(this.mediaFeatures); + if ($(';')) { + features = features && new(tree.Value)(features); + return new(tree.Import)(path, features, options, index, env.currentFileInfo); + } + } + + restore(); + }, + + importOptions: function() { + var o, options = {}, optionName, value; + + // list of options, surrounded by parens + if (! $('(')) { return null; } + do { + if (o = $(this.importOption)) { + optionName = o; + value = true; + switch(optionName) { + case "css": + optionName = "less"; + value = false; + break; + case "once": + optionName = "multiple"; + value = false; + break; + } + options[optionName] = value; + if (! $(',')) { break; } + } + } while (o); + expect(')'); + return options; + }, + + importOption: function() { + var opt = $(/^(less|css|multiple|once|inline|reference)/); + if (opt) { + return opt[1]; + } + }, + + mediaFeature: function () { + var e, p, nodes = []; + + do { + if (e = $(this.entities.keyword)) { + nodes.push(e); + } else if ($('(')) { + p = $(this.property); + e = $(this.value); + if ($(')')) { + if (p && e) { + nodes.push(new(tree.Paren)(new(tree.Rule)(p, e, null, null, i, env.currentFileInfo, true))); + } else if (e) { + nodes.push(new(tree.Paren)(e)); + } else { + return null; + } + } else { return null; } + } + } while (e); + + if (nodes.length > 0) { + return new(tree.Expression)(nodes); + } + }, + + mediaFeatures: function () { + var e, features = []; + + do { + if (e = $(this.mediaFeature)) { + features.push(e); + if (! $(',')) { break; } + } else if (e = $(this.entities.variable)) { + features.push(e); + if (! $(',')) { break; } + } + } while (e); + + return features.length > 0 ? features : null; + }, + + media: function () { + var features, rules, media, debugInfo; + + 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) { + media.debugInfo = debugInfo; + } + return media; + } + } + }, + + // + // A CSS Directive + // + // @charset "utf-8"; + // + directive: function () { + var name, value, rules, nonVendorSpecificName, + hasBlock, hasIdentifier, hasExpression; + + if (input.charAt(i) !== '@') { return; } + + if (value = $(this['import']) || $(this.media)) { + return value; + } + + save(); + + name = $(/^@[a-z-]+/); + + if (!name) { return; } + + nonVendorSpecificName = name; + if (name.charAt(1) == '-' && name.indexOf('-', 2) > 0) { + nonVendorSpecificName = "@" + name.slice(name.indexOf('-', 2) + 1); + } + + switch(nonVendorSpecificName) { + case "@font-face": + hasBlock = true; + break; + case "@viewport": + case "@top-left": + case "@top-left-corner": + case "@top-center": + case "@top-right": + case "@top-right-corner": + case "@bottom-left": + case "@bottom-left-corner": + case "@bottom-center": + case "@bottom-right": + case "@bottom-right-corner": + case "@left-top": + case "@left-middle": + case "@left-bottom": + case "@right-top": + case "@right-middle": + case "@right-bottom": + hasBlock = true; + break; + case "@page": + case "@document": + case "@supports": + case "@keyframes": + hasBlock = true; + hasIdentifier = true; + break; + case "@namespace": + hasExpression = true; + break; + } + + if (hasIdentifier) { + name += " " + ($(/^[^{]+/) || '').trim(); + } + + if (hasBlock) + { + if (rules = $(this.block)) { + return new(tree.Directive)(name, rules, i, env.currentFileInfo); + } + } else { + if ((value = hasExpression ? $(this.expression) : $(this.entity)) && $(';')) { + var directive = new(tree.Directive)(name, value, i, env.currentFileInfo); + if (env.dumpLineNumbers) { + directive.debugInfo = getDebugInfo(i, input, env); + } + return directive; + } + } + + restore(); + }, + + // + // A Value is a comma-delimited list of Expressions + // + // font-family: Baskerville, Georgia, serif; + // + // In a Rule, a Value represents everything after the `:`, + // and before the `;`. + // + value: function () { + var e, expressions = []; + + while (e = $(this.expression)) { + expressions.push(e); + if (! $(',')) { break; } + } + + if (expressions.length > 0) { + return new(tree.Value)(expressions); + } + }, + important: function () { + if (input.charAt(i) === '!') { + return $(/^! *important/); + } + }, + sub: function () { + var a, e; + + if ($('(')) { + if (a = $(this.addition)) { + e = new(tree.Expression)([a]); + expect(')'); + e.parens = true; + return e; + } + } + }, + multiplication: function () { + var m, a, op, operation, isSpaced; + if (m = $(this.operand)) { + isSpaced = isWhitespace(input.charAt(i - 1)); + while (!peek(/^\/[*\/]/) && (op = ($('/') || $('*')))) { + if (a = $(this.operand)) { + m.parensInOp = true; + a.parensInOp = true; + operation = new(tree.Operation)(op, [operation || m, a], isSpaced); + isSpaced = isWhitespace(input.charAt(i - 1)); + } else { + break; + } + } + return operation || m; + } + }, + addition: function () { + var m, a, op, operation, isSpaced; + if (m = $(this.multiplication)) { + isSpaced = isWhitespace(input.charAt(i - 1)); + while ((op = $(/^[-+]\s+/) || (!isSpaced && ($('+') || $('-')))) && + (a = $(this.multiplication))) { + m.parensInOp = true; + a.parensInOp = true; + operation = new(tree.Operation)(op, [operation || m, a], isSpaced); + isSpaced = isWhitespace(input.charAt(i - 1)); + } + return operation || m; + } + }, + conditions: function () { + var a, b, index = i, condition; + + if (a = $(this.condition)) { + while ($(',') && (b = $(this.condition))) { + condition = new(tree.Condition)('or', condition || a, b, index); + } + return condition || a; + } + }, + condition: function () { + var a, b, c, op, index = i, negate = false; + + if ($(/^not/)) { negate = true; } + expect('('); + if (a = $(this.addition) || $(this.entities.keyword) || $(this.entities.quoted)) { + if (op = $(/^(?:>=|=<|[<=>])/)) { + if (b = $(this.addition) || $(this.entities.keyword) || $(this.entities.quoted)) { + c = new(tree.Condition)(op, a, b, index, negate); + } else { + error('expected expression'); + } + } else { + c = new(tree.Condition)('=', a, new(tree.Keyword)('true'), index, negate); + } + expect(')'); + return $(/^and/) ? new(tree.Condition)('and', c, $(this.condition)) : c; + } + }, + + // + // An operand is anything that can be part of an operation, + // such as a Color, or a Variable + // + operand: function () { + var negate, p = input.charAt(i + 1); + + if (input.charAt(i) === '-' && (p === '@' || p === '(')) { negate = $('-'); } + var o = $(this.sub) || $(this.entities.dimension) || + $(this.entities.color) || $(this.entities.variable) || + $(this.entities.call); + + if (negate) { + o.parensInOp = true; + o = new(tree.Negative)(o); + } + + return o; + }, + + // + // Expressions either represent mathematical operations, + // or white-space delimited Entities. + // + // 1px solid black + // @var * 2 + // + expression: function () { + var e, delim, entities = []; + + while (e = $(this.addition) || $(this.entity)) { + entities.push(e); + // operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here + if (!peek(/^\/[\/*]/) && (delim = $('/'))) { + entities.push(new(tree.Anonymous)(delim)); + } + } + if (entities.length > 0) { + return new(tree.Expression)(entities); + } + }, + property: function () { + var name; + + if (name = $(/^(\*?-?[_a-zA-Z0-9-]+)\s*:/)) { + return name[1]; + } + }, + ruleProperty: function () { + var name; + + if (name = $(/^(\*?-?[_a-zA-Z0-9-]+)\s*(\+?)\s*:/)) { + return name[1] + (name[2] || ""); + } + } + } + }; +}; + +(function (tree) { + +tree.functions = { + rgb: function (r, g, b) { + return this.rgba(r, g, b, 1.0); + }, + rgba: function (r, g, b, a) { + var rgb = [r, g, b].map(function (c) { return scaled(c, 256); }); + a = number(a); + return new(tree.Color)(rgb, a); + }, + hsl: function (h, s, l) { + return this.hsla(h, s, l, 1.0); + }, + hsla: function (h, s, l, a) { + 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; } + } + + h = (number(h) % 360) / 360; + s = clamp(number(s)); l = clamp(number(l)); a = clamp(number(a)); + + var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s; + var m1 = l * 2 - m2; + + return this.rgba(hue(h + 1/3) * 255, + hue(h) * 255, + hue(h - 1/3) * 255, + a); + }, + + hsv: function(h, s, v) { + return this.hsva(h, s, v, 1.0); + }, + + hsva: function(h, s, v, a) { + h = ((number(h) % 360) / 360) * 360; + s = number(s); v = number(v); a = number(a); + + var i, f; + i = Math.floor((h / 60) % 6); + f = (h / 60) - i; + + var vs = [v, + v * (1 - s), + v * (1 - f * s), + v * (1 - (1 - f) * s)]; + var perm = [[0, 3, 1], + [2, 0, 1], + [1, 0, 3], + [1, 2, 0], + [3, 1, 0], + [0, 1, 2]]; + + return this.rgba(vs[perm[i][0]] * 255, + vs[perm[i][1]] * 255, + vs[perm[i][2]] * 255, + a); + }, + + hue: function (color) { + return new(tree.Dimension)(Math.round(color.toHSL().h)); + }, + saturation: function (color) { + return new(tree.Dimension)(Math.round(color.toHSL().s * 100), '%'); + }, + lightness: function (color) { + return new(tree.Dimension)(Math.round(color.toHSL().l * 100), '%'); + }, + hsvhue: function(color) { + return new(tree.Dimension)(Math.round(color.toHSV().h)); + }, + hsvsaturation: function (color) { + return new(tree.Dimension)(Math.round(color.toHSV().s * 100), '%'); + }, + hsvvalue: function (color) { + return new(tree.Dimension)(Math.round(color.toHSV().v * 100), '%'); + }, + red: function (color) { + return new(tree.Dimension)(color.rgb[0]); + }, + green: function (color) { + return new(tree.Dimension)(color.rgb[1]); + }, + blue: function (color) { + return new(tree.Dimension)(color.rgb[2]); + }, + alpha: function (color) { + return new(tree.Dimension)(color.toHSL().a); + }, + luma: function (color) { + return new(tree.Dimension)(Math.round(color.luma() * color.alpha * 100), '%'); + }, + saturate: function (color, amount) { + // filter: saturate(3.2); + // should be kept as is, so check for color + if (!color.rgb) { + return null; + } + var hsl = color.toHSL(); + + hsl.s += amount.value / 100; + hsl.s = clamp(hsl.s); + return hsla(hsl); + }, + desaturate: function (color, amount) { + var hsl = color.toHSL(); + + hsl.s -= amount.value / 100; + hsl.s = clamp(hsl.s); + return hsla(hsl); + }, + lighten: function (color, amount) { + var hsl = color.toHSL(); + + hsl.l += amount.value / 100; + hsl.l = clamp(hsl.l); + return hsla(hsl); + }, + darken: function (color, amount) { + var hsl = color.toHSL(); + + hsl.l -= amount.value / 100; + hsl.l = clamp(hsl.l); + return hsla(hsl); + }, + fadein: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a += amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + fadeout: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a -= amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + fade: function (color, amount) { + var hsl = color.toHSL(); + + hsl.a = amount.value / 100; + hsl.a = clamp(hsl.a); + return hsla(hsl); + }, + spin: function (color, amount) { + var hsl = color.toHSL(); + var hue = (hsl.h + amount.value) % 360; + + hsl.h = hue < 0 ? 360 + hue : hue; + + return hsla(hsl); + }, + // + // Copyright (c) 2006-2009 Hampton Catlin, Nathan Weizenbaum, and Chris Eppstein + // http://sass-lang.com + // + mix: function (color1, color2, weight) { + if (!weight) { + weight = new(tree.Dimension)(50); + } + var p = weight.value / 100.0; + var w = p * 2 - 1; + var a = color1.toHSL().a - color2.toHSL().a; + + var w1 = (((w * a == -1) ? w : (w + a) / (1 + w * a)) + 1) / 2.0; + var w2 = 1 - w1; + + var rgb = [color1.rgb[0] * w1 + color2.rgb[0] * w2, + color1.rgb[1] * w1 + color2.rgb[1] * w2, + color1.rgb[2] * w1 + color2.rgb[2] * w2]; + + var alpha = color1.alpha * p + color2.alpha * (1 - p); + + return new(tree.Color)(rgb, alpha); + }, + greyscale: function (color) { + return this.desaturate(color, new(tree.Dimension)(100)); + }, + contrast: function (color, dark, light, threshold) { + // filter: contrast(3.2); + // should be kept as is, so check for color + if (!color.rgb) { + return null; + } + if (typeof light === 'undefined') { + light = this.rgba(255, 255, 255, 1.0); + } + if (typeof dark === 'undefined') { + dark = this.rgba(0, 0, 0, 1.0); + } + //Figure out which is actually light and dark! + if (dark.luma() > light.luma()) { + var t = light; + light = dark; + dark = t; + } + if (typeof threshold === 'undefined') { + threshold = 0.43; + } else { + threshold = number(threshold); + } + if ((color.luma() * color.alpha) < threshold) { + return light; + } else { + return dark; + } + }, + e: function (str) { + return new(tree.Anonymous)(str instanceof tree.JavaScript ? str.evaluated : str); + }, + escape: function (str) { + return new(tree.Anonymous)(encodeURI(str.value).replace(/=/g, "%3D").replace(/:/g, "%3A").replace(/#/g, "%23").replace(/;/g, "%3B").replace(/\(/g, "%28").replace(/\)/g, "%29")); + }, + '%': function (quoted /* arg, arg, ...*/) { + var args = Array.prototype.slice.call(arguments, 1), + 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; + }); + } + str = str.replace(/%%/g, '%'); + return new(tree.Quoted)('"' + str + '"', str); + }, + unit: function (val, unit) { + return new(tree.Dimension)(val.value, unit ? unit.toCSS() : ""); + }, + convert: function (val, unit) { + return val.convertTo(unit.value); + }, + round: function (n, f) { + var fraction = typeof(f) === "undefined" ? 0 : f.value; + return this._math(function(num) { return num.toFixed(fraction); }, null, n); + }, + pi: function () { + return new(tree.Dimension)(Math.PI); + }, + mod: function(a, b) { + return new(tree.Dimension)(a.value % b.value, a.unit); + }, + pow: function(x, y) { + if (typeof x === "number" && typeof y === "number") { + x = new(tree.Dimension)(x); + y = new(tree.Dimension)(y); + } else if (!(x instanceof tree.Dimension) || !(y instanceof tree.Dimension)) { + throw { type: "Argument", message: "arguments must be numbers" }; + } + + return new(tree.Dimension)(Math.pow(x.value, y.value), x.unit); + }, + _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); + } else { + throw { type: "Argument", message: "argument must be a number" }; + } + }, + _minmax: function (isMin, args) { + args = Array.prototype.slice.call(args); + switch(args.length) { + case 0: throw { type: "Argument", message: "one or more arguments required" }; + case 1: return args[0]; + } + var i, j, current, currentUnified, referenceUnified, unit, + order = [], // elems only contains original argument values. + values = {}; // key is the unit.toString() for unified tree.Dimension values, + // value is the index into the order array. + for (i = 0; i < args.length; i++) { + current = args[i]; + if (!(current instanceof tree.Dimension)) { + order.push(current); + continue; + } + currentUnified = current.unify(); + unit = currentUnified.unit.toString(); + j = values[unit]; + if (j === undefined) { + values[unit] = order.length; + order.push(current); + continue; + } + referenceUnified = order[j].unify(); + if ( isMin && currentUnified.value < referenceUnified.value || + !isMin && currentUnified.value > referenceUnified.value) { + order[j] = current; + } + } + if (order.length == 1) { + return order[0]; + } + args = order.map(function (a) { return a.toCSS(this.env); }) + .join(this.env.compress ? "," : ", "); + return new(tree.Anonymous)((isMin ? "min" : "max") + "(" + args + ")"); + }, + min: function () { + return this._minmax(true, arguments); + }, + max: function () { + return this._minmax(false, arguments); + }, + argb: function (color) { + return new(tree.Anonymous)(color.toARGB()); + + }, + percentage: function (n) { + return new(tree.Dimension)(n.value * 100, '%'); + }, + color: function (n) { + if (n instanceof tree.Quoted) { + return new(tree.Color)(n.value.slice(1)); + } else { + throw { type: "Argument", message: "argument must be a string" }; + } + }, + iscolor: function (n) { + return this._isa(n, tree.Color); + }, + isnumber: function (n) { + return this._isa(n, tree.Dimension); + }, + isstring: function (n) { + return this._isa(n, tree.Quoted); + }, + iskeyword: function (n) { + return this._isa(n, tree.Keyword); + }, + isurl: function (n) { + return this._isa(n, tree.URL); + }, + ispixel: function (n) { + return this.isunit(n, 'px'); + }, + ispercentage: function (n) { + return this.isunit(n, '%'); + }, + isem: function (n) { + return this.isunit(n, 'em'); + }, + isunit: function (n, unit) { + return (n instanceof tree.Dimension) && n.unit.is(unit.value || unit) ? tree.True : tree.False; + }, + _isa: function (n, Type) { + return (n instanceof Type) ? tree.True : tree.False; + }, + + /* Blending modes */ + + multiply: function(color1, color2) { + var r = color1.rgb[0] * color2.rgb[0] / 255; + var g = color1.rgb[1] * color2.rgb[1] / 255; + var b = color1.rgb[2] * color2.rgb[2] / 255; + return this.rgb(r, g, b); + }, + screen: function(color1, color2) { + var r = 255 - (255 - color1.rgb[0]) * (255 - color2.rgb[0]) / 255; + var g = 255 - (255 - color1.rgb[1]) * (255 - color2.rgb[1]) / 255; + var b = 255 - (255 - color1.rgb[2]) * (255 - color2.rgb[2]) / 255; + return this.rgb(r, g, b); + }, + overlay: function(color1, color2) { + var r = color1.rgb[0] < 128 ? 2 * color1.rgb[0] * color2.rgb[0] / 255 : 255 - 2 * (255 - color1.rgb[0]) * (255 - color2.rgb[0]) / 255; + var g = color1.rgb[1] < 128 ? 2 * color1.rgb[1] * color2.rgb[1] / 255 : 255 - 2 * (255 - color1.rgb[1]) * (255 - color2.rgb[1]) / 255; + var b = color1.rgb[2] < 128 ? 2 * color1.rgb[2] * color2.rgb[2] / 255 : 255 - 2 * (255 - color1.rgb[2]) * (255 - color2.rgb[2]) / 255; + return this.rgb(r, g, b); + }, + softlight: function(color1, color2) { + var t = color2.rgb[0] * color1.rgb[0] / 255; + var r = t + color1.rgb[0] * (255 - (255 - color1.rgb[0]) * (255 - color2.rgb[0]) / 255 - t) / 255; + t = color2.rgb[1] * color1.rgb[1] / 255; + var g = t + color1.rgb[1] * (255 - (255 - color1.rgb[1]) * (255 - color2.rgb[1]) / 255 - t) / 255; + t = color2.rgb[2] * color1.rgb[2] / 255; + var b = t + color1.rgb[2] * (255 - (255 - color1.rgb[2]) * (255 - color2.rgb[2]) / 255 - t) / 255; + return this.rgb(r, g, b); + }, + hardlight: function(color1, color2) { + var r = color2.rgb[0] < 128 ? 2 * color2.rgb[0] * color1.rgb[0] / 255 : 255 - 2 * (255 - color2.rgb[0]) * (255 - color1.rgb[0]) / 255; + var g = color2.rgb[1] < 128 ? 2 * color2.rgb[1] * color1.rgb[1] / 255 : 255 - 2 * (255 - color2.rgb[1]) * (255 - color1.rgb[1]) / 255; + var b = color2.rgb[2] < 128 ? 2 * color2.rgb[2] * color1.rgb[2] / 255 : 255 - 2 * (255 - color2.rgb[2]) * (255 - color1.rgb[2]) / 255; + return this.rgb(r, g, b); + }, + difference: function(color1, color2) { + var r = Math.abs(color1.rgb[0] - color2.rgb[0]); + var g = Math.abs(color1.rgb[1] - color2.rgb[1]); + var b = Math.abs(color1.rgb[2] - color2.rgb[2]); + return this.rgb(r, g, b); + }, + exclusion: function(color1, color2) { + var r = color1.rgb[0] + color2.rgb[0] * (255 - color1.rgb[0] - color1.rgb[0]) / 255; + var g = color1.rgb[1] + color2.rgb[1] * (255 - color1.rgb[1] - color1.rgb[1]) / 255; + var b = color1.rgb[2] + color2.rgb[2] * (255 - color1.rgb[2] - color1.rgb[2]) / 255; + return this.rgb(r, g, b); + }, + average: function(color1, color2) { + var r = (color1.rgb[0] + color2.rgb[0]) / 2; + var g = (color1.rgb[1] + color2.rgb[1]) / 2; + var b = (color1.rgb[2] + color2.rgb[2]) / 2; + return this.rgb(r, g, b); + }, + negation: function(color1, color2) { + var r = 255 - Math.abs(255 - color2.rgb[0] - color1.rgb[0]); + var g = 255 - Math.abs(255 - color2.rgb[1] - color1.rgb[1]); + var b = 255 - Math.abs(255 - color2.rgb[2] - color1.rgb[2]); + return this.rgb(r, g, b); + }, + tint: function(color, amount) { + return this.mix(this.rgb(255,255,255), color, amount); + }, + shade: function(color, amount) { + return this.mix(this.rgb(0, 0, 0), color, amount); + }, + extract: function(values, index) { + index = index.value - 1; // (1-based index) + return values.value[index]; + }, + + "data-uri": function(mimetypeNode, filePathNode) { + + if (typeof window !== 'undefined') { + return new tree.URL(filePathNode || mimetypeNode, this.currentFileInfo).eval(this.env); + } + + var mimetype = mimetypeNode.value; + var filePath = (filePathNode && filePathNode.value); + + var fs = require("fs"), + path = require("path"), + useBase64 = false; + + if (arguments.length < 2) { + filePath = mimetype; + } + + if (this.env.isPathRelative(filePath)) { + if (this.currentFileInfo.relativeUrls) { + filePath = path.join(this.currentFileInfo.currentDirectory, filePath); + } else { + filePath = path.join(this.currentFileInfo.entryPath, filePath); + } + } + + // detect the mimetype if not given + if (arguments.length < 2) { + var mime; + try { + mime = require('mime'); + } catch (ex) { + mime = tree._mime; + } + + mimetype = mime.lookup(filePath); + + // 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'; } + } + else { + useBase64 = /;base64$/.test(mimetype); + } + + var buf = fs.readFileSync(filePath); + + // IE8 cannot handle a data-uri larger than 32KB. If this is exceeded + // and the --ieCompat flag is enabled, return a normal url() instead. + var DATA_URI_MAX_KB = 32, + fileSizeInKB = parseInt((buf.length / 1024), 10); + if (fileSizeInKB >= DATA_URI_MAX_KB) { + + if (this.env.ieCompat !== false) { + if (!this.env.silent) { + console.warn("Skipped data-uri embedding of %s because its size (%dKB) exceeds IE8-safe %dKB!", filePath, fileSizeInKB, DATA_URI_MAX_KB); + } + + return new tree.URL(filePathNode || mimetypeNode, this.currentFileInfo).eval(this.env); + } else if (!this.env.silent) { + // if explicitly disabled (via --no-ie-compat on CLI, or env.ieCompat === false), merely warn + console.warn("WARNING: Embedding %s (%dKB) exceeds IE8's data-uri size limit of %dKB!", filePath, fileSizeInKB, DATA_URI_MAX_KB); + } + } + + buf = useBase64 ? buf.toString('base64') + : encodeURIComponent(buf); + + var uri = "'data:" + mimetype + ',' + buf + "'"; + return new(tree.URL)(new(tree.Anonymous)(uri)); + }, + + "svg-gradient": function(direction) { + + function throwArgumentDescriptor() { + throw { type: "Argument", message: "svg-gradient expects direction, start_color [start_position], [color position,]..., end_color [end_position]" }; + } + + if (arguments.length < 3) { + throwArgumentDescriptor(); + } + var stops = Array.prototype.slice.call(arguments, 1), + gradientDirectionSvg, + gradientType = "linear", + rectangleDimension = 'x="0" y="0" width="1" height="1"', + useBase64 = true, + renderEnv = {compress: false}, + returner, + directionValue = direction.toCSS(renderEnv), + i, color, position, positionValue, alpha; + + switch (directionValue) { + case "to bottom": + gradientDirectionSvg = 'x1="0%" y1="0%" x2="0%" y2="100%"'; + break; + case "to right": + gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="0%"'; + break; + case "to bottom right": + gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="100%"'; + break; + case "to top right": + gradientDirectionSvg = 'x1="0%" y1="100%" x2="100%" y2="0%"'; + break; + case "ellipse": + case "ellipse at center": + gradientType = "radial"; + gradientDirectionSvg = 'cx="50%" cy="50%" r="75%"'; + rectangleDimension = 'x="-50" y="-50" width="101" height="101"'; + 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'" }; + } + returner = '' + + '' + + '<' + gradientType + 'Gradient id="gradient" gradientUnits="userSpaceOnUse" ' + gradientDirectionSvg + '>'; + + for (i = 0; i < stops.length; i+= 1) { + if (stops[i].value) { + color = stops[i].value[0]; + position = stops[i].value[1]; + } else { + color = stops[i]; + position = undefined; + } + + if (!(color instanceof tree.Color) || (!((i === 0 || i+1 === stops.length) && position === undefined) && !(position instanceof tree.Dimension))) { + throwArgumentDescriptor(); + } + positionValue = position ? position.toCSS(renderEnv) : i === 0 ? "0%" : "100%"; + alpha = color.alpha; + returner += ''; + } + returner += '' + + ''; + + if (useBase64) { + // only works in node, needs interface to what is supported in environment + try { + returner = new Buffer(returner).toString('base64'); + } catch(e) { + useBase64 = false; + } + } + + returner = "'data:image/svg+xml" + (useBase64 ? ";base64" : "") + "," + returner + "'"; + return new(tree.URL)(new(tree.Anonymous)(returner)); + } +}; + +// these static methods are used as a fallback when the optional 'mime' dependency is missing +tree._mime = { + // this map is intentionally incomplete + // if you want more, install 'mime' dep + _types: { + '.htm' : 'text/html', + '.html': 'text/html', + '.gif' : 'image/gif', + '.jpg' : 'image/jpeg', + '.jpeg': 'image/jpeg', + '.png' : 'image/png' + }, + lookup: function (filepath) { + var ext = require('path').extname(filepath), + type = tree._mime._types[ext]; + if (type === undefined) { + throw new Error('Optional dependency "mime" is required for ' + ext); + } + return type; + }, + charsets: { + lookup: function (type) { + // assumes all text types are UTF-8 + return type && (/^text\//).test(type) ? 'UTF-8' : ''; + } + } +}; + +var mathFunctions = [{name:"ceil"}, {name:"floor"}, {name: "sqrt"}, {name:"abs"}, + {name:"tan", unit: ""}, {name:"sin", unit: ""}, {name:"cos", unit: ""}, + {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(); + } + return this._math(Math[name], unit, n); + }; + }; + +for(var i = 0; i < mathFunctions.length; i++) { + tree.functions[mathFunctions[i].name] = createMathFunction(mathFunctions[i].name, mathFunctions[i].unit); +} + +function hsla(color) { + return tree.functions.hsla(color.h, color.s, color.l, color.a); +} + +function scaled(n, size) { + if (n instanceof tree.Dimension && n.unit.is('%')) { + return parseFloat(n.value * size / 100); + } else { + return number(n); + } +} + +function number(n) { + if (n instanceof tree.Dimension) { + return parseFloat(n.unit.is('%') ? n.value / 100 : n.value); + } else if (typeof(n) === 'number') { + return n; + } else { + throw { + error: "RuntimeError", + message: "color functions take numbers as parameters" + }; + } +} + +function clamp(val) { + return Math.min(1, Math.max(0, val)); +} + +tree.functionCall = function(env, currentFileInfo) { + this.env = env; + this.currentFileInfo = currentFileInfo; +}; + +tree.functionCall.prototype = tree.functions; + +})(require('./tree')); +(function (tree) { + tree.colors = { + 'aliceblue':'#f0f8ff', + 'antiquewhite':'#faebd7', + 'aqua':'#00ffff', + 'aquamarine':'#7fffd4', + 'azure':'#f0ffff', + 'beige':'#f5f5dc', + 'bisque':'#ffe4c4', + 'black':'#000000', + 'blanchedalmond':'#ffebcd', + 'blue':'#0000ff', + 'blueviolet':'#8a2be2', + 'brown':'#a52a2a', + 'burlywood':'#deb887', + 'cadetblue':'#5f9ea0', + 'chartreuse':'#7fff00', + 'chocolate':'#d2691e', + 'coral':'#ff7f50', + 'cornflowerblue':'#6495ed', + 'cornsilk':'#fff8dc', + 'crimson':'#dc143c', + 'cyan':'#00ffff', + 'darkblue':'#00008b', + 'darkcyan':'#008b8b', + 'darkgoldenrod':'#b8860b', + 'darkgray':'#a9a9a9', + 'darkgrey':'#a9a9a9', + 'darkgreen':'#006400', + 'darkkhaki':'#bdb76b', + 'darkmagenta':'#8b008b', + 'darkolivegreen':'#556b2f', + 'darkorange':'#ff8c00', + 'darkorchid':'#9932cc', + 'darkred':'#8b0000', + 'darksalmon':'#e9967a', + 'darkseagreen':'#8fbc8f', + 'darkslateblue':'#483d8b', + 'darkslategray':'#2f4f4f', + 'darkslategrey':'#2f4f4f', + 'darkturquoise':'#00ced1', + 'darkviolet':'#9400d3', + 'deeppink':'#ff1493', + 'deepskyblue':'#00bfff', + 'dimgray':'#696969', + 'dimgrey':'#696969', + 'dodgerblue':'#1e90ff', + 'firebrick':'#b22222', + 'floralwhite':'#fffaf0', + 'forestgreen':'#228b22', + 'fuchsia':'#ff00ff', + 'gainsboro':'#dcdcdc', + 'ghostwhite':'#f8f8ff', + 'gold':'#ffd700', + 'goldenrod':'#daa520', + 'gray':'#808080', + 'grey':'#808080', + 'green':'#008000', + 'greenyellow':'#adff2f', + 'honeydew':'#f0fff0', + 'hotpink':'#ff69b4', + 'indianred':'#cd5c5c', + 'indigo':'#4b0082', + 'ivory':'#fffff0', + 'khaki':'#f0e68c', + 'lavender':'#e6e6fa', + 'lavenderblush':'#fff0f5', + 'lawngreen':'#7cfc00', + 'lemonchiffon':'#fffacd', + 'lightblue':'#add8e6', + 'lightcoral':'#f08080', + 'lightcyan':'#e0ffff', + 'lightgoldenrodyellow':'#fafad2', + 'lightgray':'#d3d3d3', + 'lightgrey':'#d3d3d3', + 'lightgreen':'#90ee90', + 'lightpink':'#ffb6c1', + 'lightsalmon':'#ffa07a', + 'lightseagreen':'#20b2aa', + 'lightskyblue':'#87cefa', + 'lightslategray':'#778899', + 'lightslategrey':'#778899', + 'lightsteelblue':'#b0c4de', + 'lightyellow':'#ffffe0', + 'lime':'#00ff00', + 'limegreen':'#32cd32', + 'linen':'#faf0e6', + 'magenta':'#ff00ff', + 'maroon':'#800000', + 'mediumaquamarine':'#66cdaa', + 'mediumblue':'#0000cd', + 'mediumorchid':'#ba55d3', + 'mediumpurple':'#9370d8', + 'mediumseagreen':'#3cb371', + 'mediumslateblue':'#7b68ee', + 'mediumspringgreen':'#00fa9a', + 'mediumturquoise':'#48d1cc', + 'mediumvioletred':'#c71585', + 'midnightblue':'#191970', + 'mintcream':'#f5fffa', + 'mistyrose':'#ffe4e1', + 'moccasin':'#ffe4b5', + 'navajowhite':'#ffdead', + 'navy':'#000080', + 'oldlace':'#fdf5e6', + 'olive':'#808000', + 'olivedrab':'#6b8e23', + 'orange':'#ffa500', + 'orangered':'#ff4500', + 'orchid':'#da70d6', + 'palegoldenrod':'#eee8aa', + 'palegreen':'#98fb98', + 'paleturquoise':'#afeeee', + 'palevioletred':'#d87093', + 'papayawhip':'#ffefd5', + 'peachpuff':'#ffdab9', + 'peru':'#cd853f', + 'pink':'#ffc0cb', + 'plum':'#dda0dd', + 'powderblue':'#b0e0e6', + 'purple':'#800080', + 'red':'#ff0000', + 'rosybrown':'#bc8f8f', + 'royalblue':'#4169e1', + 'saddlebrown':'#8b4513', + 'salmon':'#fa8072', + 'sandybrown':'#f4a460', + 'seagreen':'#2e8b57', + 'seashell':'#fff5ee', + 'sienna':'#a0522d', + 'silver':'#c0c0c0', + 'skyblue':'#87ceeb', + 'slateblue':'#6a5acd', + 'slategray':'#708090', + 'slategrey':'#708090', + 'snow':'#fffafa', + 'springgreen':'#00ff7f', + 'steelblue':'#4682b4', + 'tan':'#d2b48c', + 'teal':'#008080', + 'thistle':'#d8bfd8', + 'tomato':'#ff6347', + // 'transparent':'rgba(0,0,0,0)', + 'turquoise':'#40e0d0', + 'violet':'#ee82ee', + 'wheat':'#f5deb3', + 'white':'#ffffff', + 'whitesmoke':'#f5f5f5', + 'yellow':'#ffff00', + 'yellowgreen':'#9acd32' + }; +})(require('./tree')); +(function (tree) { + +tree.debugInfo = function(env, ctx, lineSeperator) { + var result=""; + if (env.dumpLineNumbers && !env.compress) { + switch(env.dumpLineNumbers) { + case 'comments': + result = tree.debugInfo.asComment(ctx); + break; + case 'mediaquery': + result = tree.debugInfo.asMediaQuery(ctx); + break; + case 'all': + result = tree.debugInfo.asComment(ctx) + (lineSeperator || "") + tree.debugInfo.asMediaQuery(ctx); + break; + } + } + return result; +}; + +tree.debugInfo.asComment = function(ctx) { + return '/* line ' + ctx.debugInfo.lineNumber + ', ' + ctx.debugInfo.fileName + ' */\n'; +}; + +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; + }) + + '}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; } + } + 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(', ') + ']'; + } 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')); +(function (tree) { + +tree.Alpha = function (val) { + this.value = val; +}; +tree.Alpha.prototype = { + type: "Alpha", + accept: function (visitor) { + this.value = visitor.visit(this.value); + }, + eval: function (env) { + if (this.value.eval) { return new tree.Alpha(this.value.eval(env)); } + return this; + }, + 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')); +(function (tree) { + +tree.Anonymous = function (string) { + this.value = string.value || string; +}; +tree.Anonymous.prototype = { + type: "Anonymous", + eval: function () { return this; }, + compare: function (x) { + if (!x.toCSS) { + return -1; + } + + var left = this.toCSS(), + right = x.toCSS(); + + if (left === right) { + return 0; + } + + return left < right ? -1 : 1; + }, + genCSS: function (env, output) { + output.add(this.value); + }, + toCSS: tree.toCSS +}; + +})(require('../tree')); +(function (tree) { + +tree.Assignment = function (key, val) { + this.key = key; + this.value = val; +}; +tree.Assignment.prototype = { + type: "Assignment", + accept: function (visitor) { + this.value = visitor.visit(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')); +(function (tree) { + +// +// A function call node. +// +tree.Call = function (name, args, index, currentFileInfo) { + this.name = name; + this.args = args; + this.index = index; + this.currentFileInfo = currentFileInfo; +}; +tree.Call.prototype = { + type: "Call", + accept: function (visitor) { + this.args = visitor.visit(this.args); + }, + // + // When evaluating a function call, + // we either find the function in `tree.functions` [1], + // in which case we call it, passing the evaluated arguments, + // if this returns null or we cannot find the function, we + // simply print it out as it appeared originally [2]. + // + // The *functions.js* file contains the built-in functions. + // + // The reason why we evaluate the arguments, is in the case where + // we try to pass a variable to a function, like: `saturate(@color)`. + // The function should receive the value, not the variable. + // + eval: function (env) { + var args = this.args.map(function (a) { return a.eval(env); }), + nameLC = this.name.toLowerCase(), + result, func; + + if (nameLC in tree.functions) { // 1. + try { + func = new tree.functionCall(env, this.currentFileInfo); + result = func[nameLC].apply(func, args); + /*jshint eqnull:true */ + if (result != null) { + return result; + } + } catch (e) { + throw { type: e.type || "Runtime", + message: "error evaluating function `" + this.name + "`" + + (e.message ? ': ' + e.message : ''), + index: this.index, filename: this.currentFileInfo.filename }; + } + } + + return new tree.Call(this.name, args, this.index, this.currentFileInfo); + }, + + 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')); +(function (tree) { +// +// RGB Colors - #ff0014, #eee +// +tree.Color = function (rgb, a) { + // + // The end goal here, is to parse the arguments + // into an integer triplet, such as `128, 255, 0` + // + // This facilitates operations and conversions. + // + if (Array.isArray(rgb)) { + this.rgb = rgb; + } else if (rgb.length == 6) { + this.rgb = rgb.match(/.{2}/g).map(function (c) { + return parseInt(c, 16); + }); + } else { + this.rgb = rgb.split('').map(function (c) { + return parseInt(c + c, 16); + }); + } + this.alpha = typeof(a) === 'number' ? a : 1; +}; +tree.Color.prototype = { + type: "Color", + 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); }, + + 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); + }).concat(this.alpha).join(',' + (compress ? '' : ' ')) + ")"; + } else { + var color = this.toRGB(); + + if (compress) { + var splitcolor = color.split(''); + + // Convert color to short format + if (splitcolor[1] === splitcolor[2] && splitcolor[3] === splitcolor[4] && splitcolor[5] === splitcolor[6]) { + color = '#' + splitcolor[1] + splitcolor[3] + splitcolor[5]; + } + } + + return color; + } + }, + + // + // Operations have to be done per-channel, if not, + // channels will spill onto each other. Once we have + // our result, in the form of an integer triplet, + // we create a new Color node to hold the result. + // + operate: function (env, op, other) { + var result = []; + + if (! (other instanceof tree.Color)) { + other = other.toColor(); + } + + for (var c = 0; c < 3; c++) { + result[c] = tree.operate(env, op, this.rgb[c], other.rgb[c]); + } + return new(tree.Color)(result, this.alpha + other.alpha); + }, + + toRGB: function () { + return '#' + this.rgb.map(function (i) { + i = Math.round(i); + i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16); + return i.length === 1 ? '0' + i : i; + }).join(''); + }, + + toHSL: function () { + var r = this.rgb[0] / 255, + g = this.rgb[1] / 255, + b = this.rgb[2] / 255, + a = this.alpha; + + var max = Math.max(r, g, b), min = Math.min(r, g, b); + var h, s, l = (max + min) / 2, d = max - min; + + if (max === min) { + h = s = 0; + } else { + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + + switch (max) { + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + return { h: h * 360, s: s, l: l, a: a }; + }, + //Adapted from http://mjijackson.com/2008/02/rgb-to-hsl-and-rgb-to-hsv-color-model-conversion-algorithms-in-javascript + toHSV: function () { + var r = this.rgb[0] / 255, + g = this.rgb[1] / 255, + b = this.rgb[2] / 255, + a = this.alpha; + + var max = Math.max(r, g, b), min = Math.min(r, g, b); + var h, s, v = max; + + var d = max - min; + if (max === 0) { + s = 0; + } else { + s = d / max; + } + + if (max === min) { + h = 0; + } else { + switch(max){ + case r: h = (g - b) / d + (g < b ? 6 : 0); break; + case g: h = (b - r) / d + 2; break; + case b: h = (r - g) / d + 4; break; + } + h /= 6; + } + return { h: h * 360, s: s, v: v, a: a }; + }, + toARGB: function () { + var argb = [Math.round(this.alpha * 255)].concat(this.rgb); + return '#' + argb.map(function (i) { + i = Math.round(i); + i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16); + return i.length === 1 ? '0' + i : i; + }).join(''); + }, + compare: function (x) { + if (!x.rgb) { + return -1; + } + + return (x.rgb[0] === this.rgb[0] && + x.rgb[1] === this.rgb[1] && + x.rgb[2] === this.rgb[2] && + x.alpha === this.alpha) ? 0 : -1; + } +}; + + +})(require('../tree')); +(function (tree) { + +tree.Comment = function (value, silent, index, currentFileInfo) { + this.value = value; + this.silent = !!silent; + this.currentFileInfo = currentFileInfo; +}; +tree.Comment.prototype = { + type: "Comment", + genCSS: function (env, output) { + if (this.debugInfo) { + output.add(tree.debugInfo(env, this), this.currentFileInfo, this.index); + } + 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(/^\/\*!/); + return this.silent || isReference || isCompressed; + }, + eval: function () { return this; }, + markReferenced: function () { + this.isReferenced = true; + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Condition = function (op, l, r, i, negate) { + this.op = op.trim(); + this.lvalue = l; + this.rvalue = r; + this.index = i; + this.negate = negate; +}; +tree.Condition.prototype = { + type: "Condition", + accept: function (visitor) { + this.lvalue = visitor.visit(this.lvalue); + this.rvalue = visitor.visit(this.rvalue); + }, + eval: function (env) { + var a = this.lvalue.eval(env), + b = this.rvalue.eval(env); + + var i = this.index, result; + + result = (function (op) { + switch (op) { + case 'and': + return a && b; + case 'or': + return a || b; + default: + if (a.compare) { + result = a.compare(b); + } else if (b.compare) { + result = b.compare(a); + } else { + throw { type: "Type", + message: "Unable to perform comparison", + index: i }; + } + switch (result) { + case -1: return op === '<' || op === '=<'; + case 0: return op === '=' || op === '>=' || op === '=<'; + case 1: return op === '>' || op === '>='; + } + } + })(this.op); + return this.negate ? !result : result; + } +}; + +})(require('../tree')); +(function (tree) { + +// +// A number with a unit +// +tree.Dimension = function (value, unit) { + this.value = parseFloat(value); + this.unit = (unit && unit instanceof tree.Unit) ? unit : + new(tree.Unit)(unit ? [unit] : undefined); +}; + +tree.Dimension.prototype = { + type: "Dimension", + accept: function (visitor) { + this.unit = visitor.visit(this.unit); + }, + eval: function (env) { + return this; + }, + toColor: function () { + return new(tree.Color)([this.value, this.value, this.value]); + }, + 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()); + } + + var value = this.value, + strValue = String(value); + + if (value !== 0 && value < 0.000001 && value > -0.000001) { + // would be output 1e-6 etc. + strValue = value.toFixed(20).replace(/0+$/, ""); + } + + if (env && env.compress) { + // Zero values doesn't need a unit + if (value === 0 && this.unit.isLength()) { + output.add(strValue); + return; + } + + // Float values doesn't need a leading zero + if (value > 0 && value < 1) { + strValue = (strValue).substr(1); + } + } + + 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(); + + if (op === '+' || op === '-') { + 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) { + // do nothing + } else { + other = other.convertTo(this.unit.usedUnits()); + + if(env.strictUnits && other.unit.toString() !== unit.toString()) { + throw new Error("Incompatible units. Change the units or use the unit function. Bad units: '" + unit.toString() + + "' and '" + other.unit.toString() + "'."); + } + + value = tree.operate(env, op, this.value, other.value); + } + } else if (op === '*') { + unit.numerator = unit.numerator.concat(other.unit.numerator).sort(); + unit.denominator = unit.denominator.concat(other.unit.denominator).sort(); + unit.cancel(); + } else if (op === '/') { + unit.numerator = unit.numerator.concat(other.unit.denominator).sort(); + unit.denominator = unit.denominator.concat(other.unit.numerator).sort(); + unit.cancel(); + } + return new(tree.Dimension)(value, unit); + }, + + compare: function (other) { + if (other instanceof tree.Dimension) { + var a = this.unify(), b = other.unify(), + aValue = a.value, bValue = b.value; + + if (bValue > aValue) { + return -1; + } else if (bValue < aValue) { + return 1; + } else { + if (!b.unit.isEmpty() && a.unit.compare(b.unit) !== 0) { + return -1; + } + return 0; + } + } else { + return -1; + } + }, + + unify: function () { + return this.convertTo({ length: 'm', duration: 's', angle: 'rad' }); + }, + + convertTo: function (conversions) { + 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; + } + 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]); + } + + return targetUnit; + } + + return atomicUnit; + }; + + for (groupName in conversions) { + if (conversions.hasOwnProperty(groupName)) { + targetUnit = conversions[groupName]; + group = tree.UnitConversions[groupName]; + + unit.map(applyUnit); + } + } + + unit.cancel(); + + 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 + } +}; + +tree.Unit = function (numerator, denominator, 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); + }, + 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, + + 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; + }, + + isLength: function () { + return Boolean(this.toCSS().match(/px|em|%|in|cm|mm|pc|pt|ex/)); + }, + + isEmpty: function () { + return this.numerator.length === 0 && this.denominator.length === 0; + }, + + isSingular: function() { + return this.numerator.length <= 1 && this.denominator.length === 0; + }, + + map: function(callback) { + var i; + + for (i = 0; i < this.numerator.length; i++) { + this.numerator[i] = callback(this.numerator[i], false); + } + + for (i = 0; i < this.denominator.length; i++) { + this.denominator[i] = callback(this.denominator[i], true); + } + }, + + usedUnits: function() { + var group, result = {}, mapUnit; + + mapUnit = function (atomicUnit) { + /*jshint loopfunc:true */ + if (group.hasOwnProperty(atomicUnit) && !result[groupName]) { + result[groupName] = atomicUnit; + } + + return atomicUnit; + }; + + for (var groupName in tree.UnitConversions) { + if (tree.UnitConversions.hasOwnProperty(groupName)) { + group = tree.UnitConversions[groupName]; + + this.map(mapUnit); + } + } + + 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; + } + 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')); +(function (tree) { + +tree.Directive = function (name, value, index, currentFileInfo) { + this.name = name; + + if (Array.isArray(value)) { + this.rules = [new(tree.Ruleset)([], value)]; + this.rules[0].allowImports = true; + } else { + this.value = value; + } + this.currentFileInfo = currentFileInfo; + +}; +tree.Directive.prototype = { + type: "Directive", + accept: function (visitor) { + this.rules = visitor.visit(this.rules); + this.value = visitor.visit(this.value); + }, + genCSS: function (env, output) { + output.add(this.name, this.currentFileInfo, this.index); + if (this.rules) { + tree.outputRuleset(env, output, this.rules); + } else { + output.add(' '); + this.value.genCSS(env, output); + output.add(';'); + } + }, + toCSS: tree.toCSS, + eval: function (env) { + var evaldDirective = this; + if (this.rules) { + env.frames.unshift(this); + evaldDirective = new(tree.Directive)(this.name, null, this.index, this.currentFileInfo); + evaldDirective.rules = [this.rules[0].eval(env)]; + evaldDirective.rules[0].root = true; + env.frames.shift(); + } + return evaldDirective; + }, + 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]); }, + markReferenced: function () { + var i, rules; + this.isReferenced = true; + if (this.rules) { + rules = this.rules[0].rules; + for (i = 0; i < rules.length; i++) { + if (rules[i].markReferenced) { + rules[i].markReferenced(); + } + } + } + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Element = function (combinator, value, index, currentFileInfo) { + this.combinator = combinator instanceof tree.Combinator ? + combinator : new(tree.Combinator)(combinator); + + if (typeof(value) === 'string') { + this.value = value.trim(); + } else if (value) { + this.value = value; + } else { + this.value = ""; + } + this.index = index; + this.currentFileInfo = currentFileInfo; +}; +tree.Element.prototype = { + type: "Element", + accept: function (visitor) { + this.combinator = visitor.visit(this.combinator); + this.value = visitor.visit(this.value); + }, + eval: function (env) { + return new(tree.Element)(this.combinator, + this.value.eval ? this.value.eval(env) : this.value, + 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) === '&') { + return ''; + } else { + return this.combinator.toCSS(env || {}) + value; + } + } +}; + +tree.Attribute = function (key, op, value) { + this.key = key; + this.op = op; + this.value = value; +}; +tree.Attribute.prototype = { + type: "Attribute", + accept: function (visitor) { + this.value = visitor.visit(this.value); + }, + eval: function (env) { + 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; + + if (this.op) { + value += this.op; + value += (this.value.toCSS ? this.value.toCSS(env) : this.value); + } + + return '[' + value + ']'; + } +}; + +tree.Combinator = function (value) { + if (value === ' ') { + this.value = ' '; + } else { + this.value = value ? value.trim() : ""; + } +}; +tree.Combinator.prototype = { + type: "Combinator", + _outputMap: { + '' : '', + ' ' : ' ', + ':' : ' :', + '+' : ' + ', + '~' : ' ~ ', + '>' : ' > ', + '|' : '|' + }, + _outputMapCompressed: { + '' : '', + ' ' : ' ', + ':' : ' :', + '+' : '+', + '~' : '~', + '>' : '>', + '|' : '|' + }, + genCSS: function (env, output) { + output.add((env.compress ? this._outputMapCompressed : this._outputMap)[this.value]); + }, + toCSS: tree.toCSS +}; + +})(require('../tree')); +(function (tree) { + +tree.Expression = function (value) { this.value = value; }; +tree.Expression.prototype = { + type: "Expression", + accept: function (visitor) { + this.value = visitor.visit(this.value); + }, + eval: function (env) { + var returnValue, + inParenthesis = this.parens && !this.parensInOp, + doubleParen = false; + if (inParenthesis) { + env.inParenthesis(); + } + if (this.value.length > 1) { + returnValue = new(tree.Expression)(this.value.map(function (e) { + return e.eval(env); + })); + } else if (this.value.length === 1) { + if (this.value[0].parens && !this.value[0].parensInOp) { + doubleParen = true; + } + returnValue = this.value[0].eval(env); + } else { + returnValue = this; + } + if (inParenthesis) { + env.outOfParenthesis(); + } + if (this.parens && this.parensInOp && !(env.isMathOn()) && !doubleParen) { + returnValue = new(tree.Paren)(returnValue); + } + return returnValue; + }, + 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); + }); + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Extend = function Extend(selector, option, index) { + this.selector = selector; + this.option = option; + this.index = index; + + switch(option) { + case "all": + this.allowBefore = true; + this.allowAfter = true; + break; + default: + this.allowBefore = false; + this.allowAfter = false; + break; + } +}; + +tree.Extend.prototype = { + type: "Extend", + accept: function (visitor) { + this.selector = visitor.visit(this.selector); + }, + eval: function (env) { + return new(tree.Extend)(this.selector.eval(env), this.option, this.index); + }, + clone: function (env) { + return new(tree.Extend)(this.selector, this.option, this.index); + }, + findSelfSelectors: function (selectors) { + var selfElements = [], + i; + + for(i = 0; i < selectors.length; i++) { + selfElements = selfElements.concat(selectors[i].elements); + } + + this.selfSelectors = [{ elements: selfElements }]; + } +}; + +})(require('../tree')); +(function (tree) { +// +// CSS @import node +// +// The general strategy here is that we don't want to wait +// for the parsing to be completed, before we start importing +// the file. That's because in the context of a browser, +// most of the time will be spent waiting for the server to respond. +// +// On creation, we push the import path to our import queue, though +// `import,push`, we also pass it a callback, which it'll call once +// the file has been fetched, and parsed. +// +tree.Import = function (path, features, options, index, currentFileInfo) { + this.options = options; + this.index = index; + this.path = path; + this.features = features; + this.currentFileInfo = currentFileInfo; + + if (this.options.less !== undefined || this.options.inline) { + this.css = !this.options.less || this.options.inline; + } else { + var pathValue = this.getPath(); + if (pathValue && /css([\?;].*)?$/.test(pathValue)) { + this.css = true; + } + } +}; + +// +// The actual import node doesn't return anything, when converted to CSS. +// The reason is that it's used at the evaluation stage, so that the rules +// it imports can be treated like any other rules. +// +// In `eval`, we make sure all Import nodes get evaluated, recursively, so +// we end up with a flat structure, which can easily be imported in the parent +// ruleset. +// +tree.Import.prototype = { + type: "Import", + accept: function (visitor) { + this.features = visitor.visit(this.features); + this.path = visitor.visit(this.path); + if (!this.options.inline) { + this.root = visitor.visit(this.root); + } + }, + genCSS: function (env, output) { + if (this.css) { + 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; + return (this.css !== undefined || /(\.[a-z]*$)|([\?;].*)$/.test(path)) ? path : path + '.less'; + } else if (this.path instanceof tree.URL) { + return this.path.value.value; + } + return null; + }, + evalForImport: function (env) { + return new(tree.Import)(this.path.eval(env), this.features, this.options, this.index, this.currentFileInfo); + }, + evalPath: function (env) { + var path = this.path.eval(env); + var rootpath = this.currentFileInfo && this.currentFileInfo.rootpath; + + if (!(path instanceof tree.URL)) { + if (rootpath) { + var pathValue = path.value; + // Add the base path if the import is relative + if (pathValue && env.isPathRelative(pathValue)) { + path.value = rootpath + pathValue; + } + } + path.value = env.normalizePath(path.value); + } + + return path; + }, + eval: function (env) { + var ruleset, features = this.features && this.features.eval(env); + + if (this.skip) { return []; } + + if (this.options.inline) { + var contents = new(tree.Anonymous)(this.root); + return this.features ? new(tree.Media)([contents], this.features.value) : [contents]; + } else if (this.css) { + var newImport = new(tree.Import)(this.evalPath(env), features, this.options, this.index); + if (!newImport.css && this.error) { + throw this.error; + } + return newImport; + } else { + ruleset = new(tree.Ruleset)([], this.root.rules.slice(0)); + + ruleset.evalImports(env); + + return this.features ? new(tree.Media)(ruleset.rules, this.features.value) : ruleset.rules; + } + } +}; + +})(require('../tree')); +(function (tree) { + +tree.JavaScript = function (string, index, escaped) { + this.escaped = escaped; + this.expression = string; + this.index = index; +}; +tree.JavaScript.prototype = { + type: "JavaScript", + eval: function (env) { + var result, + that = this, + context = {}; + + var expression = this.expression.replace(/@\{([\w-]+)\}/g, function (_, name) { + return tree.jsify(new(tree.Variable)('@' + name, that.index).eval(env)); + }); + + try { + expression = new(Function)('return (' + expression + ')'); + } catch (e) { + throw { message: "JavaScript evaluation error: " + e.message + " from `" + expression + "`" , + index: this.index }; + } + + for (var k in env.frames[0].variables()) { + /*jshint loopfunc:true */ + context[k.slice(1)] = { + value: env.frames[0].variables()[k].value, + toJS: function () { + return this.value.eval(env).toCSS(); + } + }; + } + + try { + result = expression.call(context); + } catch (e) { + throw { message: "JavaScript evaluation error: '" + e.name + ': ' + e.message + "'" , + index: this.index }; + } + if (typeof(result) === 'string') { + return new(tree.Quoted)('"' + result + '"', result, this.escaped, this.index); + } else if (Array.isArray(result)) { + return new(tree.Anonymous)(result.join(', ')); + } else { + return new(tree.Anonymous)(result); + } + } +}; + +})(require('../tree')); + +(function (tree) { + +tree.Keyword = function (value) { this.value = value; }; +tree.Keyword.prototype = { + type: "Keyword", + eval: function () { return this; }, + 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; + } else { + return -1; + } + } +}; + +tree.True = new(tree.Keyword)('true'); +tree.False = new(tree.Keyword)('false'); + +})(require('../tree')); +(function (tree) { + +tree.Media = function (value, features, index, currentFileInfo) { + this.index = index; + this.currentFileInfo = currentFileInfo; + + var selectors = this.emptySelectors(); + + this.features = new(tree.Value)(features); + this.rules = [new(tree.Ruleset)(selectors, value)]; + this.rules[0].allowImports = true; +}; +tree.Media.prototype = { + type: "Media", + accept: function (visitor) { + this.features = visitor.visit(this.features); + this.rules = visitor.visit(this.rules); + }, + 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 = []; + env.mediaPath = []; + } + + var media = new(tree.Media)([], [], this.index, this.currentFileInfo); + if(this.debugInfo) { + this.rules[0].debugInfo = this.debugInfo; + media.debugInfo = this.debugInfo; + } + var strictMathBypass = false; + if (!env.strictMath) { + strictMathBypass = true; + env.strictMath = true; + } + try { + media.features = this.features.eval(env); + } + finally { + if (strictMathBypass) { + env.strictMath = false; + } + } + + env.mediaPath.push(media); + env.mediaBlocks.push(media); + + env.frames.unshift(this.rules[0]); + media.rules = [this.rules[0].eval(env)]; + env.frames.shift(); + + env.mediaPath.pop(); + + return env.mediaPath.length === 0 ? media.evalTop(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)('', '&', this.index, this.currentFileInfo); + return [new(tree.Selector)([el], null, null, this.index, this.currentFileInfo)]; + }, + markReferenced: function () { + var i, rules = this.rules[0].rules; + this.isReferenced = true; + for (i = 0; i < rules.length; i++) { + if (rules[i].markReferenced) { + rules[i].markReferenced(); + } + } + }, + + evalTop: function (env) { + var result = this; + + // Render all dependent Media blocks. + if (env.mediaBlocks.length > 1) { + var selectors = this.emptySelectors(); + result = new(tree.Ruleset)(selectors, env.mediaBlocks); + result.multiMedia = true; + } + + delete env.mediaBlocks; + delete env.mediaPath; + + return result; + }, + evalNested: function (env) { + var i, value, + path = env.mediaPath.concat([this]); + + // Extract the media-query conditions separated with `,` (OR). + for (i = 0; i < path.length; i++) { + value = path[i].features instanceof tree.Value ? + path[i].features.value : path[i].features; + path[i] = Array.isArray(value) ? value : [value]; + } + + // Trace all permutations to generate the resulting media-query. + // + // (a, b and c) with nested (d, e) -> + // a and d + // a and e + // b and c and d + // b and c and e + this.features = new(tree.Value)(this.permute(path).map(function (path) { + path = path.map(function (fragment) { + return fragment.toCSS ? fragment : new(tree.Anonymous)(fragment); + }); + + for(i = path.length - 1; i > 0; i--) { + path.splice(i, 0, new(tree.Anonymous)("and")); + } + + return new(tree.Expression)(path); + })); + + // Fake a tree-node that doesn't output anything. + return new(tree.Ruleset)([], []); + }, + permute: function (arr) { + if (arr.length === 0) { + return []; + } else if (arr.length === 1) { + return arr[0]; + } else { + var result = []; + var rest = this.permute(arr.slice(1)); + for (var i = 0; i < rest.length; i++) { + for (var j = 0; j < arr[0].length; j++) { + result.push([arr[0][j]].concat(rest[i])); + } + } + return result; + } + }, + bubbleSelectors: function (selectors) { + this.rules = [new(tree.Ruleset)(selectors.slice(0), [this.rules[0]])]; + } +}; + +})(require('../tree')); +(function (tree) { + +tree.mixin = {}; +tree.mixin.Call = function (elements, args, index, currentFileInfo, important) { + this.selector = new(tree.Selector)(elements); + this.arguments = args; + this.index = index; + this.currentFileInfo = currentFileInfo; + this.important = important; +}; +tree.mixin.Call.prototype = { + type: "MixinCall", + accept: function (visitor) { + this.selector = visitor.visit(this.selector); + this.arguments = visitor.visit(this.arguments); + }, + eval: function (env) { + var mixins, mixin, args, rules = [], match = false, i, m, f, isRecursive, isOneFound, rule; + + args = this.arguments && this.arguments.map(function (a) { + return { name: a.name, value: a.value.eval(env) }; + }); + + for (i = 0; i < env.frames.length; i++) { + if ((mixins = env.frames[i].find(this.selector)).length > 0) { + isOneFound = true; + for (m = 0; m < mixins.length; m++) { + mixin = mixins[m]; + isRecursive = false; + for(f = 0; f < env.frames.length; f++) { + if ((!(mixin instanceof tree.mixin.Definition)) && mixin === (env.frames[f].originalRuleset || env.frames[f])) { + isRecursive = true; + break; + } + } + if (isRecursive) { + continue; + } + if (mixin.matchArgs(args, env)) { + if (!mixin.matchCondition || mixin.matchCondition(args, env)) { + try { + if (!(mixin instanceof tree.mixin.Definition)) { + mixin = new tree.mixin.Definition("", [], mixin.rules, null, false); + } + Array.prototype.push.apply( + rules, mixin.eval(env, args, this.important).rules); + } catch (e) { + throw { message: e.message, index: this.index, filename: this.currentFileInfo.filename, stack: e.stack }; + } + } + match = true; + } + } + if (match) { + if (!this.currentFileInfo || !this.currentFileInfo.reference) { + for (i = 0; i < rules.length; i++) { + rule = rules[i]; + if (rule.markReferenced) { + rule.markReferenced(); + } + } + } + return rules; + } + } + } + if (isOneFound) { + throw { type: 'Runtime', + message: 'No matching definition was found for `' + + this.selector.toCSS().trim() + '(' + + (args ? args.map(function (a) { + var argValue = ""; + if (a.name) { + argValue += a.name + ":"; + } + if (a.value.toCSS) { + argValue += a.value.toCSS(); + } else { + argValue += "???"; + } + return argValue; + }).join(', ') : "") + ")`", + index: this.index, filename: this.currentFileInfo.filename }; + } else { + throw { type: 'Name', + message: this.selector.toCSS().trim() + " is undefined", + index: this.index, filename: this.currentFileInfo.filename }; + } + } +}; + +tree.mixin.Definition = function (name, params, rules, condition, variadic) { + this.name = name; + this.selectors = [new(tree.Selector)([new(tree.Element)(null, name, this.index, this.currentFileInfo)])]; + this.params = params; + this.condition = condition; + this.variadic = variadic; + this.arity = params.length; + 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; } + }, 0); + this.parent = tree.Ruleset.prototype; + this.frames = []; +}; +tree.mixin.Definition.prototype = { + type: "MixinDefinition", + accept: function (visitor) { + this.params = visitor.visit(this.params); + this.rules = visitor.visit(this.rules); + this.condition = visitor.visit(this.condition); + }, + 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), + i, j, val, name, isNamedFound, argIndex; + + mixinEnv = new tree.evalEnv(mixinEnv, [frame].concat(mixinEnv.frames)); + + if (args) { + args = args.slice(0); + + for(i = 0; i < args.length; i++) { + arg = args[i]; + if (name = (arg && arg.name)) { + isNamedFound = false; + for(j = 0; j < params.length; j++) { + if (!evaldArguments[j] && name === params[j].name) { + evaldArguments[j] = arg.value.eval(env); + frame.rules.unshift(new(tree.Rule)(name, arg.value.eval(env))); + isNamedFound = true; + break; + } + } + if (isNamedFound) { + args.splice(i, 1); + i--; + continue; + } else { + throw { type: 'Runtime', message: "Named argument for " + this.name + + ' ' + args[i].name + ' not found' }; + } + } + } + } + argIndex = 0; + for (i = 0; i < params.length; i++) { + if (evaldArguments[i]) { continue; } + + arg = args && args[argIndex]; + + if (name = params[i].name) { + if (params[i].variadic && args) { + varargs = []; + for (j = argIndex; j < args.length; j++) { + varargs.push(args[j].value.eval(env)); + } + frame.rules.unshift(new(tree.Rule)(name, new(tree.Expression)(varargs).eval(env))); + } else { + val = arg && arg.value; + if (val) { + val = val.eval(env); + } else if (params[i].value) { + val = params[i].value.eval(mixinEnv); + frame.resetCache(); + } else { + throw { type: 'Runtime', message: "wrong number of arguments for " + this.name + + ' (' + args.length + ' for ' + this.arity + ')' }; + } + + frame.rules.unshift(new(tree.Rule)(name, val)); + evaldArguments[i] = val; + } + } + + if (params[i].variadic && args) { + for (j = argIndex; j < args.length; j++) { + evaldArguments[j] = args[j].value.eval(env); + } + } + argIndex++; + } + + return frame; + }, + eval: function (env, args, important) { + var _arguments = [], + mixinFrames = this.frames.concat(env.frames), + frame = this.evalParams(env, new(tree.evalEnv)(env, mixinFrames), args, _arguments), + rules, ruleset; + + frame.rules.unshift(new(tree.Rule)('@arguments', new(tree.Expression)(_arguments).eval(env))); + + rules = important ? + this.parent.makeImportant.apply(this).rules : this.rules.slice(0); + + ruleset = new(tree.Ruleset)(null, rules).eval(new(tree.evalEnv)(env, + [this, frame].concat(mixinFrames))); + ruleset.originalRuleset = this; + return ruleset; + }, + matchCondition: function (args, env) { + + if (this.condition && !this.condition.eval( + new(tree.evalEnv)(env, + [this.evalParams(env, new(tree.evalEnv)(env, this.frames.concat(env.frames)), args, [])] + .concat(env.frames)))) { + return false; + } + return true; + }, + matchArgs: function (args, env) { + 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; } + } + + len = Math.min(argsLength, this.arity); + + for (var i = 0; i < len; i++) { + if (!this.params[i].name && !this.params[i].variadic) { + if (args[i].value.eval(env).toCSS() != this.params[i].value.eval(env).toCSS()) { + return false; + } + } + } + return true; + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Negative = function (node) { + this.value = node; +}; +tree.Negative.prototype = { + type: "Negative", + accept: function (visitor) { + this.value = visitor.visit(this.value); + }, + 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); + } + return new(tree.Negative)(this.value.eval(env)); + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Operation = function (op, operands, isSpaced) { + this.op = op.trim(); + this.operands = operands; + this.isSpaced = isSpaced; +}; +tree.Operation.prototype = { + type: "Operation", + accept: function (visitor) { + this.operands = visitor.visit(this.operands); + }, + eval: function (env) { + var a = this.operands[0].eval(env), + b = this.operands[1].eval(env), + temp; + + if (env.isMathOn()) { + if (a instanceof tree.Dimension && b instanceof tree.Color) { + if (this.op === '*' || this.op === '+') { + temp = b, b = a, a = temp; + } else { + throw { type: "Operation", + message: "Can't substract or divide a color from a number" }; + } + } + if (!a.operate) { + throw { type: "Operation", + message: "Operation on an invalid type" }; + } + + return a.operate(env, this.op, b); + } else { + return new(tree.Operation)(this.op, [a, b], this.isSpaced); + } + }, + 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) { + switch (op) { + case '+': return a + b; + case '-': return a - b; + case '*': return a * b; + case '/': return a / b; + } +}; + +})(require('../tree')); + +(function (tree) { + +tree.Paren = function (node) { + this.value = node; +}; +tree.Paren.prototype = { + type: "Paren", + accept: function (visitor) { + this.value = visitor.visit(this.value); + }, + 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)); + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Quoted = function (str, content, escaped, index, currentFileInfo) { + this.escaped = escaped; + this.value = content || ''; + this.quote = str.charAt(0); + this.index = index; + this.currentFileInfo = currentFileInfo; +}; +tree.Quoted.prototype = { + type: "Quoted", + 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) { + return new(tree.JavaScript)(exp, that.index, true).eval(env).value; + }).replace(/@\{([\w-]+)\}/g, function (_, name) { + var v = new(tree.Variable)('@' + name, that.index, that.currentFileInfo).eval(env, true); + return (v instanceof tree.Quoted) ? v.value : v.toCSS(); + }); + return new(tree.Quoted)(this.quote + value + this.quote, value, this.escaped, this.index); + }, + compare: function (x) { + if (!x.toCSS) { + return -1; + } + + var left = this.toCSS(), + right = x.toCSS(); + + if (left === right) { + return 0; + } + + return left < right ? -1 : 1; + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Rule = function (name, value, important, merge, index, currentFileInfo, inline) { + this.name = name; + this.value = (value instanceof tree.Value) ? value : new(tree.Value)([value]); + this.important = important ? ' ' + important.trim() : ''; + this.merge = merge; + this.index = index; + this.currentFileInfo = currentFileInfo; + this.inline = inline || false; + this.variable = (name.charAt(0) === '@'); +}; + +tree.Rule.prototype = { + type: "Rule", + accept: function (visitor) { + this.value = visitor.visit(this.value); + }, + 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) { + strictMathBypass = true; + env.strictMath = true; + } + try { + return new(tree.Rule)(this.name, + this.value.eval(env), + this.important, + this.merge, + this.index, this.currentFileInfo, this.inline); + } + finally { + if (strictMathBypass) { + env.strictMath = false; + } + } + }, + makeImportant: function () { + return new(tree.Rule)(this.name, + this.value, + "!important", + this.merge, + this.index, this.currentFileInfo, this.inline); + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Ruleset = function (selectors, rules, strictImports) { + this.selectors = selectors; + this.rules = rules; + this._lookups = {}; + this.strictImports = strictImports; +}; +tree.Ruleset.prototype = { + type: "Ruleset", + accept: function (visitor) { + 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 ruleset = new(tree.Ruleset)(selectors, this.rules.slice(0), this.strictImports); + var rules; + var rule; + var i; + + ruleset.originalRuleset = this; + ruleset.root = this.root; + ruleset.firstRoot = this.firstRoot; + ruleset.allowImports = this.allowImports; + + if(this.debugInfo) { + ruleset.debugInfo = this.debugInfo; + } + + // push the current ruleset to the frames stack + env.frames.unshift(ruleset); + + // currrent selectors + if (!env.selectors) { + env.selectors = []; + } + env.selectors.unshift(this.selectors); + + // Evaluate imports + if (ruleset.root || ruleset.allowImports || !ruleset.strictImports) { + ruleset.evalImports(env); + } + + // Store the frames around mixin definitions, + // so they can be evaluated like closures when the time comes. + for (i = 0; i < ruleset.rules.length; i++) { + if (ruleset.rules[i] instanceof tree.mixin.Definition) { + ruleset.rules[i].frames = env.frames.slice(0); + } + } + + var mediaBlockCount = (env.mediaBlocks && env.mediaBlocks.length) || 0; + + // Evaluate mixin calls. + 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 + // already there. consider returning false here + // but we need a way to "return" variable from mixins + return !(ruleset.variable(r.name)); + } + return true; + }); + ruleset.rules.splice.apply(ruleset.rules, [i, 1].concat(rules)); + i += rules.length-1; + ruleset.resetCache(); + } + } + + // Evaluate everything else + for (i = 0; i < ruleset.rules.length; i++) { + rule = ruleset.rules[i]; + + if (! (rule instanceof tree.mixin.Definition)) { + ruleset.rules[i] = rule.eval ? rule.eval(env) : rule; + } + } + + // Pop the stack + env.frames.shift(); + env.selectors.shift(); + + if (env.mediaBlocks) { + for (i = mediaBlockCount; i < env.mediaBlocks.length; i++) { + env.mediaBlocks[i].bubbleSelectors(selectors); + } + } + + return ruleset; + }, + evalImports: function(env) { + var i, rules; + for (i = 0; i < this.rules.length; i++) { + if (this.rules[i] instanceof tree.Import) { + rules = this.rules[i].eval(env); + if (typeof rules.length === "number") { + this.rules.splice.apply(this.rules, [i, 1].concat(rules)); + i+= rules.length-1; + } else { + this.rules.splice(i, 1, rules); + } + this.resetCache(); + } + } + }, + makeImportant: function() { + return new tree.Ruleset(this.selectors, this.rules.map(function (r) { + if (r.makeImportant) { + return r.makeImportant(); + } else { + return r; + } + }), this.strictImports); + }, + matchArgs: function (args) { + return !args || args.length === 0; + }, + matchCondition: function (args, env) { + var lastSelector = this.selectors[this.selectors.length-1]; + if (lastSelector.condition && + !lastSelector.condition.eval( + new(tree.evalEnv)(env, + env.frames))) { + return false; + } + return true; + }, + resetCache: function () { + this._rulesets = null; + this._variables = null; + this._lookups = {}; + }, + variables: function () { + if (this._variables) { return this._variables; } + else { + return this._variables = this.rules.reduce(function (hash, r) { + if (r instanceof tree.Rule && r.variable === true) { + hash[r.name] = r; + } + return hash; + }, {}); + } + }, + variable: function (name) { + return this.variables()[name]; + }, + rulesets: function () { + return this.rules.filter(function (r) { + return (r instanceof tree.Ruleset) || (r instanceof tree.mixin.Definition); + }); + }, + find: function (selector, self) { + self = self || this; + var rules = [], match, + key = selector.toCSS(); + + if (key in this._lookups) { return this._lookups[key]; } + + this.rulesets().forEach(function (rule) { + if (rule !== self) { + for (var j = 0; j < rule.selectors.length; j++) { + if (match = selector.match(rule.selectors[j])) { + if (selector.elements.length > rule.selectors[j].elements.length) { + Array.prototype.push.apply(rules, rule.find( + new(tree.Selector)(selector.elements.slice(1)), self)); + } else { + rules.push(rule); + } + break; + } + } + } + }); + return this._lookups[key] = rules; + }, + genCSS: function (env, output) { + var i, j, + ruleNodes = [], + rulesetNodes = [], + debugInfo, // Line number debugging + rule, + firstRuleset = true, + path; + + env.tabLevel = (env.tabLevel || 0); + + if (!this.root) { + env.tabLevel++; + } + + 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 {}. + if (!this.root) { + debugInfo = tree.debugInfo(env, this, tabSetStr); + + if (debugInfo) { + output.add(debugInfo); + output.add(tabSetStr); + } + + 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); + } + + // 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' : ''); + }, + + toCSS: tree.toCSS, + + markReferenced: function () { + for (var s = 0; s < this.selectors.length; s++) { + this.selectors[s].markReferenced(); + } + }, + + joinSelectors: function (paths, context, selectors) { + for (var s = 0; s < selectors.length; s++) { + this.joinSelector(paths, context, selectors[s]); + } + }, + + joinSelector: function (paths, context, selector) { + + var i, j, k, + hasParentSelector, newSelectors, el, sel, parentSel, + newSelectorPath, afterParentJoin, newJoinedSelector, + newJoinedSelectorEmpty, lastSelector, currentElements, + selectorsMultiplied; + + for (i = 0; i < selector.elements.length; i++) { + el = selector.elements[i]; + if (el.value === '&') { + hasParentSelector = true; + } + } + + if (!hasParentSelector) { + if (context.length > 0) { + for (i = 0; i < context.length; i++) { + paths.push(context[i].concat(selector)); + } + } + else { + paths.push([selector]); + } + return; + } + + // The paths are [[Selector]] + // The first list is a list of comma seperated selectors + // The inner list is a list of inheritance seperated selectors + // e.g. + // .a, .b { + // .c { + // } + // } + // == [[.a] [.c]] [[.b] [.c]] + // + + // the elements from the current selector so far + currentElements = []; + // the current list of new selectors to add to the path. + // We will build it up. We initiate it with one empty selector as we "multiply" the new selectors + // by the parents + newSelectors = [[]]; + + for (i = 0; i < selector.elements.length; i++) { + el = selector.elements[i]; + // non parent reference elements just get added + if (el.value !== "&") { + currentElements.push(el); + } else { + // the new list of selectors to add + selectorsMultiplied = []; + + // merge the current list of non parent selector elements + // on to the current list of selectors to add + if (currentElements.length > 0) { + this.mergeElementsOnToSelectors(currentElements, newSelectors); + } + + // loop through our current selectors + 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) { + // 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, el.index, el.currentFileInfo)); + } + selectorsMultiplied.push(sel); + } + else { + // and the parent selectors + 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 + + // our new selector path + newSelectorPath = []; + // selectors from the parent after the join + afterParentJoin = []; + newJoinedSelectorEmpty = true; + + //construct the joined selector - if & is the first thing this will be empty, + // if not newJoinedSelector will be the last set of elements in the selector + if (sel.length > 0) { + newSelectorPath = sel.slice(0); + lastSelector = newSelectorPath.pop(); + newJoinedSelector = selector.createDerived(lastSelector.elements.slice(0)); + newJoinedSelectorEmpty = false; + } + else { + newJoinedSelector = selector.createDerived([]); + } + + //put together the parent selectors after the join + if (parentSel.length > 1) { + afterParentJoin = afterParentJoin.concat(parentSel.slice(1)); + } + + if (parentSel.length > 0) { + 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, el.index, el.currentFileInfo)); + newJoinedSelector.elements = newJoinedSelector.elements.concat(parentSel[0].elements.slice(1)); + } + + if (!newJoinedSelectorEmpty) { + // now add the joined selector + newSelectorPath.push(newJoinedSelector); + } + + // and the rest of the parent + newSelectorPath = newSelectorPath.concat(afterParentJoin); + + // add that to our new set of selectors + selectorsMultiplied.push(newSelectorPath); + } + } + } + + // our new selectors has been multiplied, so reset the state + newSelectors = selectorsMultiplied; + currentElements = []; + } + } + + // if we have any elements left over (e.g. .a& .b == .b) + // add them on to all the current selectors + if (currentElements.length > 0) { + this.mergeElementsOnToSelectors(currentElements, newSelectors); + } + + for (i = 0; i < newSelectors.length; i++) { + if (newSelectors[i].length > 0) { + paths.push(newSelectors[i]); + } + } + }, + + mergeElementsOnToSelectors: function(elements, selectors) { + var i, sel; + + if (selectors.length === 0) { + selectors.push([ new(tree.Selector)(elements) ]); + return; + } + + 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 + if (sel.length > 0) { + sel[sel.length - 1] = sel[sel.length - 1].createDerived(sel[sel.length - 1].elements.concat(elements)); + } + else { + sel.push(new(tree.Selector)(elements)); + } + } + } +}; +})(require('../tree')); +(function (tree) { + +tree.Selector = function (elements, extendList, condition, index, currentFileInfo, isReferenced) { + this.elements = elements; + this.extendList = extendList || []; + this.condition = condition; + this.currentFileInfo = currentFileInfo || {}; + this.isReferenced = isReferenced; + if (!condition) { + this.evaldCondition = true; + } +}; +tree.Selector.prototype = { + type: "Selector", + accept: function (visitor) { + this.elements = visitor.visit(this.elements); + this.extendList = visitor.visit(this.extendList); + 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; + return newSelector; + }, + match: function (other) { + var elements = this.elements, + len = elements.length, + oelements, olen, max, i; + + oelements = other.elements.slice( + (other.elements.length && other.elements[0].value === "&") ? 1 : 0); + olen = oelements.length; + max = Math.min(len, olen); + + if (olen === 0 || len < olen) { + return false; + } else { + for (i = 0; i < max; i++) { + if (elements[i].value !== oelements[i].value) { + return false; + } + } + } + return true; + }, + eval: function (env) { + var evaldCondition = this.condition && this.condition.eval(env); + + return this.createDerived(this.elements.map(function (e) { + return e.eval(env); + }), this.extendList.map(function(extend) { + return extend.eval(env); + }), evaldCondition); + }, + genCSS: function (env, output) { + var i, element; + if ((!env || !env.firstSelector) && this.elements[0].combinator.value === "") { + output.add(' ', this.currentFileInfo, this.index); + } + if (!this._css) { + //TODO caching? speed comparison? + for(i = 0; i < this.elements.length; i++) { + element = this.elements[i]; + element.genCSS(env, output); + } + } + }, + toCSS: tree.toCSS, + markReferenced: function () { + this.isReferenced = true; + }, + getIsReferenced: function() { + return !this.currentFileInfo.reference || this.isReferenced; + }, + getIsOutput: function() { + return this.evaldCondition; + } +}; + +})(require('../tree')); +(function (tree) { + +tree.UnicodeDescriptor = function (value) { + this.value = value; +}; +tree.UnicodeDescriptor.prototype = { + type: "UnicodeDescriptor", + genCSS: function (env, output) { + output.add(this.value); + }, + toCSS: tree.toCSS, + eval: function () { return this; } +}; + +})(require('../tree')); +(function (tree) { + +tree.URL = function (val, currentFileInfo) { + this.value = val; + this.currentFileInfo = currentFileInfo; +}; +tree.URL.prototype = { + type: "Url", + accept: function (visitor) { + this.value = visitor.visit(this.value); + }, + 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; + + // Add the base path if the URL is relative + rootpath = this.currentFileInfo && this.currentFileInfo.rootpath; + if (rootpath && typeof val.value === "string" && ctx.isPathRelative(val.value)) { + if (!val.quote) { + rootpath = rootpath.replace(/[\(\)'"\s]/g, function(match) { return "\\"+match; }); + } + val.value = rootpath + val.value; + } + + val.value = ctx.normalizePath(val.value); + + return new(tree.URL)(val, null); + } +}; + +})(require('../tree')); +(function (tree) { + +tree.Value = function (value) { + this.value = value; +}; +tree.Value.prototype = { + type: "Value", + accept: function (visitor) { + this.value = visitor.visit(this.value); + }, + eval: function (env) { + if (this.value.length === 1) { + return this.value[0].eval(env); + } else { + return new(tree.Value)(this.value.map(function (v) { + return v.eval(env); + })); + } + }, + 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 && env.compress) ? ',' : ', '); + } + } + }, + toCSS: tree.toCSS +}; + +})(require('../tree')); +(function (tree) { + +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) { + name = '@' + new(tree.Variable)(name.slice(1)).eval(env).value; + } + + if (this.evaluating) { + throw { type: 'Name', + message: "Recursive variable definition for " + name, + filename: this.currentFileInfo.file, + index: this.index }; + } + + this.evaluating = true; + + if (variable = tree.find(env.frames, function (frame) { + if (v = frame.variable(name)) { + return v.value.eval(env); + } + })) { + this.evaluating = false; + return variable; + } + else { + throw { type: 'Name', + message: "variable " + name + " is undefined", + filename: this.currentFileInfo.filename, + index: this.index }; + } + } +}; + +})(require('../tree')); +(function (tree) { + + var parseCopyProperties = [ + 'paths', // option - unmodified - paths to search for imports on + 'optimization', // option - optimization level (for the chunker) + 'files', // list of files that have been imported, used for import-once + 'contents', // browser-only, contents of all the files + 'relativeUrls', // option - whether to adjust URL's to be relative + 'rootpath', // option - rootpath to append to URL's + 'strictImports', // option - + 'dumpLineNumbers', // option - whether to dump line numbers + 'compress', // option - whether to compress + 'processImports', // option - whether to process imports. if false then imports will not be imported + 'syncImport', // option - whether to import synchronously + 'javascriptEnabled',// option - whether JavaScript is enabled. if undefined, defaults to true + 'mime', // browser only - mime type for sheet import + 'useFileCache', // browser only - whether to use the per file session cache + 'currentFileInfo' // information about the current file - for error reporting and importing and making urls relative etc. + ]; + + //currentFileInfo = { + // 'relativeUrls' - option - whether to adjust URL's to be relative + // 'filename' - full resolved filename of current file + // 'rootpath' - path to append to normal URLs for this node + // 'currentDirectory' - path to the current file, absolute + // 'rootFilename' - filename of the base file + // 'entryPath' - absolute path to the entry file + // 'reference' - whether the file should not be output and only output parts that are referenced + + tree.parseEnv = function(options) { + copyFromOriginal(options, this, parseCopyProperties); + + if (!this.contents) { this.contents = {}; } + if (!this.files) { this.files = {}; } + + if (!this.currentFileInfo) { + var filename = (options && options.filename) || "input"; + var entryPath = filename.replace(/[^\/\\]*$/, ""); + if (options) { + options.filename = null; + } + this.currentFileInfo = { + filename: filename, + relativeUrls: this.relativeUrls, + rootpath: (options && options.rootpath) || "", + currentDirectory: entryPath, + entryPath: entryPath, + rootFilename: filename + }; + } + }; + + var evalCopyProperties = [ + 'silent', // whether to swallow errors and warnings + 'verbose', // whether to log more activity + 'compress', // whether to compress + 'yuicompress', // whether to compress with the outside tool yui compressor + '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 + 'sourceMap' // whether to output a source map + ]; + + tree.evalEnv = function(options, frames) { + copyFromOriginal(options, this, evalCopyProperties); + + this.frames = frames || []; + }; + + tree.evalEnv.prototype.inParenthesis = function () { + if (!this.parensStack) { + this.parensStack = []; + } + this.parensStack.push(true); + }; + + tree.evalEnv.prototype.outOfParenthesis = function () { + this.parensStack.pop(); + }; + + tree.evalEnv.prototype.isMathOn = function () { + return this.strictMath ? (this.parensStack && this.parensStack.length) : true; + }; + + tree.evalEnv.prototype.isPathRelative = function (path) { + return !/^(?:[a-z-]+:|\/)/.test(path); + }; + + tree.evalEnv.prototype.normalizePath = function( path ) { + var + segments = path.split("/").reverse(), + segment; + + path = []; + while (segments.length !== 0 ) { + segment = segments.pop(); + switch( segment ) { + case ".": + break; + case "..": + if ((path.length === 0) || (path[path.length - 1] === "..")) { + path.push( segment ); + } else { + path.pop(); + } + break; + default: + path.push( segment ); + break; + } + } + + return path.join("/"); + }; + + //todo - do the same for the toCSS env + //tree.toCSSEnv = function (options) { + //}; + + var copyFromOriginal = function(original, destination, propertiesToCopy) { + if (!original) { return; } + + for(var i = 0; i < propertiesToCopy.length; i++) { + if (original.hasOwnProperty(propertiesToCopy[i])) { + destination[propertiesToCopy[i]] = original[propertiesToCopy[i]]; + } + } + }; + +})(require('./tree')); +(function (tree) { + + tree.visitor = function(implementation) { + this._implementation = implementation; + }; + + tree.visitor.prototype = { + visit: function(node) { + + if (node instanceof Array) { + return this.visitArray(node); + } + + if (!node || !node.type) { + return node; + } + + var funcName = "visit" + node.type, + func = this._implementation[funcName], + visitArgs, newNode; + if (func) { + visitArgs = {visitDeeper: true}; + newNode = func.call(this._implementation, node, visitArgs); + if (this._implementation.isReplacing) { + node = newNode; + } + } + if ((!visitArgs || visitArgs.visitDeeper) && node && node.accept) { + node.accept(this); + } + funcName = funcName + "Out"; + if (this._implementation[funcName]) { + this._implementation[funcName](node); + } + return node; + }, + visitArray: function(nodes) { + var i, newNodes = []; + for(i = 0; i < nodes.length; i++) { + var evald = this.visit(nodes[i]); + if (evald instanceof Array) { + evald = this.flatten(evald); + newNodes = newNodes.concat(evald); + } else { + newNodes.push(evald); + } + } + if (this._implementation.isReplacing) { + return newNodes; + } + return nodes; + }, + doAccept: function (node) { + node.accept(this); + }, + flatten: function(arr, master) { + return arr.reduce(this.flattenReduce.bind(this), master || []); + }, + flattenReduce: function(sum, element) { + if (element instanceof Array) { + sum = this.flatten(element, sum); + } else { + sum.push(element); + } + return sum; + } + }; + +})(require('./tree'));(function (tree) { + tree.importVisitor = function(importer, finish, evalEnv) { + this._visitor = new tree.visitor(this); + this._importer = importer; + this._finish = finish; + this.env = evalEnv || new tree.evalEnv(); + this.importCount = 0; + }; + + tree.importVisitor.prototype = { + isReplacing: true, + run: function (root) { + var error; + try { + // process the contents + this._visitor.visit(root); + } + catch(e) { + error = e; + } + + this.isFinished = true; + + if (this.importCount === 0) { + this._finish(error); + } + }, + visitImport: function (importNode, visitArgs) { + var importVisitor = this, + evaldImportNode, + inlineCSS = importNode.options.inline; + + if (!importNode.css || inlineCSS) { + + try { + evaldImportNode = importNode.evalForImport(this.env); + } catch(e){ + if (!e.filename) { e.index = importNode.index; e.filename = importNode.currentFileInfo.filename; } + // attempt to eval properly and treat as css + importNode.css = true; + // if that fails, this error will be thrown + importNode.error = e; + } + + if (evaldImportNode && (!evaldImportNode.css || inlineCSS)) { + importNode = evaldImportNode; + this.importCount++; + var env = new tree.evalEnv(this.env, this.env.frames.slice(0)); + + this._importer.push(importNode.getPath(), importNode.currentFileInfo, importNode.options, function (e, root, imported) { + if (e && !e.filename) { e.index = importNode.index; e.filename = importNode.currentFileInfo.filename; } + if (imported && !importNode.options.multiple) { importNode.skip = imported; } + + var subFinish = function(e) { + importVisitor.importCount--; + + if (importVisitor.importCount === 0 && importVisitor.isFinished) { + importVisitor._finish(e); + } + }; + + if (root) { + importNode.root = root; + if (!inlineCSS && !importNode.skip) { + new(tree.importVisitor)(importVisitor._importer, subFinish, env) + .run(root); + return; + } + } + + subFinish(); + }); + } + } + visitArgs.visitDeeper = false; + return importNode; + }, + visitRule: function (ruleNode, visitArgs) { + visitArgs.visitDeeper = false; + return ruleNode; + }, + visitDirective: function (directiveNode, visitArgs) { + this.env.frames.unshift(directiveNode); + return directiveNode; + }, + visitDirectiveOut: function (directiveNode) { + this.env.frames.shift(); + }, + visitMixinDefinition: function (mixinDefinitionNode, visitArgs) { + this.env.frames.unshift(mixinDefinitionNode); + return mixinDefinitionNode; + }, + visitMixinDefinitionOut: function (mixinDefinitionNode) { + this.env.frames.shift(); + }, + visitRuleset: function (rulesetNode, visitArgs) { + this.env.frames.unshift(rulesetNode); + return rulesetNode; + }, + visitRulesetOut: function (rulesetNode) { + this.env.frames.shift(); + }, + visitMedia: function (mediaNode, visitArgs) { + this.env.frames.unshift(mediaNode.ruleset); + return mediaNode; + }, + visitMediaOut: function (mediaNode) { + this.env.frames.shift(); + } + }; + +})(require('./tree'));(function (tree) { + tree.joinSelectorVisitor = function() { + this.contexts = [[]]; + this._visitor = new tree.visitor(this); + }; + + tree.joinSelectorVisitor.prototype = { + run: function (root) { + return this._visitor.visit(root); + }, + visitRule: function (ruleNode, visitArgs) { + visitArgs.visitDeeper = false; + }, + visitMixinDefinition: function (mixinDefinitionNode, visitArgs) { + visitArgs.visitDeeper = false; + }, + + visitRuleset: function (rulesetNode, visitArgs) { + var context = this.contexts[this.contexts.length - 1]; + var paths = []; + this.contexts.push(paths); + + if (! rulesetNode.root) { + rulesetNode.selectors = rulesetNode.selectors.filter(function(selector) { return selector.getIsOutput(); }); + if (rulesetNode.selectors.length === 0) { + rulesetNode.rules.length = 0; + } + rulesetNode.joinSelectors(paths, context, rulesetNode.selectors); + rulesetNode.paths = paths; + } + }, + visitRulesetOut: function (rulesetNode) { + this.contexts.length = this.contexts.length - 1; + }, + visitMedia: function (mediaNode, visitArgs) { + var context = this.contexts[this.contexts.length - 1]; + mediaNode.rules[0].root = (context.length === 0 || context[0].multiMedia); + } + }; + +})(require('./tree'));(function (tree) { + tree.toCSSVisitor = function(env) { + this._visitor = new tree.visitor(this); + this._env = env; + }; + + tree.toCSSVisitor.prototype = { + isReplacing: true, + run: function (root) { + return this._visitor.visit(root); + }, + + visitRule: function (ruleNode, visitArgs) { + if (ruleNode.variable) { + return []; + } + return ruleNode; + }, + + visitMixinDefinition: function (mixinNode, visitArgs) { + return []; + }, + + visitExtend: function (extendNode, visitArgs) { + return []; + }, + + visitComment: function (commentNode, visitArgs) { + if (commentNode.isSilent(this._env)) { + return []; + } + 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 + // be considered illegal css as it has to be on the first line + if (this.charset) { + if (directiveNode.debugInfo) { + var comment = new tree.Comment("/* " + directiveNode.toCSS(this._env).replace(/\n/g, "")+" */\n"); + comment.debugInfo = directiveNode.debugInfo; + return this._visitor.visit(comment); + } + return []; + } + this.charset = true; + } + return directiveNode; + }, + + checkPropertiesInRoot: function(rules) { + var ruleNode; + for(var i = 0; i < rules.length; i++) { + ruleNode = rules[i]; + if (ruleNode instanceof tree.Rule && !ruleNode.variable) { + throw { message: "properties must be inside selector blocks, they cannot be in the root.", + index: ruleNode.index, filename: ruleNode.currentFileInfo ? ruleNode.currentFileInfo.filename : null}; + } + } + }, + + visitRuleset: function (rulesetNode, visitArgs) { + var rule, rulesets = []; + if (rulesetNode.firstRoot) { + this.checkPropertiesInRoot(rulesetNode.rules); + } + if (! rulesetNode.root) { + + 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; + } + return false; + } + }); + + // Compile rules and rulesets + for (var i = 0; i < rulesetNode.rules.length; i++) { + rule = rulesetNode.rules[i]; + + if (rule.rules) { + // visit because we are moving them out from being a child + rulesets.push(this._visitor.visit(rule)); + rulesetNode.rules.splice(i, 1); + i--; + continue; + } + } + // 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.accept(this._visitor); + } + visitArgs.visitDeeper = false; + + this._mergeRules(rulesetNode.rules); + this._removeDuplicateRules(rulesetNode.rules); + + // now decide whether we keep the ruleset + 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 === 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; + })); + } + }); + } + }; + +})(require('./tree'));(function (tree) { + /*jshint loopfunc:true */ + + tree.extendFinderVisitor = function() { + this._visitor = new tree.visitor(this); + this.contexts = []; + this.allExtendsStack = [[]]; + }; + + tree.extendFinderVisitor.prototype = { + run: function (root) { + root = this._visitor.visit(root); + root.allExtends = this.allExtendsStack[0]; + return root; + }, + visitRule: function (ruleNode, visitArgs) { + visitArgs.visitDeeper = false; + }, + visitMixinDefinition: function (mixinDefinitionNode, visitArgs) { + visitArgs.visitDeeper = false; + }, + visitRuleset: function (rulesetNode, visitArgs) { + + if (rulesetNode.root) { + return; + } + + var i, j, extend, allSelectorsExtendList = [], extendList; + + // get &:extend(.a); rules which apply to all selectors in this ruleset + for(i = 0; i < rulesetNode.rules.length; i++) { + if (rulesetNode.rules[i] instanceof tree.Extend) { + allSelectorsExtendList.push(rulesetNode.rules[i]); + } + } + + // now find every selector and apply the extends that apply to all extends + // and the ones which apply to an individual extend + for(i = 0; i < rulesetNode.paths.length; i++) { + var selectorPath = rulesetNode.paths[i], + selector = selectorPath[selectorPath.length-1]; + extendList = selector.extendList.slice(0).concat(allSelectorsExtendList).map(function(allSelectorsExtend) { + return allSelectorsExtend.clone(); + }); + for(j = 0; j < extendList.length; j++) { + this.foundExtends = true; + extend = extendList[j]; + extend.findSelfSelectors(selectorPath); + extend.ruleset = rulesetNode; + if (j === 0) { extend.firstExtendOnThisSelectorPath = true; } + this.allExtendsStack[this.allExtendsStack.length-1].push(extend); + } + } + + this.contexts.push(rulesetNode.selectors); + }, + visitRulesetOut: function (rulesetNode) { + if (!rulesetNode.root) { + this.contexts.length = this.contexts.length - 1; + } + }, + visitMedia: function (mediaNode, visitArgs) { + mediaNode.allExtends = []; + this.allExtendsStack.push(mediaNode.allExtends); + }, + visitMediaOut: function (mediaNode) { + this.allExtendsStack.length = this.allExtendsStack.length - 1; + }, + visitDirective: function (directiveNode, visitArgs) { + directiveNode.allExtends = []; + this.allExtendsStack.push(directiveNode.allExtends); + }, + visitDirectiveOut: function (directiveNode) { + this.allExtendsStack.length = this.allExtendsStack.length - 1; + } + }; + + tree.processExtendsVisitor = function() { + this._visitor = new tree.visitor(this); + }; + + tree.processExtendsVisitor.prototype = { + run: function(root) { + var extendFinder = new tree.extendFinderVisitor(); + extendFinder.run(root); + if (!extendFinder.foundExtends) { return root; } + root.allExtends = root.allExtends.concat(this.doExtendChaining(root.allExtends, root.allExtends)); + this.allExtendsStack = [root.allExtends]; + return this._visitor.visit(root); + }, + doExtendChaining: function (extendsList, extendsListTarget, iterationCount) { + // + // chaining is different from normal extension.. if we extend an extend then we are not just copying, altering and pasting + // the selector we would do normally, but we are also adding an extend with the same target selector + // this means this new extend can then go and alter other extends + // + // this method deals with all the chaining work - without it, extend is flat and doesn't work on other extend selectors + // this is also the most expensive.. and a match on one selector can cause an extension of a selector we had already processed if + // we look at each selector at a time, as is done in visitRuleset + + var extendIndex, targetExtendIndex, matches, extendsToAdd = [], newSelector, extendVisitor = this, selectorPath, extend, targetExtend, newExtend; + + iterationCount = iterationCount || 0; + + //loop through comparing every extend with every target extend. + // a target extend is the one on the ruleset we are looking at copy/edit/pasting in place + // e.g. .a:extend(.b) {} and .b:extend(.c) {} then the first extend extends the second one + // and the second is the target. + // the seperation into two lists allows us to process a subset of chains with a bigger set, as is the + // case when processing media queries + for(extendIndex = 0; extendIndex < extendsList.length; extendIndex++){ + for(targetExtendIndex = 0; targetExtendIndex < extendsListTarget.length; targetExtendIndex++){ + + extend = extendsList[extendIndex]; + targetExtend = extendsListTarget[targetExtendIndex]; + + // look for circular references + if (this.inInheritanceChain(targetExtend, extend)) { continue; } + + // find a match in the target extends self selector (the bit before :extend) + selectorPath = [targetExtend.selfSelectors[0]]; + matches = extendVisitor.findMatch(extend, selectorPath); + + if (matches.length) { + + // we found a match, so for each self selector.. + extend.selfSelectors.forEach(function(selfSelector) { + + // process the extend as usual + newSelector = extendVisitor.extendSelector(matches, selectorPath, selfSelector); + + // but now we create a new extend from it + newExtend = new(tree.Extend)(targetExtend.selector, targetExtend.option, 0); + newExtend.selfSelectors = newSelector; + + // add the extend onto the list of extends for that selector + newSelector[newSelector.length-1].extendList = [newExtend]; + + // record that we need to add it. + extendsToAdd.push(newExtend); + newExtend.ruleset = targetExtend.ruleset; + + //remember its parents for circular references + newExtend.parents = [targetExtend, extend]; + + // only process the selector once.. if we have :extend(.a,.b) then multiple + // extends will look at the same selector path, so when extending + // we know that any others will be duplicates in terms of what is added to the css + if (targetExtend.firstExtendOnThisSelectorPath) { + newExtend.firstExtendOnThisSelectorPath = true; + targetExtend.ruleset.paths.push(newSelector); + } + }); + } + } + } + + if (extendsToAdd.length) { + // try to detect circular references to stop a stack overflow. + // may no longer be needed. + this.extendChainCount++; + if (iterationCount > 100) { + var selectorOne = "{unable to calculate}"; + var selectorTwo = "{unable to calculate}"; + try + { + selectorOne = extendsToAdd[0].selfSelectors[0].toCSS(); + selectorTwo = extendsToAdd[0].selector.toCSS(); + } + catch(e) {} + throw {message: "extend circular reference detected. One of the circular extends is currently:"+selectorOne+":extend(" + selectorTwo+")"}; + } + + // now process the new extends on the existing rules so that we can handle a extending b extending c ectending d extending e... + return extendsToAdd.concat(extendVisitor.doExtendChaining(extendsToAdd, extendsListTarget, iterationCount+1)); + } else { + return extendsToAdd; + } + }, + inInheritanceChain: function (possibleParent, possibleChild) { + if (possibleParent === possibleChild) { + return true; + } + if (possibleChild.parents) { + if (this.inInheritanceChain(possibleParent, possibleChild.parents[0])) { + return true; + } + if (this.inInheritanceChain(possibleParent, possibleChild.parents[1])) { + return true; + } + } + return false; + }, + visitRule: function (ruleNode, visitArgs) { + visitArgs.visitDeeper = false; + }, + visitMixinDefinition: function (mixinDefinitionNode, visitArgs) { + visitArgs.visitDeeper = false; + }, + visitSelector: function (selectorNode, visitArgs) { + visitArgs.visitDeeper = false; + }, + visitRuleset: function (rulesetNode, visitArgs) { + if (rulesetNode.root) { + return; + } + var matches, pathIndex, extendIndex, allExtends = this.allExtendsStack[this.allExtendsStack.length-1], selectorsToAdd = [], extendVisitor = this, selectorPath; + + // look at each selector path in the ruleset, find any extend matches and then copy, find and replace + + for(extendIndex = 0; extendIndex < allExtends.length; extendIndex++) { + for(pathIndex = 0; pathIndex < rulesetNode.paths.length; pathIndex++) { + + selectorPath = rulesetNode.paths[pathIndex]; + + // extending extends happens initially, before the main pass + if (selectorPath[selectorPath.length-1].extendList.length) { continue; } + + matches = this.findMatch(allExtends[extendIndex], selectorPath); + + if (matches.length) { + + allExtends[extendIndex].selfSelectors.forEach(function(selfSelector) { + selectorsToAdd.push(extendVisitor.extendSelector(matches, selectorPath, selfSelector)); + }); + } + } + } + rulesetNode.paths = rulesetNode.paths.concat(selectorsToAdd); + }, + findMatch: function (extend, haystackSelectorPath) { + // + // look through the haystack selector path to try and find the needle - extend.selector + // returns an array of selector matches that can then be replaced + // + var haystackSelectorIndex, hackstackSelector, hackstackElementIndex, haystackElement, + targetCombinator, i, + extendVisitor = this, + needleElements = extend.selector.elements, + potentialMatches = [], potentialMatch, matches = []; + + // loop through the haystack elements + for(haystackSelectorIndex = 0; haystackSelectorIndex < haystackSelectorPath.length; haystackSelectorIndex++) { + hackstackSelector = haystackSelectorPath[haystackSelectorIndex]; + + for(hackstackElementIndex = 0; hackstackElementIndex < hackstackSelector.elements.length; hackstackElementIndex++) { + + 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)) { + potentialMatches.push({pathIndex: haystackSelectorIndex, index: hackstackElementIndex, matched: 0, initialCombinator: haystackElement.combinator}); + } + + for(i = 0; i < potentialMatches.length; i++) { + potentialMatch = potentialMatches[i]; + + // selectors add " " onto the first element. When we use & it joins the selectors together, but if we don't + // 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) { + targetCombinator = ' '; + } + + // if we don't match, null our match to indicate failure + if (!extendVisitor.isElementValuesEqual(needleElements[potentialMatch.matched].value, haystackElement.value) || + (potentialMatch.matched > 0 && needleElements[potentialMatch.matched].combinator.value !== targetCombinator)) { + potentialMatch = null; + } else { + potentialMatch.matched++; + } + + // if we are still valid and have finished, test whether we have elements after and whether these are allowed + if (potentialMatch) { + potentialMatch.finished = potentialMatch.matched === needleElements.length; + if (potentialMatch.finished && + (!extend.allowAfter && (hackstackElementIndex+1 < hackstackSelector.elements.length || haystackSelectorIndex+1 < haystackSelectorPath.length))) { + potentialMatch = null; + } + } + // if null we remove, if not, we are still valid, so either push as a valid match or continue + if (potentialMatch) { + if (potentialMatch.finished) { + potentialMatch.length = needleElements.length; + potentialMatch.endPathIndex = haystackSelectorIndex; + potentialMatch.endPathElementIndex = hackstackElementIndex + 1; // index after end of match + potentialMatches.length = 0; // we don't allow matches to overlap, so start matching again + matches.push(potentialMatch); + } + } else { + potentialMatches.splice(i, 1); + i--; + } + } + } + } + return matches; + }, + isElementValuesEqual: function(elementValue1, elementValue2) { + if (typeof elementValue1 === "string" || typeof elementValue2 === "string") { + return elementValue1 === elementValue2; + } + if (elementValue1 instanceof tree.Attribute) { + if (elementValue1.op !== elementValue2.op || elementValue1.key !== elementValue2.key) { + return false; + } + if (!elementValue1.value || !elementValue2.value) { + if (elementValue1.value || elementValue2.value) { + return false; + } + return true; + } + elementValue1 = elementValue1.value.value || elementValue1.value; + elementValue2 = elementValue2.value.value || elementValue2.value; + return elementValue1 === elementValue2; + } + return false; + }, + extendSelector:function (matches, selectorPath, replacementSelector) { + + //for a set of matches, replace each match with the replacement selector + + var currentSelectorPathIndex = 0, + currentSelectorPathElementIndex = 0, + path = [], + matchIndex, + selector, + firstElement, + match, + newElements; + + for (matchIndex = 0; matchIndex < matches.length; matchIndex++) { + match = matches[matchIndex]; + selector = selectorPath[match.pathIndex]; + firstElement = new tree.Element( + match.initialCombinator, + replacementSelector.elements[0].value, + replacementSelector.elements[0].index, + replacementSelector.elements[0].currentFileInfo + ); + + if (match.pathIndex > currentSelectorPathIndex && currentSelectorPathElementIndex > 0) { + path[path.length - 1].elements = path[path.length - 1].elements.concat(selectorPath[currentSelectorPathIndex].elements.slice(currentSelectorPathElementIndex)); + currentSelectorPathElementIndex = 0; + currentSelectorPathIndex++; + } + + newElements = selector.elements + .slice(currentSelectorPathElementIndex, match.index) + .concat([firstElement]) + .concat(replacementSelector.elements.slice(1)); + + if (currentSelectorPathIndex === match.pathIndex && matchIndex > 0) { + path[path.length - 1].elements = + path[path.length - 1].elements.concat(newElements); + } else { + path = path.concat(selectorPath.slice(currentSelectorPathIndex, match.pathIndex)); + + path.push(new tree.Selector( + newElements + )); + } + currentSelectorPathIndex = match.endPathIndex; + currentSelectorPathElementIndex = match.endPathElementIndex; + if (currentSelectorPathElementIndex >= selectorPath[currentSelectorPathIndex].elements.length) { + currentSelectorPathElementIndex = 0; + currentSelectorPathIndex++; + } + } + + if (currentSelectorPathIndex < selectorPath.length && currentSelectorPathElementIndex > 0) { + path[path.length - 1].elements = path[path.length - 1].elements.concat(selectorPath[currentSelectorPathIndex].elements.slice(currentSelectorPathElementIndex)); + currentSelectorPathElementIndex = 0; + currentSelectorPathIndex++; + } + + path = path.concat(selectorPath.slice(currentSelectorPathIndex, selectorPath.length)); + + return path; + }, + visitRulesetOut: function (rulesetNode) { + }, + visitMedia: function (mediaNode, visitArgs) { + var newAllExtends = mediaNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]); + newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, mediaNode.allExtends)); + this.allExtendsStack.push(newAllExtends); + }, + visitMediaOut: function (mediaNode) { + this.allExtendsStack.length = this.allExtendsStack.length - 1; + }, + visitDirective: function (directiveNode, visitArgs) { + var newAllExtends = directiveNode.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]); + newAllExtends = newAllExtends.concat(this.doExtendChaining(newAllExtends, directiveNode.allExtends)); + this.allExtendsStack.push(newAllExtends); + }, + visitDirectiveOut: function (directiveNode) { + this.allExtendsStack.length = this.allExtendsStack.length - 1; + } + }; + +})(require('./tree')); +// +// browser.js - client-side engine +// +/*global less, window, document, XMLHttpRequest, location */ + +var isFileProtocol = /^(file|chrome(-extension)?|resource|qrc|app):/.test(location.protocol); + +less.env = less.env || (location.hostname == '127.0.0.1' || + location.hostname == '0.0.0.0' || + location.hostname == 'localhost' || + location.port.length > 0 || + isFileProtocol ? 'development' + : 'production'); + +var logLevel = { + info: 2, + errors: 1, + none: 0 +}; + +// The amount of logging in the javascript console. +// 2 - Information and errors +// 1 - Errors +// 0 - None +// Defaults to 2 +less.logLevel = typeof(less.logLevel) != 'undefined' ? less.logLevel : logLevel.info; + +// Load styles asynchronously (default: false) +// +// This is set to `false` by default, so that the body +// doesn't start loading before the stylesheets are parsed. +// Setting this to `true` can result in flickering. +// +less.async = less.async || false; +less.fileAsync = less.fileAsync || false; + +// Interval between watch polls +less.poll = less.poll || (isFileProtocol ? 1000 : 1500); + +//Setup user functions +if (less.functions) { + for(var func in less.functions) { + less.tree.functions[func] = less.functions[func]; + } +} + +var dumpLineNumbers = /!dumpLineNumbers:(comments|mediaquery|all)/.exec(location.hash); +if (dumpLineNumbers) { + less.dumpLineNumbers = dumpLineNumbers[1]; +} + +var typePattern = /^text\/(x-)?less$/; +var cache = null; +var fileCache = {}; + +function log(str, level) { + if (less.env == 'development' && typeof(console) !== 'undefined' && less.logLevel >= level) { + console.log('less: ' + str); + } +} + +function extractId(href) { + return href.replace(/^[a-z-]+:\/+?[^\/]+/, '' ) // Remove protocol & domain + .replace(/^\//, '' ) // Remove root / + .replace(/\.[a-zA-Z]+$/, '' ) // Remove simple extension + .replace(/[^\.\w-]+/g, '-') // Replace illegal characters + .replace(/\./g, ':'); // Replace dots with colons(for valid id) +} + +function errorConsole(e, rootHref) { + var template = '{line} {content}'; + var filename = e.filename || rootHref; + var errors = []; + var content = (e.type || "Syntax") + "Error: " + (e.message || 'There is an error in your .less file') + + " in " + filename + " "; + + var errorline = function (e, i, classname) { + 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])); + } + }; + + if (e.extract) { + errorline(e, 0, ''); + errorline(e, 1, 'line'); + errorline(e, 2, ''); + content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':\n' + + errors.join('\n'); + } else if (e.stack) { + content += e.stack; + } + log(content, logLevel.errors); +} + +function createCSS(styles, sheet, lastModified) { + // Strip the query-string + var href = sheet.href || ''; + + // If there is no title set, use the filename, minus the extension + var id = 'less:' + (sheet.title || extractId(href)); + + // If this has already been inserted into the DOM, we may need to replace it + var oldCss = document.getElementById(id); + var keepOldCss = false; + + // Create a new stylesheet node for insertion or (if necessary) replacement + var css = document.createElement('style'); + css.setAttribute('type', 'text/css'); + if (sheet.media) { + css.setAttribute('media', sheet.media); + } + css.id = id; + + if (css.styleSheet) { // IE + try { + css.styleSheet.cssText = styles; + } catch (e) { + throw new(Error)("Couldn't reassign styleSheet.cssText."); + } + } else { + css.appendChild(document.createTextNode(styles)); + + // If new contents match contents of oldCss, don't replace oldCss + keepOldCss = (oldCss !== null && oldCss.childNodes.length > 0 && css.childNodes.length > 0 && + oldCss.firstChild.nodeValue === css.firstChild.nodeValue); + } + + var head = document.getElementsByTagName('head')[0]; + + // 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) { + var nextEl = sheet && sheet.nextSibling || null; + if (nextEl) { + nextEl.parentNode.insertBefore(css, nextEl); + } else { + head.appendChild(css); + } + } + if (oldCss && keepOldCss === false) { + oldCss.parentNode.removeChild(oldCss); + } + + // Don't update the local store if the file wasn't modified + if (lastModified && cache) { + log('saving ' + href + ' to cache.', logLevel.info); + try { + cache.setItem(href, styles); + cache.setItem(href + ':timestamp', lastModified); + } catch(e) { + //TODO - could do with adding more robust error handling + log('failed to save', logLevel.errors); + } + } +} + +function errorHTML(e, rootHref) { + var id = 'less-error-message:' + extractId(rootHref || ""); + var template = '
  • {content}
  • '; + var elem = document.createElement('div'), timer, content, errors = []; + var filename = e.filename || rootHref; + var filenameNoPath = filename.match(/([^\/]+(\?.*)?)$/)[1]; + + elem.id = id; + elem.className = "less-error-message"; + + content = '

    ' + (e.type || "Syntax") + "Error: " + (e.message || 'There is an error in your .less file') + + '

    ' + '

    in ' + filenameNoPath + " "; + + var errorline = function (e, i, classname) { + 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])); + } + }; + + if (e.extract) { + errorline(e, 0, ''); + 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('
    '); + } + elem.innerHTML = content; + + // CSS for error messages + createCSS([ + '.less-error-message ul, .less-error-message li {', + 'list-style-type: none;', + 'margin-right: 15px;', + 'padding: 4px 0;', + 'margin: 0;', + '}', + '.less-error-message label {', + 'font-size: 12px;', + 'margin-right: 15px;', + 'padding: 4px 0;', + 'color: #cc7777;', + '}', + '.less-error-message pre {', + 'color: #dd6666;', + 'padding: 4px 0;', + 'margin: 0;', + 'display: inline-block;', + '}', + '.less-error-message pre.line {', + 'color: #ff0000;', + '}', + '.less-error-message h3 {', + 'font-size: 20px;', + 'font-weight: bold;', + 'padding: 15px 0 5px 0;', + 'margin: 0;', + '}', + '.less-error-message a {', + 'color: #10a', + '}', + '.less-error-message .error {', + 'color: red;', + 'font-weight: bold;', + 'padding-bottom: 2px;', + 'border-bottom: 1px dashed red;', + '}' + ].join('\n'), { title: 'error-message' }); + + elem.style.cssText = [ + "font-family: Arial, sans-serif", + "border: 1px solid #e00", + "background-color: #eee", + "border-radius: 5px", + "-webkit-border-radius: 5px", + "-moz-border-radius: 5px", + "color: #e00", + "padding: 15px", + "margin-bottom: 15px" + ].join(';'); + + if (less.env == 'development') { + timer = setInterval(function () { + if (document.body) { + if (document.getElementById(id)) { + document.body.replaceChild(elem, document.getElementById(id)); + } else { + document.body.insertBefore(elem, document.body.firstChild); + } + clearInterval(timer); + } + }, 10); + } +} + +function error(e, rootHref) { + if (!less.errorReporting || less.errorReporting === "html") { + errorHTML(e, rootHref); + } else if (less.errorReporting === "console") { + errorConsole(e, rootHref); + } else if (typeof less.errorReporting === 'function') { + less.errorReporting("add", e, rootHref); + } +} + +function removeErrorHTML(path) { + var node = document.getElementById('less-error-message:' + extractId(path)); + if (node) { + node.parentNode.removeChild(node); + } +} + +function removeErrorConsole(path) { + //no action +} + +function removeError(path) { + if (!less.errorReporting || less.errorReporting === "html") { + removeErrorHTML(path); + } else if (less.errorReporting === "console") { + removeErrorConsole(path); + } else if (typeof less.errorReporting === 'function') { + less.errorReporting("remove", path); + } +} + +function loadStyles(newVars) { + var styles = document.getElementsByTagName('style'), + style; + for (var i = 0; i < styles.length; i++) { + style = styles[i]; + if (style.type.match(typePattern)) { + var env = new less.tree.parseEnv(less), + lessText = style.innerHTML || ''; + env.filename = document.location.href.replace(/#.*$/, ''); + if (newVars) { + env.useFileCache = true; + lessText += "\n" + newVars; + } + + /*jshint loopfunc:true */ + // use closure to store current value of i + var callback = (function(style) { + return function (e, cssAST) { + if (e) { + return error(e, "inline"); + } + var css = cssAST.toCSS(less); + style.type = 'text/css'; + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.innerHTML = css; + } + }; + })(style); + new(less.Parser)(env).parse(lessText, callback); + } + } +} + +function extractUrlParts(url, baseUrl) { + // urlParts[1] = protocol&hostname || / + // urlParts[2] = / if path relative to host base + // urlParts[3] = directories + // urlParts[4] = filename + // urlParts[5] = parameters + + var urlPartsRegex = /^((?:[a-z-]+:)?\/+?(?:[^\/\?#]*\/)|([\/\\]))?((?:[^\/\\\?#]*[\/\\])*)([^\/\\\?#]*)([#\?].*)?$/i, + urlParts = url.match(urlPartsRegex), + returner = {}, directories = [], i, baseUrlParts; + + if (!urlParts) { + throw new Error("Could not parse sheet href - '"+url+"'"); + } + + // Stylesheets in IE don't always return the full path + if (!urlParts[1] || urlParts[2]) { + baseUrlParts = baseUrl.match(urlPartsRegex); + if (!baseUrlParts) { + throw new Error("Could not parse page url - '"+baseUrl+"'"); + } + urlParts[1] = urlParts[1] || baseUrlParts[1] || ""; + if (!urlParts[2]) { + urlParts[3] = baseUrlParts[3] + urlParts[3]; + } + } + + if (urlParts[3]) { + directories = urlParts[3].replace(/\\/g, "/").split("/"); + + // extract out . before .. so .. doesn't absorb a non-directory + for(i = 0; i < directories.length; i++) { + if (directories[i] === ".") { + directories.splice(i, 1); + i -= 1; + } + } + + for(i = 0; i < directories.length; i++) { + if (directories[i] === ".." && i > 0) { + directories.splice(i-1, 2); + i -= 2; + } + } + } + + returner.hostPart = urlParts[1]; + returner.directories = directories; + returner.path = urlParts[1] + directories.join("/"); + returner.fileUrl = returner.path + (urlParts[4] || ""); + returner.url = returner.fileUrl + (urlParts[5] || ""); + return returner; +} + +function pathDiff(url, baseUrl) { + // diff between two paths to create a relative path + + var urlParts = extractUrlParts(url), + baseUrlParts = extractUrlParts(baseUrl), + i, max, urlDirectories, baseUrlDirectories, diff = ""; + if (urlParts.hostPart !== baseUrlParts.hostPart) { + return ""; + } + max = Math.max(baseUrlParts.directories.length, urlParts.directories.length); + for(i = 0; i < max; i++) { + if (baseUrlParts.directories[i] !== urlParts.directories[i]) { break; } + } + baseUrlDirectories = baseUrlParts.directories.slice(i); + urlDirectories = urlParts.directories.slice(i); + for(i = 0; i < baseUrlDirectories.length-1; i++) { + diff += "../"; + } + for(i = 0; i < urlDirectories.length-1; i++) { + diff += urlDirectories[i] + "/"; + } + return diff; +} + +function getXMLHttpRequest() { + if (window.XMLHttpRequest) { + return new XMLHttpRequest(); + } else { + try { + /*global ActiveXObject */ + return new ActiveXObject("MSXML2.XMLHTTP.3.0"); + } catch (e) { + log("browser doesn't support AJAX.", logLevel.errors); + return null; + } + } +} + +function doXHR(url, type, callback, errback) { + var xhr = getXMLHttpRequest(); + var async = isFileProtocol ? less.fileAsync : less.async; + + if (typeof(xhr.overrideMimeType) === 'function') { + xhr.overrideMimeType('text/css'); + } + log("XHR: Getting '" + url + "'", logLevel.info); + xhr.open('GET', url, async); + xhr.setRequestHeader('Accept', type || 'text/x-less, text/css; q=0.9, */*; q=0.5'); + xhr.send(null); + + function handleResponse(xhr, callback, errback) { + if (xhr.status >= 200 && xhr.status < 300) { + callback(xhr.responseText, + xhr.getResponseHeader("Last-Modified")); + } else if (typeof(errback) === 'function') { + errback(xhr.status, url); + } + } + + if (isFileProtocol && !less.fileAsync) { + if (xhr.status === 0 || (xhr.status >= 200 && xhr.status < 300)) { + callback(xhr.responseText); + } else { + errback(xhr.status, url); + } + } else if (async) { + xhr.onreadystatechange = function () { + if (xhr.readyState == 4) { + handleResponse(xhr, callback, errback); + } + }; + } else { + handleResponse(xhr, callback, errback); + } +} + +function loadFile(originalHref, currentFileInfo, callback, env, newVars) { + + if (currentFileInfo && currentFileInfo.currentDirectory && !/^([a-z-]+:)?\//.test(originalHref)) { + originalHref = currentFileInfo.currentDirectory + originalHref; + } + + // sheet may be set to the stylesheet for the initial load or a collection of properties including + // some env variables for imports + var hrefParts = extractUrlParts(originalHref, window.location.href); + var href = hrefParts.url; + var newFileInfo = { + currentDirectory: hrefParts.path, + filename: href + }; + + if (currentFileInfo) { + newFileInfo.entryPath = currentFileInfo.entryPath; + newFileInfo.rootpath = currentFileInfo.rootpath; + newFileInfo.rootFilename = currentFileInfo.rootFilename; + newFileInfo.relativeUrls = currentFileInfo.relativeUrls; + } else { + newFileInfo.entryPath = hrefParts.path; + newFileInfo.rootpath = less.rootpath || hrefParts.path; + newFileInfo.rootFilename = href; + newFileInfo.relativeUrls = env.relativeUrls; + } + + if (newFileInfo.relativeUrls) { + if (env.rootpath) { + newFileInfo.rootpath = extractUrlParts(env.rootpath + pathDiff(hrefParts.path, newFileInfo.entryPath)).path; + } else { + newFileInfo.rootpath = hrefParts.path; + } + } + + if (env.useFileCache && fileCache[href]) { + try { + var lessText = fileCache[href]; + if (newVars) { + lessText += "\n" + newVars; + } + callback(null, lessText, href, newFileInfo, { lastModified: new Date() }); + } catch (e) { + callback(e, null, href); + } + return; + } + + 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 }); + } catch (e) { + callback(e, null, href); + } + }, function (status, url) { + callback({ type: 'File', message: "'" + url + "' wasn't found (" + status + ")" }, null, href); + }); +} + +function loadStyleSheet(sheet, callback, reload, remaining, newVars) { + + var env = new less.tree.parseEnv(less); + env.mime = sheet.type; + + if (newVars) { + env.useFileCache = true; + } + + loadFile(sheet.href, null, function(e, data, path, newFileInfo, webInfo) { + + if (webInfo) { + webInfo.remaining = remaining; + + var css = cache && cache.getItem(path), + timestamp = cache && cache.getItem(path + ':timestamp'); + + if (!reload && timestamp && webInfo.lastModified && + (new(Date)(webInfo.lastModified).valueOf() === + new(Date)(timestamp).valueOf())) { + // Use local copy + createCSS(css, sheet); + webInfo.local = true; + callback(null, null, data, sheet, webInfo, path); + return; + } + } + + //TODO add tests around how this behaves when reloading + removeError(path); + + if (data) { + env.currentFileInfo = newFileInfo; + new(less.Parser)(env).parse(data, function (e, root) { + if (e) { return callback(e, null, null, sheet); } + try { + callback(e, root, data, sheet, webInfo, path); + } catch (e) { + callback(e, null, null, sheet); + } + }); + } else { + callback(e, null, null, sheet, webInfo, path); + } + }, env, newVars); +} + +function loadStyleSheets(callback, reload, newVars) { + for (var i = 0; i < less.sheets.length; i++) { + loadStyleSheet(less.sheets[i], callback, reload, less.sheets.length - (i + 1), newVars); + } +} + +function initRunningMode(){ + if (less.env === 'development') { + less.optimization = 0; + less.watchTimer = setInterval(function () { + if (less.watchMode) { + loadStyleSheets(function (e, root, _, sheet, env) { + if (e) { + error(e, sheet.href); + } else if (root) { + createCSS(root.toCSS(less), sheet, env.lastModified); + } + }); + } + }, less.poll); + } else { + less.optimization = 3; + } +} + +// +// Watch mode +// +less.watch = function () { + if (!less.watchMode ){ + less.env = 'development'; + initRunningMode(); + } + return this.watchMode = true; +}; + +less.unwatch = function () {clearInterval(less.watchTimer); return this.watchMode = false; }; + +if (/!watch/.test(location.hash)) { + less.watch(); +} + +if (less.env != 'development') { + try { + cache = (typeof(window.localStorage) === 'undefined') ? null : window.localStorage; + } catch (_) {} +} + +// +// Get all tags with the 'rel' attribute set to "stylesheet/less" +// +var links = document.getElementsByTagName('link'); + +less.sheets = []; + +for (var i = 0; i < links.length; i++) { + if (links[i].rel === 'stylesheet/less' || (links[i].rel.match(/stylesheet/) && + (links[i].type.match(typePattern)))) { + less.sheets.push(links[i]); + } +} + +// +// With this function, it's possible to alter variables and re-render +// CSS without reloading less-files +// +less.modifyVars = function(record) { + var newVars = ""; + for (var name in record) { + newVars += ((name.slice(0,1) === '@')? '' : '@') + name +': '+ + ((record[name].slice(-1) === ';')? record[name] : record[name] +';'); + } + less.refresh(false, newVars); +}; + +less.refresh = function (reload, newVars) { + var startTime, endTime; + startTime = endTime = new Date(); + + loadStyleSheets(function (e, root, _, sheet, env) { + if (e) { + return error(e, sheet.href); + } + if (env.local) { + log("loading " + sheet.href + " from cache.", logLevel.info); + } else { + log("parsed " + sheet.href + " successfully.", logLevel.info); + createCSS(root.toCSS(less), sheet, env.lastModified); + } + log("css for " + sheet.href + " generated in " + (new Date() - endTime) + 'ms', logLevel.info); + if (env.remaining === 0) { + log("css generated in " + (new Date() - startTime) + 'ms', logLevel.info); + } + endTime = new Date(); + }, reload, newVars); + + loadStyles(newVars); +}; + +less.refreshStyles = loadStyles; + +less.Parser.fileLoader = loadFile; + +less.refresh(less.env === 'development'); +// amd.js +// +// Define Less as an AMD module. +if (typeof define === "function" && define.amd) { + define(function () { return less; } ); +} +})(window); diff --git a/dist/less-1.5.0.min.js b/dist/less-1.5.0.min.js new file mode 100644 index 00000000..c94eb0fc --- /dev/null +++ b/dist/less-1.5.0.min.js @@ -0,0 +1,11 @@ +/* + * LESS - Leaner CSS v1.5.0 + * http://lesscss.org + * + * Copyright (c) 2009-2013, Alexis Sellier + * Licensed under the Apache 2.0 License. + * + * @licence + */(function(e,t){function n(t){return e.less[t.split("/")[1]]}function h(e,t){r.env=="development"&&typeof console!="undefined"&&r.logLevel>=t&&console.log("less: "+e)}function p(e){return e.replace(/^[a-z-]+:\/+?[^\/]+/,"").replace(/^\//,"").replace(/\.[a-zA-Z]+$/,"").replace(/[^\.\w-]+/g,"-").replace(/\./g,":")}function d(e,n){var r="{line} {content}",i=e.filename||n,s=[],u=(e.type||"Syntax")+"Error: "+(e.message||"There is an error in your .less file")+" in "+i+" ",a=function(e,n,i){e.extract[n]!==t&&s.push(r.replace(/\{line\}/,(parseInt(e.line,10)||0)+(n-1)).replace(/\{class\}/,i).replace(/\{content\}/,e.extract[n]))};e.extract?(a(e,0,""),a(e,1,"line"),a(e,2,""),u+="on line "+e.line+", column "+(e.column+1)+":\n"+s.join("\n")):e.stack&&(u+=e.stack),h(u,o.errors)}function v(e,t,n){var r=t.href||"",i="less:"+(t.title||p(r)),s=document.getElementById(i),u=!1,a=document.createElement("style");a.setAttribute("type","text/css"),t.media&&a.setAttribute("media",t.media),a.id=i;if(a.styleSheet)try{a.styleSheet.cssText=e}catch(f){throw new Error("Couldn't reassign styleSheet.cssText.")}else a.appendChild(document.createTextNode(e)),u=s!==null&&s.childNodes.length>0&&a.childNodes.length>0&&s.firstChild.nodeValue===a.firstChild.nodeValue;var c=document.getElementsByTagName("head")[0];if(s===null||u===!1){var d=t&&t.nextSibling||null;d?d.parentNode.insertBefore(a,d):c.appendChild(a)}s&&u===!1&&s.parentNode.removeChild(s);if(n&&l){h("saving "+r+" to cache.",o.info);try{l.setItem(r,e),l.setItem(r+":timestamp",n)}catch(f){h("failed to save",o.errors)}}}function m(e,n){var i="less-error-message:"+p(n||""),s='
  • {content}
  • ',o=document.createElement("div"),u,a,f=[],l=e.filename||n,c=l.match(/([^\/]+(\?.*)?)$/)[1];o.id=i,o.className="less-error-message",a="

    "+(e.type||"Syntax")+"Error: "+(e.message||"There is an error in your .less file")+"

    "+'

    in '+c+" ";var h=function(e,n,r){e.extract[n]!==t&&f.push(s.replace(/\{line\}/,(parseInt(e.line,10)||0)+(n-1)).replace(/\{class\}/,r).replace(/\{content\}/,e.extract[n]))};e.extract?(h(e,0,""),h(e,1,"line"),h(e,2,""),a+="on line "+e.line+", column "+(e.column+1)+":

    "+""):e.stack&&(a+="
    "+e.stack.split("\n").slice(1).join("
    ")),o.innerHTML=a,v([".less-error-message ul, .less-error-message li {","list-style-type: none;","margin-right: 15px;","padding: 4px 0;","margin: 0;","}",".less-error-message label {","font-size: 12px;","margin-right: 15px;","padding: 4px 0;","color: #cc7777;","}",".less-error-message pre {","color: #dd6666;","padding: 4px 0;","margin: 0;","display: inline-block;","}",".less-error-message pre.line {","color: #ff0000;","}",".less-error-message h3 {","font-size: 20px;","font-weight: bold;","padding: 15px 0 5px 0;","margin: 0;","}",".less-error-message a {","color: #10a","}",".less-error-message .error {","color: red;","font-weight: bold;","padding-bottom: 2px;","border-bottom: 1px dashed red;","}"].join("\n"),{title:"error-message"}),o.style.cssText=["font-family: Arial, sans-serif","border: 1px solid #e00","background-color: #eee","border-radius: 5px","-webkit-border-radius: 5px","-moz-border-radius: 5px","color: #e00","padding: 15px","margin-bottom: 15px"].join(";"),r.env=="development"&&(u=setInterval(function(){document.body&&(document.getElementById(i)?document.body.replaceChild(o,document.getElementById(i)):document.body.insertBefore(o,document.body.firstChild),clearInterval(u))},10))}function g(e,t){!r.errorReporting||r.errorReporting==="html"?m(e,t):r.errorReporting==="console"?d(e,t):typeof r.errorReporting=="function"&&r.errorReporting("add",e,t)}function y(e){var t=document.getElementById("less-error-message:"+p(e));t&&t.parentNode.removeChild(t)}function b(e){}function w(e){!r.errorReporting||r.errorReporting==="html"?y(e):r.errorReporting==="console"?b(e):typeof r.errorReporting=="function"&&r.errorReporting("remove",e)}function E(e){var t=document.getElementsByTagName("style"),n;for(var i=0;i0&&(s.splice(o-1,2),o-=2)}return i.hostPart=r[1],i.directories=s,i.path=r[1]+s.join("/"),i.fileUrl=i.path+(r[4]||""),i.url=i.fileUrl+(r[5]||""),i}function x(e,t){var n=S(e),r=S(t),i,s,o,u,a="";if(n.hostPart!==r.hostPart)return"";s=Math.max(r.directories.length,n.directories.length);for(i=0;i=200&&t.status<300?n(t.responseText,t.getResponseHeader("Last-Modified")):typeof r=="function"&&r(t.status,e)}var u=T(),a=s?r.fileAsync:r.async;typeof u.overrideMimeType=="function"&&u.overrideMimeType("text/css"),h("XHR: Getting '"+e+"'",o.info),u.open("GET",e,a),u.setRequestHeader("Accept",t||"text/x-less, text/css; q=0.9, */*; q=0.5"),u.send(null),s&&!r.fileAsync?u.status===0||u.status>=200&&u.status<300?n(u.responseText):i(u.status,e):a?u.onreadystatechange=function(){u.readyState==4&&f(u,n,i)}:f(u,n,i)}function C(t,n,i,s,o){n&&n.currentDirectory&&!/^([a-z-]+:)?\//.test(t)&&(t=n.currentDirectory+t);var u=S(t,e.location.href),a=u.url,f={currentDirectory:u.path,filename:a};n?(f.entryPath=n.entryPath,f.rootpath=n.rootpath,f.rootFilename=n.rootFilename,f.relativeUrls=n.relativeUrls):(f.entryPath=u.path,f.rootpath=r.rootpath||u.path,f.rootFilename=a,f.relativeUrls=s.relativeUrls),f.relativeUrls&&(s.rootpath?f.rootpath=S(s.rootpath+x(u.path,f.entryPath)).path:f.rootpath=u.path);if(s.useFileCache&&c[a]){try{var l=c[a];o&&(l+="\n"+o),i(null,l,a,f,{lastModified:new Date})}catch(h){i(h,null,a)}return}N(a,s.mime,function(e,t){c[a]=e;try{i(null,e,a,f,{lastModified:t})}catch(n){i(n,null,a)}},function(e,t){i({type:"File",message:"'"+t+"' wasn't found ("+e+")"},null,a)})}function k(e,t,n,i,s){var o=new r.tree.parseEnv(r);o.mime=e.type,s&&(o.useFileCache=!0),C(e.href,null,function(s,u,a,f,c){if(c){c.remaining=i;var h=l&&l.getItem(a),p=l&&l.getItem(a+":timestamp");if(!n&&p&&c.lastModified&&(new Date(c.lastModified)).valueOf()===(new Date(p)).valueOf()){v(h,e),c.local=!0,t(null,null,u,e,c,a);return}}w(a),u?(o.currentFileInfo=f,(new r.Parser(o)).parse(u,function(n,r){if(n)return t(n,null,null,e);try{t(n,r,u,e,c,a)}catch(n){t(n,null,null,e)}})):t(s,null,null,e,c,a)},o,s)}function L(e,t,n){for(var i=0;ip&&(h[a]=h[a].slice(u-p),p=u)}function b(e){var t=e.charCodeAt(0);return t===32||t===10||t===9}function w(e){var t,n;if(e instanceof Function)return e.call(d.parsers);if(typeof e=="string")t=o.charAt(u)===e?e:null,n=1,y();else{y();if(!(t=e.exec(h[a])))return null;n=t[0].length}if(t)return E(n),typeof t=="string"?t:t.length===1?t[0]:t}function E(e){var t=u,n=a,r=u+h[a].length,i=u+=e;while(u=0&&t.charAt(n)!=="\n")i++;return typeof e=="number"&&(r=(t.slice(0,e).match(/\n/g)||"").length),{line:r,column:i}}function k(e,t,i){var s=i.currentFileInfo.filename;return r.mode!=="browser"&&r.mode!=="rhino"&&(s=n("path").resolve(s)),{lineNumber:C(e,t).line+1,fileName:s}}function L(e,t){var n=N(e,t),r=C(e.index,n),i=r.line,s=r.column,o=e.call&&C(e.call,n).line,u=n.split("\n");this.type=e.type||"Syntax",this.message=e.message,this.filename=e.filename||t.currentFileInfo.filename,this.index=e.index,this.line=typeof i=="number"?i+1:null,this.callLine=o+1,this.callExtract=u[o],this.stack=e.stack,this.column=s,this.extract=[u[i-1],u[i],u[i+1]]}var o,u,a,f,l,c,h,p,d;s instanceof i.parseEnv||(s=new i.parseEnv(s));var v=this.imports={paths:s.paths||[],queue:[],files:s.files,contents:s.contents,mime:s.mime,error:null,push:function(e,t,n,o){var u=this;this.queue.push(e);var a=function(t,n,r){u.queue.splice(u.queue.indexOf(e),1);var i=r in u.files;u.files[r]=n,t&&!u.error&&(u.error=t),o(t,n,i)};r.Parser.importer?r.Parser.importer(e,t,a,s):r.Parser.fileLoader(e,t,function(e,o,u,f){if(e){a(e);return}var l=new i.parseEnv(s);l.currentFileInfo=f,l.processImports=!1,l.contents[u]=o;if(t.reference||n.reference)f.reference=!0;n.inline?a(null,o,u):(new r.Parser(l)).parse(o,function(e,t){a(e,t,u)})},s)}};return L.prototype=new Error,L.prototype.constructor=L,this.env=s=s||{},this.optimization="optimization"in this.env?this.env.optimization:1,d={imports:v,parse:function(e,t){var f,l,v,m=null;u=a=p=c=0,o=e.replace(/\r\n/g,"\n"),o=o.replace(/^\uFEFF/,""),d.imports.contents[s.currentFileInfo.filename]=o,h=function(e){var t=0,n=/(?:@\{[\w-]+\}|[^"'`\{\}\/\(\)\\])+/g,r=/\/\*(?:[^*]|\*+[^\/*])*\*+\/|\/\/.*/g,i=/"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'|`((?:[^`]|\\.)*)`/g,u=0,a,f=e[0],l;for(var c=0,h,p;c0?"missing closing `}`":"missing opening `{`",filename:s.currentFileInfo.filename},s)),e.map(function(e){return e.join("")})}([[]]);if(m)return t(new L(m,s));try{f=new i.Ruleset([],w(this.parsers.primary)),f.root=!0,f.firstRoot=!0}catch(g){return t(new L(g,s))}f.toCSS=function(e){return function(t,o){t=t||{};var u,a,f=new i.evalEnv(t);typeof o=="object"&&!Array.isArray(o)&&(o=Object.keys(o).map(function(e){var t=o[e];return t instanceof i.Value||(t instanceof i.Expression||(t=new i.Expression([t])),t=new i.Value([t])),new i.Rule("@"+e,t,!1,null,0)}),f.frames=[new i.Ruleset(null,o)]);try{u=e.call(this,f),(new i.joinSelectorVisitor).run(u),(new i.processExtendsVisitor).run(u),(new i.toCSSVisitor({compress:Boolean(t.compress)})).run(u),t.sourceMap&&(u=new i.sourceMapOutput({writeSourceMap:t.writeSourceMap,rootNode:u,contentsMap:d.imports.contents,sourceMapFilename:t.sourceMapFilename,outputFilename:t.sourceMapOutputFilename,sourceMapBasepath:t.sourceMapBasepath,sourceMapRootpath:t.sourceMapRootpath,outputSourceFiles:t.outputSourceFiles})),a=u.toCSS({compress:Boolean(t.compress),dumpLineNumbers:s.dumpLineNumbers,strictUnits:Boolean(t.strictUnits)})}catch(l){throw new L(l,s)}return t.cleancss&&r.mode==="node"?n("clean-css").process(a):t.compress?a.replace(/(^(\s)+)|((\s)+$)/g,""):a}}(f.eval);if(u57||t<43||t===47||t==44)return;if(e=w(/^([+-]?\d*\.?\d+)(%|[a-z]+)?/))return new i.Dimension(e[1],e[2])},unicodeDescriptor:function(){var e;if(e=w(/^U\+[0-9a-fA-F?]+(\-[0-9a-fA-F?]+)?/))return new i.UnicodeDescriptor(e[0])},javascript:function(){var e,n=u,r;o.charAt(n)==="~"&&(n++,r=!0);if(o.charAt(n)!=="`")return;s.javascriptEnabled!==t&&!s.javascriptEnabled&&x("You are using JavaScript, which has been disabled."),r&&w("~");if(e=w(/^`([^`]*)`/))return new i.JavaScript(e[1],u,r)}},variable:function(){var e;if(o.charAt(u)==="@"&&(e=w(/^(@[\w-]+)\s*:/)))return e[1]},extend:function(e){var t,n,r=u,s,o=[];if(!w(e?/^&:extend\(/:/^:extend\(/))return;do{s=null,t=[];for(;;){s=w(/^(all)(?=\s*(\)|,))/);if(s)break;n=w(this.element);if(!n)break;t.push(n)}s=s&&s[1],o.push(new i.Extend(new i.Selector(t),s,r))}while(w(","));return S(/^\)/),e&&S(/^;/),o},extendRule:function(){return this.extend(!0)},mixin:{call:function(){var e=[],t,n,r,a=u,f=o.charAt(u),l=!1;if(f!=="."&&f!=="#")return;m();while(t=w(/^[#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/))e.push(new i.Element(n,t,u,s.currentFileInfo)),n=w(">");w("(")&&(r=this.mixin.args.call(this,!0).args,S(")")),r=r||[],w(this.important)&&(l=!0);if(e.length>0&&(w(";")||T("}")))return new i.mixin.Call(e,r,a,s.currentFileInfo,l);g()},args:function(e){var t=[],n=[],r,s=[],a,f,l,c,h,p={args:null,variadic:!1};for(;;){if(e)h=w(this.expression);else{w(this.comments);if(o.charAt(u)==="."&&w(/^\.{3}/)){p.variadic=!0,w(";")&&!r&&(r=!0),(r?n:s).push({variadic:!0});break}h=w(this.entities.variable)||w(this.entities.literal)||w(this.entities.keyword)}if(!h)break;l=null,h.throwAwayComments&&h.throwAwayComments(),c=h;var d=null;e?h.value.length==1&&(d=h.value[0]):d=h;if(d&&d instanceof i.Variable)if(w(":"))t.length>0&&(r&&x("Cannot mix ; and , as delimiter types"),a=!0),c=S(this.expression),l=f=d.name;else{if(!e&&w(/^\.{3}/)){p.variadic=!0,w(";")&&!r&&(r=!0),(r?n:s).push({name:h.name,variadic:!0});break}e||(f=l=d.name,c=null)}c&&t.push(c),s.push({name:l,value:c});if(w(","))continue;if(w(";")||r)a&&x("Cannot mix ; and , as delimiter types"),r=!0,t.length>1&&(c=new i.Value(t)),n.push({name:f,value:c}),f=null,t=[],a=!1}return p.args=r?n:s,p},definition:function(){var e,t=[],n,r,s,a=!1;if(o.charAt(u)!=="."&&o.charAt(u)!=="#"||T(/^[^{]*\}/))return;m();if(n=w(/^([#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/)){e=n[1];var f=this.mixin.args.call(this,!1);t=f.args,a=f.variadic,w(")")||(c=u,g()),w(this.comments),w(/^when/)&&(s=S(this.conditions,"expected condition")),r=w(this.block);if(r)return new i.mixin.Definition(e,t,r,s,a);g()}}},entity:function(){return w(this.entities.literal)||w(this.entities.variable)||w(this.entities.url)||w(this.entities.call)||w(this.entities.keyword)||w(this.entities.javascript)||w(this.comment)},end:function(){return w(";")||T("}")},alpha:function(){var e;if(!w(/^\(opacity=/i))return;if(e=w(/^\d+/)||w(this.entities.variable))return S(")"),new i.Alpha(e)},element:function(){var e,t,n;t=w(this.combinator),e=w(/^(?:\d+\.\d+|\d+)%/)||w(/^(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/)||w("*")||w("&")||w(this.attribute)||w(/^\([^()@]+\)/)||w(/^[\.#](?=@)/)||w(this.entities.variableCurly),e||w("(")&&(n=w(this.selector))&&w(")")&&(e=new i.Paren(n));if(e)return new i.Element(t,e,u,s.currentFileInfo)},combinator:function(){var e=o.charAt(u);if(e===">"||e==="+"||e==="~"||e==="|"){u++;while(o.charAt(u).match(/\s/))u++;return new i.Combinator(e)}return o.charAt(u-1).match(/\s/)?new i.Combinator(" "):new i.Combinator(null)},lessSelector:function(){return this.selector(!0)},selector:function(e){var t,n=[],r,a,f=[],l,c;while(e&&(a=w(this.extend))||e&&(l=w(/^when/))||(t=w(this.element))){l?c=S(this.conditions,"expected condition"):c?x("CSS guard can only be used at the end of selector"):a?f.push.apply(f,a):(f.length&&x("Extend can only be used at the end of selector"),r=o.charAt(u),n.push(t),t=null);if(r==="{"||r==="}"||r===";"||r===","||r===")")break}if(n.length>0)return new i.Selector(n,f,c,u,s.currentFileInfo);f.length&&x("Extend must be used to extend a selector, it cannot be used on its own")},attribute:function(){var e,t,n;if(!w("["))return;(e=w(this.entities.variableCurly))||(e=S(/^(?:[_A-Za-z0-9-\*]*\|)?(?:[_A-Za-z0-9-]|\\.)+/));if(n=w(/^[|~*$^]?=/))t=w(this.entities.quoted)||w(/^[\w-]+/)||w(this.entities.variableCurly);return S("]"),new i.Attribute(e,n,t)},block:function(){var e;if(w("{")&&(e=w(this.primary))&&w("}"))return e},ruleset:function(){var e=[],t,n,r;m(),s.dumpLineNumbers&&(r=k(u,o,s));while(t=w(this.lessSelector)){e.push(t),w(this.comments);if(!w(","))break;t.condition&&x("Guards are only currently allowed on a single selector"),w(this.comments)}if(e.length>0&&(n=w(this.block))){var a=new i.Ruleset(e,n,s.strictImports);return s.dumpLineNumbers&&(a.debugInfo=r),a}c=u,g()},rule:function(e){var t,n,r=o.charAt(u),a,f=!1;m();if(r==="."||r==="#"||r==="&")return;if(t=w(this.variable)||w(this.ruleProperty)){n=!e&&(s.compress||t.charAt(0)==="@")?w(this.value)||w(this.anonymousValue):w(this.anonymousValue)||w(this.value),a=w(this.important),t[t.length-1]==="+"&&(f=!0,t=t.substr(0,t.length-1));if(n&&w(this.end))return new i.Rule(t,n,a,f,l,s.currentFileInfo);c=u,g();if(n&&!e)return this.rule(!0)}},anonymousValue:function(){var e;if(e=/^([^@+\/'"*`(;{}-]*);/.exec(h[a]))return u+=e[0].length-1,new i.Anonymous(e[1])},"import":function(){var e,t,n=u;m();var r=w(/^@import?\s+/),o=(r?w(this.importOptions):null)||{};if(r&&(e=w(this.entities.quoted)||w(this.entities.url))){t=w(this.mediaFeatures);if(w(";"))return t=t&&new i.Value(t),new i.Import(e,t,o,n,s.currentFileInfo)}g()},importOptions:function(){var e,t={},n,r;if(!w("("))return null;do if(e=w(this.importOption)){n=e,r=!0;switch(n){case"css":n="less",r=!1;break;case"once":n="multiple",r=!1}t[n]=r;if(!w(","))break}while(e);return S(")"),t},importOption:function(){var e=w(/^(less|css|multiple|once|inline|reference)/);if(e)return e[1]},mediaFeature:function(){var e,t,n=[];do if(e=w(this.entities.keyword))n.push(e);else if(w("(")){t=w(this.property),e=w(this.value);if(!w(")"))return null;if(t&&e)n.push(new i.Paren(new i.Rule(t,e,null,null,u,s.currentFileInfo,!0)));else{if(!e)return null;n.push(new i.Paren(e))}}while(e);if(n.length>0)return new i.Expression(n)},mediaFeatures:function(){var e,t=[];do if(e=w(this.mediaFeature)){t.push(e);if(!w(","))break}else if(e=w(this.entities.variable)){t.push(e);if(!w(","))break}while(e);return t.length>0?t:null},media:function(){var e,t,n,r;s.dumpLineNumbers&&(r=k(u,o,s));if(w(/^@media/)){e=w(this.mediaFeatures);if(t=w(this.block))return n=new i.Media(t,e,u,s.currentFileInfo),s.dumpLineNumbers&&(n.debugInfo=r),n}},directive:function(){var e,t,n,r,a,f,l;if(o.charAt(u)!=="@")return;if(t=w(this["import"])||w(this.media))return t;m(),e=w(/^@[a-z-]+/);if(!e)return;r=e,e.charAt(1)=="-"&&e.indexOf("-",2)>0&&(r="@"+e.slice(e.indexOf("-",2)+1));switch(r){case"@font-face":a=!0;break;case"@viewport":case"@top-left":case"@top-left-corner":case"@top-center":case"@top-right":case"@top-right-corner":case"@bottom-left":case"@bottom-left-corner":case"@bottom-center":case"@bottom-right":case"@bottom-right-corner":case"@left-top":case"@left-middle":case"@left-bottom":case"@right-top":case"@right-middle":case"@right-bottom":a=!0;break;case"@page":case"@document":case"@supports":case"@keyframes":a=!0,f=!0;break;case"@namespace":l=!0}f&&(e+=" "+(w(/^[^{]+/)||"").trim());if(a){if(n=w(this.block))return new i.Directive(e,n,u,s.currentFileInfo)}else if((t=l?w(this.expression):w(this.entity))&&w(";")){var c=new i.Directive(e,t,u,s.currentFileInfo);return s.dumpLineNumbers&&(c.debugInfo=k(u,o,s)),c}g()},value:function(){var e,t=[];while(e=w(this.expression)){t.push(e);if(!w(","))break}if(t.length>0)return new i.Value(t)},important:function(){if(o.charAt(u)==="!")return w(/^! *important/)},sub:function(){var e,t;if(w("("))if(e=w(this.addition))return t=new i.Expression([e]),S(")"),t.parens=!0,t},multiplication:function(){var e,t,n,r,s;if(e=w(this.operand)){s=b(o.charAt(u-1));while(!T(/^\/[*\/]/)&&(n=w("/")||w("*"))){if(!(t=w(this.operand)))break;e.parensInOp=!0,t.parensInOp=!0,r=new i.Operation(n,[r||e,t],s),s=b(o.charAt(u-1))}return r||e}},addition:function(){var e,t,n,r,s;if(e=w(this.multiplication)){s=b(o.charAt(u-1));while((n=w(/^[-+]\s+/)||!s&&(w("+")||w("-")))&&(t=w(this.multiplication)))e.parensInOp=!0,t.parensInOp=!0,r=new i.Operation(n,[r||e,t],s),s=b(o.charAt(u-1));return r||e}},conditions:function(){var e,t,n=u,r;if(e=w(this.condition)){while(w(",")&&(t=w(this.condition)))r=new i.Condition("or",r||e,t,n);return r||e}},condition:function(){var e,t,n,r,s=u,o=!1;w(/^not/)&&(o=!0),S("(");if(e=w(this.addition)||w(this.entities.keyword)||w(this.entities.quoted))return(r=w(/^(?:>=|=<|[<=>])/))?(t=w(this.addition)||w(this.entities.keyword)||w(this.entities.quoted))?n=new i.Condition(r,e,t,s,o):x("expected expression"):n=new i.Condition("=",e,new i.Keyword("true"),s,o),S(")"),w(/^and/)?new i.Condition("and",n,w(this.condition)):n},operand:function(){var e,t=o.charAt(u+1);o.charAt(u)==="-"&&(t==="@"||t==="(")&&(e=w("-"));var n=w(this.sub)||w(this.entities.dimension)||w(this.entities.color)||w(this.entities.variable)||w(this.entities.call);return e&&(n.parensInOp=!0,n=new i.Negative(n)),n},expression:function(){var e,t,n=[];while(e=w(this.addition)||w(this.entity))n.push(e),!T(/^\/[\/*]/)&&(t=w("/"))&&n.push(new i.Anonymous(t));if(n.length>0)return new i.Expression(n)},property:function(){var e;if(e=w(/^(\*?-?[_a-zA-Z0-9-]+)\s*:/))return e[1]},ruleProperty:function(){var e;if(e=w(/^(\*?-?[_a-zA-Z0-9-]+)\s*(\+?)\s*:/))return e[1]+(e[2]||"")}}}},function(r){function u(e){return r.functions.hsla(e.h,e.s,e.l,e.a)}function a(e,t){return e instanceof r.Dimension&&e.unit.is("%")?parseFloat(e.value*t/100):f(e)}function f(e){if(e instanceof r.Dimension)return parseFloat(e.unit.is("%")?e.value/100:e.value);if(typeof e=="number")return e;throw{error:"RuntimeError",message:"color functions take numbers as parameters"}}function l(e){return Math.min(1,Math.max(0,e))}r.functions={rgb:function(e,t,n){return this.rgba(e,t,n,1)},rgba:function(e,t,n,i){var s=[e,t,n].map(function(e){return a(e,256)});return i=f(i),new r.Color(s,i)},hsl:function(e,t,n){return this.hsla(e,t,n,1)},hsla:function(e,t,n,r){function i(e){return e=e<0?e+1:e>1?e-1:e,e*6<1?o+(s-o)*e*6:e*2<1?s:e*3<2?o+(s-o)*(2/3-e)*6:o}e=f(e)%360/360,t=l(f(t)),n=l(f(n)),r=l(f(r));var s=n<=.5?n*(t+1):n+t-n*t,o=n*2-s;return this.rgba(i(e+1/3)*255,i(e)*255,i(e-1/3)*255,r)},hsv:function(e,t,n){return this.hsva(e,t,n,1)},hsva:function(e,t,n,r){e=f(e)%360/360*360,t=f(t),n=f(n),r=f(r);var i,s;i=Math.floor(e/60%6),s=e/60-i;var o=[n,n*(1-t),n*(1-s*t),n*(1-(1-s)*t)],u=[[0,3,1],[2,0,1],[1,0,3],[1,2,0],[3,1,0],[0,1,2]];return this.rgba(o[u[i][0]]*255,o[u[i][1]]*255,o[u[i][2]]*255,r)},hue:function(e){return new r.Dimension(Math.round(e.toHSL().h))},saturation:function(e){return new r.Dimension(Math.round(e.toHSL().s*100),"%")},lightness:function(e){return new r.Dimension(Math.round(e.toHSL().l*100),"%")},hsvhue:function(e){return new r.Dimension(Math.round(e.toHSV().h))},hsvsaturation:function(e){return new r.Dimension(Math.round(e.toHSV().s*100),"%")},hsvvalue:function(e){return new r.Dimension(Math.round(e.toHSV().v*100),"%")},red:function(e){return new r.Dimension(e.rgb[0])},green:function(e){return new r.Dimension(e.rgb[1])},blue:function(e){return new r.Dimension(e.rgb[2])},alpha:function(e){return new r.Dimension(e.toHSL().a)},luma:function(e){return new r.Dimension(Math.round(e.luma()*e.alpha*100),"%")},saturate:function(e,t){if(!e.rgb)return null;var n=e.toHSL();return n.s+=t.value/100,n.s=l(n.s),u(n)},desaturate:function(e,t){var n=e.toHSL();return n.s-=t.value/100,n.s=l(n.s),u(n)},lighten:function(e,t){var n=e.toHSL();return n.l+=t.value/100,n.l=l(n.l),u(n)},darken:function(e,t){var n=e.toHSL();return n.l-=t.value/100,n.l=l(n.l),u(n)},fadein:function(e,t){var n=e.toHSL();return n.a+=t.value/100,n.a=l(n.a),u(n)},fadeout:function(e,t){var n=e.toHSL();return n.a-=t.value/100,n.a=l(n.a),u(n)},fade:function(e,t){var n=e.toHSL();return n.a=t.value/100,n.a=l(n.a),u(n)},spin:function(e,t){var n=e.toHSL(),r=(n.h+t.value)%360;return n.h=r<0?360+r:r,u(n)},mix:function(e,t,n){n||(n=new r.Dimension(50));var i=n.value/100,s=i*2-1,o=e.toHSL().a-t.toHSL().a,u=((s*o==-1?s:(s+o)/(1+s*o))+1)/2,a=1-u,f=[e.rgb[0]*u+t.rgb[0]*a,e.rgb[1]*u+t.rgb[1]*a,e.rgb[2]*u+t.rgb[2]*a],l=e.alpha*i+t.alpha*(1-i);return new r.Color(f,l)},greyscale:function(e){return this.desaturate(e,new r.Dimension(100))},contrast:function(e,t,n,r){if(!e.rgb)return null;typeof n=="undefined"&&(n=this.rgba(255,255,255,1)),typeof t=="undefined"&&(t=this.rgba(0,0,0,1));if(t.luma()>n.luma()){var i=n;n=t,t=i}return typeof r=="undefined"?r=.43:r=f(r),e.luma()*e.alphaa.value)l[s]=o}return l.length==1?l[0]:(n=l.map(function(e){return e.toCSS(this.env)}).join(this.env.compress?",":", "),new r.Anonymous((e?"min":"max")+"("+n+")"))},min:function(){return this._minmax(!0,arguments)},max:function(){return this._minmax(!1,arguments)},argb:function(e){return new r.Anonymous(e.toARGB())},percentage:function(e){return new r.Dimension(e.value*100,"%")},color:function(e){if(e instanceof r.Quoted)return new r.Color(e.value.slice(1));throw{type:"Argument",message:"argument must be a string"}},iscolor:function(e){return this._isa(e,r.Color)},isnumber:function(e){return this._isa(e,r.Dimension)},isstring:function(e){return this._isa(e,r.Quoted)},iskeyword:function(e){return this._isa(e,r.Keyword)},isurl:function(e){return this._isa(e,r.URL)},ispixel:function(e){return this.isunit(e,"px")},ispercentage:function(e){return this.isunit(e,"%")},isem:function(e){return this.isunit(e,"em")},isunit:function(e,t){return e instanceof r.Dimension&&e.unit.is(t.value||t)?r.True:r.False},_isa:function(e,t){return e instanceof t?r.True:r.False},multiply:function(e,t){var n=e.rgb[0]*t.rgb[0]/255,r=e.rgb[1]*t.rgb[1]/255,i=e.rgb[2]*t.rgb[2]/255;return this.rgb(n,r,i)},screen:function(e,t){var n=255-(255-e.rgb[0])*(255-t.rgb[0])/255,r=255-(255-e.rgb[1])*(255-t.rgb[1])/255,i=255-(255-e.rgb[2])*(255-t.rgb[2])/255;return this.rgb(n,r,i)},overlay:function(e,t){var n=e.rgb[0]<128?2*e.rgb[0]*t.rgb[0]/255:255-2*(255-e.rgb[0])*(255-t.rgb[0])/255,r=e.rgb[1]<128?2*e.rgb[1]*t.rgb[1]/255:255-2*(255-e.rgb[1])*(255-t.rgb[1])/255,i=e.rgb[2]<128?2*e.rgb[2]*t.rgb[2]/255:255-2*(255-e.rgb[2])*(255-t.rgb[2])/255;return this.rgb(n,r,i)},softlight:function(e,t){var n=t.rgb[0]*e.rgb[0]/255,r=n+e.rgb[0]*(255-(255-e.rgb[0])*(255-t.rgb[0])/255-n)/255;n=t.rgb[1]*e.rgb[1]/255;var i=n+e.rgb[1]*(255-(255-e.rgb[1])*(255-t.rgb[1])/255-n)/255;n=t.rgb[2]*e.rgb[2]/255;var s=n+e.rgb[2]*(255-(255-e.rgb[2])*(255-t.rgb[2])/255-n)/255;return this.rgb(r,i,s)},hardlight:function(e,t){var n=t.rgb[0]<128?2*t.rgb[0]*e.rgb[0]/255:255-2*(255-t.rgb[0])*(255-e.rgb[0])/255,r=t.rgb[1]<128?2*t.rgb[1]*e.rgb[1]/255:255-2*(255-t.rgb[1])*(255-e.rgb[1])/255,i=t.rgb[2]<128?2*t.rgb[2]*e.rgb[2]/255:255-2*(255-t.rgb[2])*(255-e.rgb[2])/255;return this.rgb(n,r,i)},difference:function(e,t){var n=Math.abs(e.rgb[0]-t.rgb[0]),r=Math.abs(e.rgb[1]-t.rgb[1]),i=Math.abs(e.rgb[2]-t.rgb[2]);return this.rgb(n,r,i)},exclusion:function(e,t){var n=e.rgb[0]+t.rgb[0]*(255-e.rgb[0]-e.rgb[0])/255,r=e.rgb[1]+t.rgb[1]*(255-e.rgb[1]-e.rgb[1])/255,i=e.rgb[2]+t.rgb[2]*(255-e.rgb[2]-e.rgb[2])/255;return this.rgb(n,r,i)},average:function(e,t){var n=(e.rgb[0]+t.rgb[0])/2,r=(e.rgb[1]+t.rgb[1])/2,i=(e.rgb[2]+t.rgb[2])/2;return this.rgb(n,r,i)},negation:function(e,t){var n=255-Math.abs(255-t.rgb[0]-e.rgb +[0]),r=255-Math.abs(255-t.rgb[1]-e.rgb[1]),i=255-Math.abs(255-t.rgb[2]-e.rgb[2]);return this.rgb(n,r,i)},tint:function(e,t){return this.mix(this.rgb(255,255,255),e,t)},shade:function(e,t){return this.mix(this.rgb(0,0,0),e,t)},extract:function(e,t){return t=t.value-1,e.value[t]},"data-uri":function(t,i){if(typeof e!="undefined")return(new r.URL(i||t,this.currentFileInfo)).eval(this.env);var s=t.value,o=i&&i.value,u=n("fs"),a=n("path"),f=!1;arguments.length<2&&(o=s),this.env.isPathRelative(o)&&(this.currentFileInfo.relativeUrls?o=a.join(this.currentFileInfo.currentDirectory,o):o=a.join(this.currentFileInfo.entryPath,o));if(arguments.length<2){var l;try{l=n("mime")}catch(c){l=r._mime}s=l.lookup(o);var h=l.charsets.lookup(s);f=["US-ASCII","UTF-8"].indexOf(h)<0,f&&(s+=";base64")}else f=/;base64$/.test(s);var p=u.readFileSync(o),d=32,v=parseInt(p.length/1024,10);if(v>=d){if(this.env.ieCompat!==!1)return this.env.silent||console.warn("Skipped data-uri embedding of %s because its size (%dKB) exceeds IE8-safe %dKB!",o,v,d),(new r.URL(i||t,this.currentFileInfo)).eval(this.env);this.env.silent||console.warn("WARNING: Embedding %s (%dKB) exceeds IE8's data-uri size limit of %dKB!",o,v,d)}p=f?p.toString("base64"):encodeURIComponent(p);var m="'data:"+s+","+p+"'";return new r.URL(new r.Anonymous(m))},"svg-gradient":function(e){function n(){throw{type:"Argument",message:"svg-gradient expects direction, start_color [start_position], [color position,]..., end_color [end_position]"}}arguments.length<3&&n();var i=Array.prototype.slice.call(arguments,1),s,o="linear",u='x="0" y="0" width="1" height="1"',a=!0,f={compress:!1},l,c=e.toCSS(f),h,p,d,v,m;switch(c){case"to bottom":s='x1="0%" y1="0%" x2="0%" y2="100%"';break;case"to right":s='x1="0%" y1="0%" x2="100%" y2="0%"';break;case"to bottom right":s='x1="0%" y1="0%" x2="100%" y2="100%"';break;case"to top right":s='x1="0%" y1="100%" x2="100%" y2="0%"';break;case"ellipse":case"ellipse at center":o="radial",s='cx="50%" cy="50%" r="75%"',u='x="-50" y="-50" width="101" height="101"';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'"}}l='<'+o+'Gradient id="gradient" gradientUnits="userSpaceOnUse" '+s+">";for(h=0;h";l+=""+"';if(a)try{l=(new Buffer(l)).toString("base64")}catch(g){a=!1}return l="'data:image/svg+xml"+(a?";base64":"")+","+l+"'",new r.URL(new r.Anonymous(l))}},r._mime={_types:{".htm":"text/html",".html":"text/html",".gif":"image/gif",".jpg":"image/jpeg",".jpeg":"image/jpeg",".png":"image/png"},lookup:function(e){var i=n("path").extname(e),s=r._mime._types[i];if(s===t)throw new Error('Optional dependency "mime" is required for '+i);return s},charsets:{lookup:function(e){return e&&/^text\//.test(e)?"UTF-8":""}}};var i=[{name:"ceil"},{name:"floor"},{name:"sqrt"},{name:"abs"},{name:"tan",unit:""},{name:"sin",unit:""},{name:"cos",unit:""},{name:"atan",unit:"rad"},{name:"asin",unit:"rad"},{name:"acos",unit:"rad"}],s=function(e,t){return function(n){return t!=null&&(n=n.unify()),this._math(Math[e],t,n)}};for(var o=0;o1?"["+e.value.map(function(e){return e.toCSS(!1)}).join(", ")+"]":e.toCSS(!1)},e.toCSS=function(e){var t=[];return this.genCSS(e,{add:function(e,n){t.push(e)}}),t.join("")},e.outputRuleset=function(e,t,n){t.add(e.compress?"{":" {\n"),e.tabLevel=(e.tabLevel||0)+1;var r=e.compress?"":Array(e.tabLevel+1).join(" "),i=e.compress?"":Array(e.tabLevel).join(" ");for(var s=0;s255?255:e<0?0:e).toString(16),e.length===1?"0"+e:e}).join("")},toHSL:function(){var e=this.rgb[0]/255,t=this.rgb[1]/255,n=this.rgb[2]/255,r=this.alpha,i=Math.max(e,t,n),s=Math.min(e,t,n),o,u,a=(i+s)/2,f=i-s;if(i===s)o=u=0;else{u=a>.5?f/(2-i-s):f/(i+s);switch(i){case e:o=(t-n)/f+(t255?255:e<0?0:e).toString(16),e.length===1?"0"+e:e}).join("")},compare:function(e){return e.rgb?e.rgb[0]===this.rgb[0]&&e.rgb[1]===this.rgb[1]&&e.rgb[2]===this.rgb[2]&&e.alpha===this.alpha?0:-1:-1}}}(n("../tree")),function(e){e.Comment=function(e,t,n,r){this.value=e,this.silent=!!t,this.currentFileInfo=r},e.Comment.prototype={type:"Comment",genCSS:function(t,n){this.debugInfo&&n.add(e.debugInfo(t,this),this.currentFileInfo,this.index),n.add(this.value.trim())},toCSS:e.toCSS,isSilent:function(e){var t=this.currentFileInfo&&this.currentFileInfo.reference&&!this.isReferenced,n=e.compress&&!this.value.match(/^\/\*!/);return this.silent||t||n},eval:function(){return this},markReferenced:function(){this.isReferenced=!0}}}(n("../tree")),function(e){e.Condition=function(e,t,n,r,i){this.op=e.trim(),this.lvalue=t,this.rvalue=n,this.index=r,this.negate=i},e.Condition.prototype={type:"Condition",accept:function(e){this.lvalue=e.visit(this.lvalue),this.rvalue=e.visit(this.rvalue)},eval:function(e){var t=this.lvalue.eval(e),n=this.rvalue.eval(e),r=this.index,i;return i=function(e){switch(e){case"and":return t&&n;case"or":return t||n;default:if(t.compare)i=t.compare(n);else{if(!n.compare)throw{type:"Type",message:"Unable to perform comparison",index:r};i=n.compare(t)}switch(i){case-1:return e==="<"||e==="=<";case 0:return e==="="||e===">="||e==="=<";case 1:return e===">"||e===">="}}}(this.op),this.negate?!i:i}}}(n("../tree")),function(e){e.Dimension=function(n,r){this.value=parseFloat(n),this.unit=r&&r instanceof e.Unit?r:new e.Unit(r?[r]:t)},e.Dimension.prototype={type:"Dimension",accept:function(e){this.unit=e.visit(this.unit)},eval:function(e){return this},toColor:function(){return new e.Color([this.value,this.value,this.value])},genCSS:function(e,t){if(e&&e.strictUnits&&!this.unit.isSingular())throw new Error("Multiple units in dimension. Correct the units or use the unit function. Bad unit: "+this.unit.toString());var n=this.value,r=String(n);n!==0&&n<1e-6&&n>-0.000001&&(r=n.toFixed(20).replace(/0+$/,""));if(e&&e.compress){if(n===0&&this.unit.isLength()){t.add(r);return}n>0&&n<1&&(r=r.substr(1))}t.add(r),this.unit.genCSS(e,t)},toCSS:e.toCSS,operate:function(t,n,r){var i=e.operate(t,n,this.value,r.value),s=this.unit.clone();if(n==="+"||n==="-"){if(s.numerator.length===0&&s.denominator.length===0)s.numerator=r.unit.numerator.slice(0),s.denominator=r.unit.denominator.slice(0);else if(r.unit.numerator.length!==0||s.denominator.length!==0){r=r.convertTo(this.unit.usedUnits());if(t.strictUnits&&r.unit.toString()!==s.toString())throw new Error("Incompatible units. Change the units or use the unit function. Bad units: '"+s.toString()+"' and '"+r.unit.toString()+"'.");i=e.operate(t,n,this.value,r.value)}}else n==="*"?(s.numerator=s.numerator.concat(r.unit.numerator).sort(),s.denominator=s.denominator.concat(r.unit.denominator).sort(),s.cancel()):n==="/"&&(s.numerator=s.numerator.concat(r.unit.denominator).sort(),s.denominator=s.denominator.concat(r.unit.numerator).sort(),s.cancel());return new e.Dimension(i,s)},compare:function(t){if(t instanceof e.Dimension){var n=this.unify(),r=t.unify(),i=n.value,s=r.value;return s>i?-1:s=1?t.add(this.numerator[0]):this.denominator.length>=1?t.add(this.denominator[0]):(!e||!e.strictUnits)&&this.backupUnit&&t.add(this.backupUnit)},toCSS:e.toCSS,toString:function(){var e,t=this.numerator.join("*");for(e=0;e0)for(n=0;n":" > ","|":"|"},_outputMapCompressed:{"":""," ":" ",":":" :","+":"+","~":"~",">":">","|":"|"},genCSS:function(e,t){t.add((e.compress?this._outputMapCompressed:this._outputMap)[this.value])},toCSS:e.toCSS}}(n("../tree")),function(e){e.Expression=function(e){this.value=e},e.Expression.prototype={type:"Expression",accept:function(e){this.value=e.visit(this.value)},eval:function(t){var n,r=this.parens&&!this.parensInOp,i=!1;return r&&t.inParenthesis(),this.value.length>1?n=new e.Expression(this.value.map(function(e){return e.eval(t)})):this.value.length===1?(this.value[0].parens&&!this.value[0].parensInOp&&(i=!0),n=this.value[0].eval(t)):n=this,r&&t.outOfParenthesis(),this.parens&&this.parensInOp&&!t.isMathOn()&&!i&&(n=new e.Paren(n)),n},genCSS:function(e,t){for(var n=0;n1){var r=this.emptySelectors();n=new e.Ruleset(r,t.mediaBlocks),n.multiMedia=!0}return delete t.mediaBlocks,delete t.mediaPath,n},evalNested:function(t){var n,r,i=t.mediaPath.concat([this]);for(n=0;n0;n--)t.splice(n,0,new e.Anonymous("and"));return new e.Expression(t)})),new e.Ruleset([],[])},permute:function(e){if(e.length===0)return[];if(e.length===1)return e[0];var t=[],n=this.permute(e.slice(1));for(var r=0;r0){c=!0;for(a=0;athis.params.length)return!1;if(this.required>0&&n>this.params.length)return!1}r=Math.min(n,this.arity);for(var i=0;is.selectors[o].elements.length?Array.prototype.push.apply(r,s.find(new e.Selector(t.elements.slice(1)),n)):r.push(s);break}}),this._lookups[s]=r)},genCSS:function(t,n){var r,i,s=[],o=[],u,a,f=!0,l;t.tabLevel=t.tabLevel||0,this.root||t.tabLevel++;var c=t.compress?"":Array(t.tabLevel+1).join(" "),h=t.compress?"":Array(t.tabLevel).join(" ");for(r=0;r0)for(i=0;i0&&this.mergeElementsOnToSelectors(g,a);for(s=0;s0&&(l[0].elements=l[0].elements.slice(0),l[0].elements.push(new e.Element(f.combinator,"",0,f.index,f.currentFileInfo))),y.push(l);else for(o=0;o0?(h=l.slice(0),m=h.pop(),d=r.createDerived(m.elements.slice(0)),v=!1):d=r.createDerived([]),c.length>1&&(p=p.concat(c.slice(1))),c.length>0&&(v=!1,d.elements.push(new e.Element(f.combinator,c[0].elements[0].value,f.index,f.currentFileInfo)),d.elements=d.elements.concat(c[0].elements.slice(1))),v||h.push(d),h=h.concat(p),y.push(h)}a=y,g=[]}}g.length>0&&this.mergeElementsOnToSelectors(g,a);for(i=0;i0&&t.push(a[i])},mergeElementsOnToSelectors:function(t,n){var r,i;if(n.length===0){n.push([new e.Selector(t)]);return}for(r=0;r0?i[i.length-1]=i[i.length-1].createDerived(i[i.length-1].elements.concat(t)):i.push(new e.Selector(t))}}}(n("../tree")),function(e){e.Selector=function(e,t,n,r,i,s){this.elements=e,this.extendList=t||[],this.condition=n,this.currentFileInfo=i||{},this.isReferenced=s,n||(this.evaldCondition=!0)},e.Selector.prototype={type:"Selector",accept:function(e){this.elements=e.visit(this.elements),this.extendList=e.visit(this.extendList),this.condition=e.visit(this.condition)},createDerived:function(t,n,r){r=r!=null?r:this.evaldCondition;var i=new e.Selector(t,n||this.extendList,this.condition,this.index,this.currentFileInfo,this.isReferenced);return i.evaldCondition=r,i},match:function(e){var t=this.elements,n=t.length,r,i,s,o;r=e.elements.slice(e.elements.length&&e.elements[0].value==="&"?1:0),i=r.length,s=Math.min(n,i);if(i===0||n0&&t.accept(this._visitor),n.visitDeeper=!1,this._mergeRules(t.rules),this._removeDuplicateRules(t.rules),t.rules.length>0&&t.paths.length>0&&i.splice(0,0,t)}else t.accept(this._visitor),n.visitDeeper=!1,(t.firstRoot||t.rules.length>0)&&i.splice(0,0,t);return i.length===1?i[0]:i},_removeDuplicateRules:function(t){var n={},r,i,s;for(s=t.length-1;s>=0;s--){i=t[s];if(i instanceof e.Rule)if(!n[i.name])n[i.name]=i;else{r=n[i.name],r instanceof e.Rule&&(r=n[i.name]=[n[i.name].toCSS(this._env)]);var o=i.toCSS(this._env);r.indexOf(o)!==-1?t.splice(s,1):r.push(o)}}},_mergeRules:function(t){var n={},r,i,s;for(var o=0;o1&&(i=r[0],i.value=new e.Value(r.map(function(e){return e.value})))})}}}(n("./tree")),function(e){e.extendFinderVisitor=function(){this._visitor=new e.visitor(this),this.contexts=[],this.allExtendsStack=[[]]},e.extendFinderVisitor.prototype={run:function(e){return e=this._visitor.visit(e),e.allExtends=this.allExtendsStack[0],e},visitRule:function(e,t){t.visitDeeper=!1},visitMixinDefinition:function(e,t){t.visitDeeper=!1},visitRuleset:function(t,n){if(t.root)return;var r,i,s,o=[],u;for(r=0;r100){var d="{unable to calculate}",v="{unable to calculate}";try{d=u[0].selfSelectors[0].toCSS(),v=u[0].selector.toCSS()}catch(m){}throw{message:"extend circular reference detected. One of the circular extends is currently:"+d+":extend("+v+")"}}return u.concat(f.doExtendChaining(u,n,r+1))}return u},inInheritanceChain:function(e,t){if(e===t)return!0;if(t.parents){if(this.inInheritanceChain(e,t.parents[0]))return!0;if(this.inInheritanceChain(e,t.parents[1]))return!0}return!1},visitRule:function(e,t){t.visitDeeper=!1},visitMixinDefinition:function(e,t){t.visitDeeper=!1},visitSelector:function(e,t){t.visitDeeper=!1},visitRuleset:function(e,t){if(e.root)return;var n,r,i,s=this.allExtendsStack[this.allExtendsStack.length-1],o=[],u=this,a;for(i=0;i0&&f[c.matched].combinator.value!==o?c=null:c.matched++,c&&(c.finished=c.matched===f.length,c.finished&&!e.allowAfter&&(i+1i&&s>0&&(o[o.length-1].elements=o[o.length-1].elements.concat(n[i].elements.slice(s)),s=0,i++),c=a.elements.slice(s,l.index).concat([f]).concat(r.elements.slice(1)),i===l.pathIndex&&u>0?o[o.length-1].elements=o[o.length-1].elements.concat(c):(o=o.concat(n.slice(i,l.pathIndex)),o.push(new e.Selector(c))),i=l.endPathIndex,s=l.endPathElementIndex,s>=n[i].elements.length&&(s=0,i++);return i0&&(o[o.length-1].elements=o[o.length-1].elements.concat(n[i].elements.slice(s)),s=0,i++),o=o.concat(n.slice(i,n.length)),o},visitRulesetOut:function(e){},visitMedia:function(e,t){var n=e.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]);n=n.concat(this.doExtendChaining(n,e.allExtends)),this.allExtendsStack.push(n)},visitMediaOut:function(e){this.allExtendsStack.length=this.allExtendsStack.length-1},visitDirective:function(e,t){var n=e.allExtends.concat(this.allExtendsStack[this.allExtendsStack.length-1]);n=n.concat(this.doExtendChaining(n,e.allExtends)),this.allExtendsStack.push(n)},visitDirectiveOut:function(e){this.allExtendsStack.length=this.allExtendsStack.length-1}}}(n("./tree"));var s=/^(file|chrome(-extension)?|resource|qrc|app):/.test(location.protocol);r.env=r.env||(location.hostname=="127.0.0.1"||location.hostname=="0.0.0.0"||location.hostname=="localhost"||location.port.length>0||s?"development":"production");var o={info:2,errors:1,none:0};r.logLevel=typeof r.logLevel!="undefined"?r.logLevel:o.info,r.async=r.async||!1,r.fileAsync=r.fileAsync||!1,r.poll=r.poll||(s?1e3:1500);if(r.functions)for(var u in r.functions)r.tree.functions[u]=r.functions[u];var a=/!dumpLineNumbers:(comments|mediaquery|all)/.exec(location.hash);a&&(r.dumpLineNumbers=a[1]);var f=/^text\/(x-)?less$/,l=null,c={};r.watch=function(){return r.watchMode||(r.env="development",A()),this.watchMode=!0},r.unwatch=function(){return clearInterval(r.watchTimer),this.watchMode=!1},/!watch/.test(location.hash)&&r.watch();if(r.env!="development")try{l=typeof e.localStorage=="undefined"?null:e.localStorage}catch(O){}var M=document.getElementsByTagName("link");r.sheets=[];for(var _=0;_ tags with the 'rel' attribute set to "stylesheet/less" -// -var links = document.getElementsByTagName('link'); var typePattern = /^text\/(x-)?less$/; +var cache = null; +var fileCache = {}; -less.sheets = []; - -for (var i = 0; i < links.length; i++) { - if (links[i].rel === 'stylesheet/less' || (links[i].rel.match(/stylesheet/) && - (links[i].type.match(typePattern)))) { - less.sheets.push(links[i]); +function log(str, level) { + if (less.env == 'development' && typeof(console) !== 'undefined' && less.logLevel >= level) { + console.log('less: ' + str); } } -// -// With this function, it's possible to alter variables and re-render -// CSS without reloading less-files -// -var session_cache = ''; -less.modifyVars = function(record) { - var str = session_cache; - for (var name in record) { - str += ((name.slice(0,1) === '@')? '' : '@') + name +': '+ - ((record[name].slice(-1) === ';')? record[name] : record[name] +';'); - } - new(less.Parser)(new less.tree.parseEnv(less)).parse(str, function (e, root) { - if (e) { - error(e, "session_cache"); - } else { - createCSS(root.toCSS(less), less.sheets[less.sheets.length - 1]); - } - }); -}; - -less.refresh = function (reload) { - var startTime, endTime; - startTime = endTime = new(Date); - - loadStyleSheets(function (e, root, _, sheet, env) { - if (e) { - return error(e, sheet.href); - } - if (env.local) { - log("loading " + sheet.href + " from cache."); - } else { - 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); - }, reload); - - loadStyles(); -}; -less.refreshStyles = loadStyles; - -less.refresh(less.env === 'development'); - -function loadStyles() { - var styles = document.getElementsByTagName('style'); - for (var i = 0; i < styles.length; i++) { - if (styles[i].type.match(typePattern)) { - var env = new less.tree.parseEnv(less); - env.filename = document.location.href.replace(/#.*$/, ''); - - new(less.Parser)(env).parse(styles[i].innerHTML || '', function (e, cssAST) { - if (e) { - return error(e, "inline"); - } - var css = cssAST.toCSS(less); - var style = styles[i]; - style.type = 'text/css'; - if (style.styleSheet) { - style.styleSheet.cssText = css; - } else { - style.innerHTML = css; - } - }); - } - } -} - -function loadStyleSheets(callback, reload) { - for (var i = 0; i < less.sheets.length; i++) { - loadStyleSheet(less.sheets[i], callback, reload, less.sheets.length - (i + 1)); - } -} - -function pathDiff(url, baseUrl) { - // diff between two paths to create a relative path - - var urlParts = extractUrlParts(url), - baseUrlParts = extractUrlParts(baseUrl), - i, max, urlDirectories, baseUrlDirectories, diff = ""; - if (urlParts.hostPart !== baseUrlParts.hostPart) { - return ""; - } - max = Math.max(baseUrlParts.directories.length, urlParts.directories.length); - for(i = 0; i < max; i++) { - if (baseUrlParts.directories[i] !== urlParts.directories[i]) { break; } - } - baseUrlDirectories = baseUrlParts.directories.slice(i); - urlDirectories = urlParts.directories.slice(i); - for(i = 0; i < baseUrlDirectories.length-1; i++) { - diff += "../"; - } - for(i = 0; i < urlDirectories.length-1; i++) { - diff += urlDirectories[i] + "/"; - } - return diff; -} - -function extractUrlParts(url, baseUrl) { - // urlParts[1] = protocol&hostname || / - // urlParts[2] = / if path relative to host base - // urlParts[3] = directories - // urlParts[4] = filename - // urlParts[5] = parameters - - var urlPartsRegex = /^((?:[a-z-]+:)?\/+?(?:[^\/\?#]*\/)|([\/\\]))?((?:[^\/\\\?#]*[\/\\])*)([^\/\\\?#]*)([#\?].*)?$/i, - urlParts = url.match(urlPartsRegex), - returner = {}, directories = [], i, baseUrlParts; - - if (!urlParts) { - throw new Error("Could not parse sheet href - '"+url+"'"); - } - - // Stylesheets in IE don't always return the full path - if (!urlParts[1] || urlParts[2]) { - baseUrlParts = baseUrl.match(urlPartsRegex); - if (!baseUrlParts) { - throw new Error("Could not parse page url - '"+baseUrl+"'"); - } - urlParts[1] = urlParts[1] || baseUrlParts[1] || ""; - if (!urlParts[2]) { - urlParts[3] = baseUrlParts[3] + urlParts[3]; - } - } - - if (urlParts[3]) { - directories = urlParts[3].replace(/\\/g, "/").split("/"); - - // extract out . before .. so .. doesn't absorb a non-directory - for(i = 0; i < directories.length; i++) { - if (directories[i] === ".") { - directories.splice(i, 1); - i -= 1; - } - } - - for(i = 0; i < directories.length; i++) { - if (directories[i] === ".." && i > 0) { - directories.splice(i-1, 2); - i -= 2; - } - } - } - - returner.hostPart = urlParts[1]; - returner.directories = directories; - returner.path = urlParts[1] + directories.join("/"); - returner.fileUrl = returner.path + (urlParts[4] || ""); - returner.url = returner.fileUrl + (urlParts[5] || ""); - return returner; -} - -function loadStyleSheet(sheet, callback, reload, remaining) { - - // sheet may be set to the stylesheet for the initial load or a collection of properties including - // some env variables for imports - var hrefParts = extractUrlParts(sheet.href, window.location.href); - var href = hrefParts.url; - var css = cache && cache.getItem(href); - var timestamp = cache && cache.getItem(href + ':timestamp'); - var styles = { css: css, timestamp: timestamp }; - var env; - var newFileInfo = { - relativeUrls: less.relativeUrls, - currentDirectory: hrefParts.path, - filename: href - }; - - if (sheet instanceof less.tree.parseEnv) { - env = new less.tree.parseEnv(sheet); - newFileInfo.entryPath = env.currentFileInfo.entryPath; - newFileInfo.rootpath = env.currentFileInfo.rootpath; - newFileInfo.rootFilename = env.currentFileInfo.rootFilename; - } else { - env = new less.tree.parseEnv(less); - env.mime = sheet.type; - newFileInfo.entryPath = hrefParts.path; - newFileInfo.rootpath = less.rootpath || hrefParts.path; - newFileInfo.rootFilename = href; - } - - if (env.relativeUrls) { - //todo - this relies on option being set on less object rather than being passed in as an option - // - need an originalRootpath - if (less.rootpath) { - newFileInfo.rootpath = extractUrlParts(less.rootpath + pathDiff(hrefParts.path, newFileInfo.entryPath)).path; - } else { - newFileInfo.rootpath = hrefParts.path; - } - } - - xhr(href, sheet.type, function (data, lastModified) { - // Store data this session - session_cache += data.replace(/@import .+?;/ig, ''); - - if (!reload && styles && lastModified && - (new(Date)(lastModified).valueOf() === - new(Date)(styles.timestamp).valueOf())) { - // Use local copy - createCSS(styles.css, sheet); - callback(null, null, data, sheet, { local: true, remaining: remaining }, href); - } else { - // Use remote copy (re-parse) - try { - env.contents[href] = data; // Updating content cache - env.paths = [hrefParts.path]; - env.currentFileInfo = newFileInfo; - - new(less.Parser)(env).parse(data, function (e, root) { - if (e) { return callback(e, null, null, sheet); } - try { - callback(e, root, data, sheet, { local: false, lastModified: lastModified, remaining: remaining }, href); - //TODO - there must be a better way? A generic less-to-css function that can both call error - //and removeNode where appropriate - //should also add tests - if (env.currentFileInfo.rootFilename === href) { - removeNode(document.getElementById('less-error-message:' + extractId(href))); - } - } catch (e) { - callback(e, null, null, sheet); - } - }); - } catch (e) { - callback(e, null, null, sheet); - } - } - }, function (status, url) { - callback({ type: 'File', message: "'" + url + "' wasn't found (" + status + ")" }, null, null, sheet); - }); -} - function extractId(href) { return href.replace(/^[a-z-]+:\/+?[^\/]+/, '' ) // Remove protocol & domain - .replace(/^\//, '' ) // Remove root / - .replace(/\.[a-zA-Z]+$/, '' ) // Remove simple extension - .replace(/[^\.\w-]+/g, '-') // Replace illegal characters - .replace(/\./g, ':'); // Replace dots with colons(for valid id) + .replace(/^\//, '' ) // Remove root / + .replace(/\.[a-zA-Z]+$/, '' ) // Remove simple extension + .replace(/[^\.\w-]+/g, '-') // Replace illegal characters + .replace(/\./g, ':'); // Replace dots with colons(for valid id) +} + +function errorConsole(e, rootHref) { + var template = '{line} {content}'; + var filename = e.filename || rootHref; + var errors = []; + var content = (e.type || "Syntax") + "Error: " + (e.message || 'There is an error in your .less file') + + " in " + filename + " "; + + var errorline = function (e, i, classname) { + 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])); + } + }; + + if (e.extract) { + errorline(e, 0, ''); + errorline(e, 1, 'line'); + errorline(e, 2, ''); + content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':\n' + + errors.join('\n'); + } else if (e.stack) { + content += e.stack; + } + log(content, logLevel.errors); } function createCSS(styles, sheet, lastModified) { @@ -371,89 +131,35 @@ 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); + if (nextEl) { + nextEl.parentNode.insertBefore(css, nextEl); + } else { + head.appendChild(css); + } } if (oldCss && keepOldCss === false) { - head.removeChild(oldCss); + oldCss.parentNode.removeChild(oldCss); } // Don't update the local store if the file wasn't modified if (lastModified && cache) { - log('saving ' + href + ' to cache.'); + log('saving ' + href + ' to cache.', logLevel.info); try { cache.setItem(href, styles); cache.setItem(href + ':timestamp', lastModified); } catch(e) { //TODO - could do with adding more robust error handling - log('failed to save'); + log('failed to save', logLevel.errors); } } } -function xhr(url, type, callback, errback) { - var xhr = getXMLHttpRequest(); - var async = isFileProtocol ? less.fileAsync : less.async; - - if (typeof(xhr.overrideMimeType) === 'function') { - xhr.overrideMimeType('text/css'); - } - xhr.open('GET', url, async); - xhr.setRequestHeader('Accept', type || 'text/x-less, text/css; q=0.9, */*; q=0.5'); - xhr.send(null); - - if (isFileProtocol && !less.fileAsync) { - if (xhr.status === 0 || (xhr.status >= 200 && xhr.status < 300)) { - callback(xhr.responseText); - } else { - errback(xhr.status, url); - } - } else if (async) { - xhr.onreadystatechange = function () { - if (xhr.readyState == 4) { - handleResponse(xhr, callback, errback); - } - }; - } else { - handleResponse(xhr, callback, errback); - } - - function handleResponse(xhr, callback, errback) { - if (xhr.status >= 200 && xhr.status < 300) { - callback(xhr.responseText, - xhr.getResponseHeader("Last-Modified")); - } else if (typeof(errback) === 'function') { - errback(xhr.status, url); - } - } -} - -function getXMLHttpRequest() { - if (window.XMLHttpRequest) { - return new(XMLHttpRequest); - } else { - try { - return new(ActiveXObject)("MSXML2.XMLHTTP.3.0"); - } catch (e) { - log("browser doesn't support AJAX."); - return null; - } - } -} - -function removeNode(node) { - return node && node.parentNode.removeChild(node); -} - -function log(str) { - if (less.env == 'development' && typeof(console) !== "undefined") { console.log('less: ' + str) } -} - -function error(e, rootHref) { +function errorHTML(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]; @@ -461,13 +167,13 @@ function error(e, rootHref) { elem.className = "less-error-message"; content = '

    ' + (e.type || "Syntax") + "Error: " + (e.message || 'There is an error in your .less file') + - '

    ' + '

    in ' + filenameNoPath + " "; + '' + '

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

    ' + - '
      ' + error.join('') + '
    '; + '
      ' + errors.join('') + '
    '; } else if (e.stack) { content += '
    ' + e.stack.split('\n').slice(1).join('
    '); } @@ -485,40 +191,40 @@ function error(e, rootHref) { // CSS for error messages createCSS([ '.less-error-message ul, .less-error-message li {', - 'list-style-type: none;', - 'margin-right: 15px;', - 'padding: 4px 0;', - 'margin: 0;', + 'list-style-type: none;', + 'margin-right: 15px;', + 'padding: 4px 0;', + 'margin: 0;', '}', '.less-error-message label {', - 'font-size: 12px;', - 'margin-right: 15px;', - 'padding: 4px 0;', - 'color: #cc7777;', + 'font-size: 12px;', + 'margin-right: 15px;', + 'padding: 4px 0;', + 'color: #cc7777;', '}', '.less-error-message pre {', - 'color: #dd6666;', - 'padding: 4px 0;', - 'margin: 0;', - 'display: inline-block;', + 'color: #dd6666;', + 'padding: 4px 0;', + 'margin: 0;', + 'display: inline-block;', '}', '.less-error-message pre.line {', - 'color: #ff0000;', + 'color: #ff0000;', '}', '.less-error-message h3 {', - 'font-size: 20px;', - 'font-weight: bold;', - 'padding: 15px 0 5px 0;', - 'margin: 0;', + 'font-size: 20px;', + 'font-weight: bold;', + 'padding: 15px 0 5px 0;', + 'margin: 0;', '}', '.less-error-message a {', - 'color: #10a', + 'color: #10a', '}', '.less-error-message .error {', - 'color: red;', - 'font-weight: bold;', - 'padding-bottom: 2px;', - 'border-bottom: 1px dashed red;', + 'color: red;', + 'font-weight: bold;', + 'padding-bottom: 2px;', + 'border-bottom: 1px dashed red;', '}' ].join('\n'), { title: 'error-message' }); @@ -547,3 +253,414 @@ function error(e, rootHref) { }, 10); } } + +function error(e, rootHref) { + if (!less.errorReporting || less.errorReporting === "html") { + errorHTML(e, rootHref); + } else if (less.errorReporting === "console") { + errorConsole(e, rootHref); + } else if (typeof less.errorReporting === 'function') { + less.errorReporting("add", e, rootHref); + } +} + +function removeErrorHTML(path) { + var node = document.getElementById('less-error-message:' + extractId(path)); + if (node) { + node.parentNode.removeChild(node); + } +} + +function removeErrorConsole(path) { + //no action +} + +function removeError(path) { + if (!less.errorReporting || less.errorReporting === "html") { + removeErrorHTML(path); + } else if (less.errorReporting === "console") { + removeErrorConsole(path); + } else if (typeof less.errorReporting === 'function') { + less.errorReporting("remove", path); + } +} + +function loadStyles(newVars) { + var styles = document.getElementsByTagName('style'), + style; + for (var i = 0; i < styles.length; i++) { + style = styles[i]; + if (style.type.match(typePattern)) { + var env = new less.tree.parseEnv(less), + lessText = style.innerHTML || ''; + env.filename = document.location.href.replace(/#.*$/, ''); + if (newVars) { + env.useFileCache = true; + lessText += "\n" + newVars; + } + + /*jshint loopfunc:true */ + // use closure to store current value of i + var callback = (function(style) { + return function (e, cssAST) { + if (e) { + return error(e, "inline"); + } + var css = cssAST.toCSS(less); + style.type = 'text/css'; + if (style.styleSheet) { + style.styleSheet.cssText = css; + } else { + style.innerHTML = css; + } + }; + })(style); + new(less.Parser)(env).parse(lessText, callback); + } + } +} + +function extractUrlParts(url, baseUrl) { + // urlParts[1] = protocol&hostname || / + // urlParts[2] = / if path relative to host base + // urlParts[3] = directories + // urlParts[4] = filename + // urlParts[5] = parameters + + var urlPartsRegex = /^((?:[a-z-]+:)?\/+?(?:[^\/\?#]*\/)|([\/\\]))?((?:[^\/\\\?#]*[\/\\])*)([^\/\\\?#]*)([#\?].*)?$/i, + urlParts = url.match(urlPartsRegex), + returner = {}, directories = [], i, baseUrlParts; + + if (!urlParts) { + throw new Error("Could not parse sheet href - '"+url+"'"); + } + + // Stylesheets in IE don't always return the full path + if (!urlParts[1] || urlParts[2]) { + baseUrlParts = baseUrl.match(urlPartsRegex); + if (!baseUrlParts) { + throw new Error("Could not parse page url - '"+baseUrl+"'"); + } + urlParts[1] = urlParts[1] || baseUrlParts[1] || ""; + if (!urlParts[2]) { + urlParts[3] = baseUrlParts[3] + urlParts[3]; + } + } + + if (urlParts[3]) { + directories = urlParts[3].replace(/\\/g, "/").split("/"); + + // extract out . before .. so .. doesn't absorb a non-directory + for(i = 0; i < directories.length; i++) { + if (directories[i] === ".") { + directories.splice(i, 1); + i -= 1; + } + } + + for(i = 0; i < directories.length; i++) { + if (directories[i] === ".." && i > 0) { + directories.splice(i-1, 2); + i -= 2; + } + } + } + + returner.hostPart = urlParts[1]; + returner.directories = directories; + returner.path = urlParts[1] + directories.join("/"); + returner.fileUrl = returner.path + (urlParts[4] || ""); + returner.url = returner.fileUrl + (urlParts[5] || ""); + return returner; +} + +function pathDiff(url, baseUrl) { + // diff between two paths to create a relative path + + var urlParts = extractUrlParts(url), + baseUrlParts = extractUrlParts(baseUrl), + i, max, urlDirectories, baseUrlDirectories, diff = ""; + if (urlParts.hostPart !== baseUrlParts.hostPart) { + return ""; + } + max = Math.max(baseUrlParts.directories.length, urlParts.directories.length); + for(i = 0; i < max; i++) { + if (baseUrlParts.directories[i] !== urlParts.directories[i]) { break; } + } + baseUrlDirectories = baseUrlParts.directories.slice(i); + urlDirectories = urlParts.directories.slice(i); + for(i = 0; i < baseUrlDirectories.length-1; i++) { + diff += "../"; + } + for(i = 0; i < urlDirectories.length-1; i++) { + diff += urlDirectories[i] + "/"; + } + return diff; +} + +function getXMLHttpRequest() { + if (window.XMLHttpRequest) { + return new XMLHttpRequest(); + } else { + try { + /*global ActiveXObject */ + return new ActiveXObject("MSXML2.XMLHTTP.3.0"); + } catch (e) { + log("browser doesn't support AJAX.", logLevel.errors); + return null; + } + } +} + +function doXHR(url, type, callback, errback) { + var xhr = getXMLHttpRequest(); + var async = isFileProtocol ? less.fileAsync : less.async; + + if (typeof(xhr.overrideMimeType) === 'function') { + xhr.overrideMimeType('text/css'); + } + log("XHR: Getting '" + url + "'", logLevel.info); + xhr.open('GET', url, async); + xhr.setRequestHeader('Accept', type || 'text/x-less, text/css; q=0.9, */*; q=0.5'); + xhr.send(null); + + function handleResponse(xhr, callback, errback) { + if (xhr.status >= 200 && xhr.status < 300) { + callback(xhr.responseText, + xhr.getResponseHeader("Last-Modified")); + } else if (typeof(errback) === 'function') { + errback(xhr.status, url); + } + } + + if (isFileProtocol && !less.fileAsync) { + if (xhr.status === 0 || (xhr.status >= 200 && xhr.status < 300)) { + callback(xhr.responseText); + } else { + errback(xhr.status, url); + } + } else if (async) { + xhr.onreadystatechange = function () { + if (xhr.readyState == 4) { + handleResponse(xhr, callback, errback); + } + }; + } else { + handleResponse(xhr, callback, errback); + } +} + +function loadFile(originalHref, currentFileInfo, callback, env, newVars) { + + if (currentFileInfo && currentFileInfo.currentDirectory && !/^([a-z-]+:)?\//.test(originalHref)) { + originalHref = currentFileInfo.currentDirectory + originalHref; + } + + // sheet may be set to the stylesheet for the initial load or a collection of properties including + // some env variables for imports + var hrefParts = extractUrlParts(originalHref, window.location.href); + var href = hrefParts.url; + var newFileInfo = { + currentDirectory: hrefParts.path, + filename: href + }; + + if (currentFileInfo) { + newFileInfo.entryPath = currentFileInfo.entryPath; + newFileInfo.rootpath = currentFileInfo.rootpath; + newFileInfo.rootFilename = currentFileInfo.rootFilename; + newFileInfo.relativeUrls = currentFileInfo.relativeUrls; + } else { + newFileInfo.entryPath = hrefParts.path; + newFileInfo.rootpath = less.rootpath || hrefParts.path; + newFileInfo.rootFilename = href; + newFileInfo.relativeUrls = env.relativeUrls; + } + + if (newFileInfo.relativeUrls) { + if (env.rootpath) { + newFileInfo.rootpath = extractUrlParts(env.rootpath + pathDiff(hrefParts.path, newFileInfo.entryPath)).path; + } else { + newFileInfo.rootpath = hrefParts.path; + } + } + + if (env.useFileCache && fileCache[href]) { + try { + var lessText = fileCache[href]; + if (newVars) { + lessText += "\n" + newVars; + } + callback(null, lessText, href, newFileInfo, { lastModified: new Date() }); + } catch (e) { + callback(e, null, href); + } + return; + } + + 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 }); + } catch (e) { + callback(e, null, href); + } + }, function (status, url) { + callback({ type: 'File', message: "'" + url + "' wasn't found (" + status + ")" }, null, href); + }); +} + +function loadStyleSheet(sheet, callback, reload, remaining, newVars) { + + var env = new less.tree.parseEnv(less); + env.mime = sheet.type; + + if (newVars) { + env.useFileCache = true; + } + + loadFile(sheet.href, null, function(e, data, path, newFileInfo, webInfo) { + + if (webInfo) { + webInfo.remaining = remaining; + + var css = cache && cache.getItem(path), + timestamp = cache && cache.getItem(path + ':timestamp'); + + if (!reload && timestamp && webInfo.lastModified && + (new(Date)(webInfo.lastModified).valueOf() === + new(Date)(timestamp).valueOf())) { + // Use local copy + createCSS(css, sheet); + webInfo.local = true; + callback(null, null, data, sheet, webInfo, path); + return; + } + } + + //TODO add tests around how this behaves when reloading + removeError(path); + + if (data) { + env.currentFileInfo = newFileInfo; + new(less.Parser)(env).parse(data, function (e, root) { + if (e) { return callback(e, null, null, sheet); } + try { + callback(e, root, data, sheet, webInfo, path); + } catch (e) { + callback(e, null, null, sheet); + } + }); + } else { + callback(e, null, null, sheet, webInfo, path); + } + }, env, newVars); +} + +function loadStyleSheets(callback, reload, newVars) { + for (var i = 0; i < less.sheets.length; i++) { + loadStyleSheet(less.sheets[i], callback, reload, less.sheets.length - (i + 1), newVars); + } +} + +function initRunningMode(){ + if (less.env === 'development') { + less.optimization = 0; + less.watchTimer = setInterval(function () { + if (less.watchMode) { + loadStyleSheets(function (e, root, _, sheet, env) { + if (e) { + error(e, sheet.href); + } else if (root) { + createCSS(root.toCSS(less), sheet, env.lastModified); + } + }); + } + }, less.poll); + } else { + less.optimization = 3; + } +} + +// +// Watch mode +// +less.watch = function () { + if (!less.watchMode ){ + less.env = 'development'; + initRunningMode(); + } + return this.watchMode = true; +}; + +less.unwatch = function () {clearInterval(less.watchTimer); return this.watchMode = false; }; + +if (/!watch/.test(location.hash)) { + less.watch(); +} + +if (less.env != 'development') { + try { + cache = (typeof(window.localStorage) === 'undefined') ? null : window.localStorage; + } catch (_) {} +} + +// +// Get all tags with the 'rel' attribute set to "stylesheet/less" +// +var links = document.getElementsByTagName('link'); + +less.sheets = []; + +for (var i = 0; i < links.length; i++) { + if (links[i].rel === 'stylesheet/less' || (links[i].rel.match(/stylesheet/) && + (links[i].type.match(typePattern)))) { + less.sheets.push(links[i]); + } +} + +// +// With this function, it's possible to alter variables and re-render +// CSS without reloading less-files +// +less.modifyVars = function(record) { + var newVars = ""; + for (var name in record) { + newVars += ((name.slice(0,1) === '@')? '' : '@') + name +': '+ + ((record[name].slice(-1) === ';')? record[name] : record[name] +';'); + } + less.refresh(false, newVars); +}; + +less.refresh = function (reload, newVars) { + var startTime, endTime; + startTime = endTime = new Date(); + + loadStyleSheets(function (e, root, _, sheet, env) { + if (e) { + return error(e, sheet.href); + } + if (env.local) { + log("loading " + sheet.href + " from cache.", logLevel.info); + } else { + log("parsed " + sheet.href + " successfully.", logLevel.info); + createCSS(root.toCSS(less), sheet, env.lastModified); + } + log("css for " + sheet.href + " generated in " + (new Date() - endTime) + 'ms', logLevel.info); + if (env.remaining === 0) { + log("css generated in " + (new Date() - startTime) + 'ms', logLevel.info); + } + endTime = new Date(); + }, reload, newVars); + + loadStyles(newVars); +}; + +less.refreshStyles = loadStyles; + +less.Parser.fileLoader = loadFile; + +less.refresh(less.env === 'development'); diff --git a/lib/less/env.js b/lib/less/env.js index 91b0027e..595a54f9 100644 --- a/lib/less/env.js +++ b/lib/less/env.js @@ -6,12 +6,15 @@ 'files', // list of files that have been imported, used for import-once 'contents', // browser-only, contents of all the files 'relativeUrls', // option - whether to adjust URL's to be relative + 'rootpath', // option - rootpath to append to URL's 'strictImports', // option - 'dumpLineNumbers', // option - whether to dump line numbers 'compress', // option - whether to compress 'processImports', // option - whether to process imports. if false then imports will not be imported 'syncImport', // option - whether to import synchronously + 'javascriptEnabled',// option - whether JavaScript is enabled. if undefined, defaults to true 'mime', // browser only - mime type for sheet import + 'useFileCache', // browser only - whether to use the per file session cache 'currentFileInfo' // information about the current file - for error reporting and importing and making urls relative etc. ]; @@ -21,7 +24,8 @@ // 'rootpath' - path to append to normal URLs for this node // 'currentDirectory' - path to the current file, absolute // 'rootFilename' - filename of the base file - // 'entryPath' = absolute path to the entry file + // 'entryPath' - absolute path to the entry file + // 'reference' - whether the file should not be output and only output parts that are referenced tree.parseEnv = function(options) { copyFromOriginal(options, this, parseCopyProperties); @@ -46,14 +50,6 @@ } }; - tree.parseEnv.prototype.toSheet = function (path) { - var env = new tree.parseEnv(this); - env.href = path; - //env.title = path; - env.type = this.mime; - return env; - }; - var evalCopyProperties = [ 'silent', // whether to swallow errors and warnings 'verbose', // whether to log more activity @@ -61,7 +57,9 @@ 'yuicompress', // whether to compress with the outside tool yui compressor 'ieCompat', // whether to enforce IE compatibility (IE8 data-uri) 'strictMath', // whether math has to be within parenthesis - 'strictUnits' // whether units need to evaluate correctly + 'strictUnits', // whether units need to evaluate correctly + 'cleancss', // whether to compress with clean-css + 'sourceMap' // whether to output a source map ]; tree.evalEnv = function(options, frames) { @@ -89,6 +87,33 @@ return !/^(?:[a-z-]+:|\/)/.test(path); }; + tree.evalEnv.prototype.normalizePath = function( path ) { + var + segments = path.split("/").reverse(), + segment; + + path = []; + while (segments.length !== 0 ) { + segment = segments.pop(); + switch( segment ) { + case ".": + break; + case "..": + if ((path.length === 0) || (path[path.length - 1] === "..")) { + path.push( segment ); + } else { + path.pop(); + } + break; + default: + path.push( segment ); + break; + } + } + + return path.join("/"); + }; + //todo - do the same for the toCSS env //tree.toCSSEnv = function (options) { //}; @@ -101,5 +126,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..9e66c089 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 = ' '; } @@ -325,7 +327,8 @@ matchIndex, selector, firstElement, - match; + match, + newElements; for (matchIndex = 0; matchIndex < matches.length; matchIndex++) { match = matches[matchIndex]; @@ -333,7 +336,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) { @@ -342,17 +346,24 @@ currentSelectorPathIndex++; } - path = path.concat(selectorPath.slice(currentSelectorPathIndex, match.pathIndex)); + newElements = selector.elements + .slice(currentSelectorPathElementIndex, match.index) + .concat([firstElement]) + .concat(replacementSelector.elements.slice(1)); - path.push(new tree.Selector( - selector.elements - .slice(currentSelectorPathElementIndex, match.index) - .concat([firstElement]) - .concat(replacementSelector.elements.slice(1)) - )); + if (currentSelectorPathIndex === match.pathIndex && matchIndex > 0) { + path[path.length - 1].elements = + path[path.length - 1].elements.concat(newElements); + } else { + path = path.concat(selectorPath.slice(currentSelectorPathIndex, match.pathIndex)); + + path.push(new tree.Selector( + newElements + )); + } currentSelectorPathIndex = match.endPathIndex; currentSelectorPathElementIndex = match.endPathElementIndex; - if (currentSelectorPathElementIndex >= selector.elements.length) { + if (currentSelectorPathElementIndex >= selectorPath[currentSelectorPathIndex].elements.length) { currentSelectorPathElementIndex = 0; currentSelectorPathIndex++; } @@ -388,4 +399,4 @@ } }; -})(require('./tree')); \ No newline at end of file +})(require('./tree')); diff --git a/lib/less/functions.js b/lib/less/functions.js index fbde54ff..249aaf17 100644 --- a/lib/less/functions.js +++ b/lib/less/functions.js @@ -13,6 +13,14 @@ tree.functions = { return this.hsla(h, s, l, 1.0); }, hsla: function (h, s, l, a) { + 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; } + } + h = (number(h) % 360) / 360; s = clamp(number(s)); l = clamp(number(l)); a = clamp(number(a)); @@ -23,14 +31,6 @@ tree.functions = { hue(h) * 255, hue(h - 1/3) * 255, a); - - 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; - } }, hsv: function(h, s, v) { @@ -96,6 +96,11 @@ tree.functions = { return new(tree.Dimension)(Math.round(color.luma() * color.alpha * 100), '%'); }, saturate: function (color, amount) { + // filter: saturate(3.2); + // should be kept as is, so check for color + if (!color.rgb) { + return null; + } var hsl = color.toHSL(); hsl.s += amount.value / 100; @@ -218,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; @@ -254,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); @@ -261,6 +268,49 @@ tree.functions = { throw { type: "Argument", message: "argument must be a number" }; } }, + _minmax: function (isMin, args) { + args = Array.prototype.slice.call(args); + switch(args.length) { + case 0: throw { type: "Argument", message: "one or more arguments required" }; + case 1: return args[0]; + } + var i, j, current, currentUnified, referenceUnified, unit, + order = [], // elems only contains original argument values. + values = {}; // key is the unit.toString() for unified tree.Dimension values, + // value is the index into the order array. + for (i = 0; i < args.length; i++) { + current = args[i]; + if (!(current instanceof tree.Dimension)) { + order.push(current); + continue; + } + currentUnified = current.unify(); + unit = currentUnified.unit.toString(); + j = values[unit]; + if (j === undefined) { + values[unit] = order.length; + order.push(current); + continue; + } + referenceUnified = order[j].unify(); + if ( isMin && currentUnified.value < referenceUnified.value || + !isMin && currentUnified.value > referenceUnified.value) { + order[j] = current; + } + } + if (order.length == 1) { + return order[0]; + } + args = order.map(function (a) { return a.toCSS(this.env); }) + .join(this.env.compress ? "," : ", "); + return new(tree.Anonymous)((isMin ? "min" : "max") + "(" + args + ")"); + }, + min: function () { + return this._minmax(true, arguments); + }, + max: function () { + return this._minmax(false, arguments); + }, argb: function (color) { return new(tree.Anonymous)(color.toARGB()); @@ -415,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); @@ -446,6 +496,83 @@ tree.functions = { var uri = "'data:" + mimetype + ',' + buf + "'"; return new(tree.URL)(new(tree.Anonymous)(uri)); + }, + + "svg-gradient": function(direction) { + + function throwArgumentDescriptor() { + throw { type: "Argument", message: "svg-gradient expects direction, start_color [start_position], [color position,]..., end_color [end_position]" }; + } + + if (arguments.length < 3) { + throwArgumentDescriptor(); + } + var stops = Array.prototype.slice.call(arguments, 1), + gradientDirectionSvg, + gradientType = "linear", + rectangleDimension = 'x="0" y="0" width="1" height="1"', + useBase64 = true, + renderEnv = {compress: false}, + returner, + directionValue = direction.toCSS(renderEnv), + i, color, position, positionValue, alpha; + + switch (directionValue) { + case "to bottom": + gradientDirectionSvg = 'x1="0%" y1="0%" x2="0%" y2="100%"'; + break; + case "to right": + gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="0%"'; + break; + case "to bottom right": + gradientDirectionSvg = 'x1="0%" y1="0%" x2="100%" y2="100%"'; + break; + case "to top right": + gradientDirectionSvg = 'x1="0%" y1="100%" x2="100%" y2="0%"'; + break; + case "ellipse": + case "ellipse at center": + gradientType = "radial"; + gradientDirectionSvg = 'cx="50%" cy="50%" r="75%"'; + rectangleDimension = 'x="-50" y="-50" width="101" height="101"'; + 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'" }; + } + returner = '' + + '' + + '<' + gradientType + 'Gradient id="gradient" gradientUnits="userSpaceOnUse" ' + gradientDirectionSvg + '>'; + + for (i = 0; i < stops.length; i+= 1) { + if (stops[i].value) { + color = stops[i].value[0]; + position = stops[i].value[1]; + } else { + color = stops[i]; + position = undefined; + } + + if (!(color instanceof tree.Color) || (!((i === 0 || i+1 === stops.length) && position === undefined) && !(position instanceof tree.Dimension))) { + throwArgumentDescriptor(); + } + positionValue = position ? position.toCSS(renderEnv) : i === 0 ? "0%" : "100%"; + alpha = color.alpha; + returner += ''; + } + returner += '' + + ''; + + if (useBase64) { + // only works in node, needs interface to what is supported in environment + try { + returner = new Buffer(returner).toString('base64'); + } catch(e) { + useBase64 = false; + } + } + + returner = "'data:image/svg+xml" + (useBase64 ? ";base64" : "") + "," + returner + "'"; + return new(tree.URL)(new(tree.Anonymous)(returner)); } }; @@ -482,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/import-visitor.js b/lib/less/import-visitor.js index 5548430e..5a972211 100644 --- a/lib/less/import-visitor.js +++ b/lib/less/import-visitor.js @@ -27,9 +27,10 @@ }, visitImport: function (importNode, visitArgs) { var importVisitor = this, - evaldImportNode; + evaldImportNode, + inlineCSS = importNode.options.inline; - if (!importNode.css) { + if (!importNode.css || inlineCSS) { try { evaldImportNode = importNode.evalForImport(this.env); @@ -41,11 +42,12 @@ importNode.error = e; } - if (evaldImportNode && !evaldImportNode.css) { + if (evaldImportNode && (!evaldImportNode.css || inlineCSS)) { importNode = evaldImportNode; this.importCount++; var env = new tree.evalEnv(this.env, this.env.frames.slice(0)); - this._importer.push(importNode.getPath(), importNode.currentFileInfo, function (e, root, imported) { + + this._importer.push(importNode.getPath(), importNode.currentFileInfo, importNode.options, function (e, root, imported) { if (e && !e.filename) { e.index = importNode.index; e.filename = importNode.currentFileInfo.filename; } if (imported && !importNode.options.multiple) { importNode.skip = imported; } @@ -59,11 +61,14 @@ if (root) { importNode.root = root; - new(tree.importVisitor)(importVisitor._importer, subFinish, env) - .run(root); - } else { - subFinish(); + if (!inlineCSS && !importNode.skip) { + new(tree.importVisitor)(importVisitor._importer, subFinish, env) + .run(root); + return; + } } + + subFinish(); }); } } diff --git a/lib/less/index.js b/lib/less/index.js index 4fccef78..aaf6e9b4 100644 --- a/lib/less/index.js +++ b/lib/less/index.js @@ -5,9 +5,8 @@ var path = require('path'), fs = require('fs'); var less = { - version: [1, 4, 2], + version: [1, 5, "0-b1"], Parser: require('./parser').Parser, - importer: require('./parser').importer, tree: require('./tree'), render: function (input, options, callback) { options = options || {}; @@ -21,15 +20,17 @@ var less = { if (callback) { parser.parse(input, function (e, root) { - callback(e, root && root.toCSS && root.toCSS(options)); + try { callback(e, root && root.toCSS && root.toCSS(options)); } + catch (err) { callback(err); } }); } else { - ee = new(require('events').EventEmitter); + ee = new (require('events').EventEmitter)(); process.nextTick(function () { parser.parse(input, function (e, root) { - if (e) { ee.emit('error', e) } - else { ee.emit('success', root.toCSS(options)) } + if (e) { return ee.emit('error', e); } + try { ee.emit('success', root.toCSS(options)); } + catch (err) { ee.emit('error', err); } }); }); return ee; @@ -41,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; @@ -84,7 +85,7 @@ var less = { }, writeError: function (ctx, options) { options = options || {}; - if (options.silent) { return } + if (options.silent) { return; } sys.error(less.formatError(ctx, options)); } }; @@ -103,7 +104,7 @@ var less = { var isUrlRe = /^(?:https?:)?\/\//i; -less.Parser.importer = function (file, currentFileInfo, callback, env) { +less.Parser.fileLoader = function (file, currentFileInfo, callback, env) { var pathname, dirname, data, newFileInfo = { relativeUrls: env.relativeUrls, @@ -112,12 +113,7 @@ less.Parser.importer = function (file, currentFileInfo, callback, env) { rootFilename: currentFileInfo.rootFilename }; - function parseFile(e, data) { - if (e) { return callback(e); } - - env = new less.tree.parseEnv(env); - env.processImports = false; - + function handleDataAndCallCallback(data) { var j = file.lastIndexOf('/'); // Pass on an updated rootpath if path of imported file is relative and file @@ -135,12 +131,8 @@ less.Parser.importer = function (file, currentFileInfo, callback, env) { newFileInfo.currentDirectory = pathname.replace(/[^\\\/]*$/, ""); newFileInfo.filename = pathname; - env.contents[pathname] = data; // Updating top importing parser content cache. - env.currentFileInfo = newFileInfo; - new(less.Parser)(env).parse(data, function (e, root) { - callback(e, root, pathname); - }); - }; + callback(null, data, pathname, newFileInfo); + } var isUrl = isUrlRe.test( file ); if (isUrl || isUrlRe.test(currentFileInfo.currentDirectory)) { @@ -154,12 +146,7 @@ less.Parser.importer = 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) { @@ -174,7 +161,7 @@ less.Parser.importer = function (file, currentFileInfo, callback, env) { } pathname = urlStr; dirname = urlObj.protocol +'//'+ urlObj.host + urlObj.pathname.replace(/[^\/]*$/, ''); - parseFile(null, body); + handleDataAndCallCallback(body); }); } else { @@ -202,15 +189,18 @@ less.Parser.importer = function (file, currentFileInfo, callback, env) { if (env.syncImport) { try { data = fs.readFileSync(pathname, 'utf-8'); - parseFile(null, data); + handleDataAndCallCallback(data); } catch (e) { - parseFile(e); + callback(e); } } else { - fs.readFile(pathname, 'utf-8', parseFile); + fs.readFile(pathname, 'utf-8', function(e, data) { + if (e) { callback(e); } + handleDataAndCallCallback(data); + }); } } -} +}; require('./env'); require('./functions'); @@ -219,5 +209,7 @@ require('./visitor.js'); 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/join-selector-visitor.js b/lib/less/join-selector-visitor.js index c478447f..7c9a1af8 100644 --- a/lib/less/join-selector-visitor.js +++ b/lib/less/join-selector-visitor.js @@ -21,6 +21,10 @@ this.contexts.push(paths); if (! rulesetNode.root) { + rulesetNode.selectors = rulesetNode.selectors.filter(function(selector) { return selector.getIsOutput(); }); + if (rulesetNode.selectors.length === 0) { + rulesetNode.rules.length = 0; + } rulesetNode.joinSelectors(paths, context, rulesetNode.selectors); rulesetNode.paths = paths; } @@ -30,7 +34,7 @@ }, visitMedia: function (mediaNode, visitArgs) { var context = this.contexts[this.contexts.length - 1]; - mediaNode.ruleset.root = (context.length === 0 || context[0].multiMedia); + mediaNode.rules[0].root = (context.length === 0 || context[0].multiMedia); } }; diff --git a/lib/less/lessc_helper.js b/lib/less/lessc_helper.js index 576a8060..14e70cca 100644 --- a/lib/less/lessc_helper.js +++ b/lib/less/lessc_helper.js @@ -33,14 +33,14 @@ var lessc_helper = { sys.puts(" -M, --depends Output a makefile import dependency list to stdout"); sys.puts(" --no-color Disable colorized output."); sys.puts(" --no-ie-compat Disable IE compatibility checks."); + sys.puts(" --no-js Disable JavaScript in less files"); sys.puts(" -l, --lint Syntax check only (lint)."); sys.puts(" -s, --silent Suppress output of error messages."); sys.puts(" --strict-imports Force evaluation of imports."); sys.puts(" --verbose Be verbose."); sys.puts(" -v, --version Print version number and exit."); sys.puts(" -x, --compress Compress output by removing some whitespaces."); - sys.puts(" --yui-compress Compress output using ycssmin"); - sys.puts(" --max-line-len=LINELEN Max line length used by ycssmin"); + sys.puts(" --clean-css Compress output using clean-css"); sys.puts(" -O0, -O1, -O2 Set the parser's optimization level. The lower"); sys.puts(" the number, the less nodes it will create in the"); sys.puts(" tree. This could matter for debugging, or if you"); @@ -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."); @@ -60,12 +63,10 @@ var lessc_helper = { sys.puts(" -su=on|off Allow mixed units, e.g. 1px+1em or 1px*1px which have units"); sys.puts(" --strict-units=on|off that cannot be represented."); sys.puts(""); - sys.puts("Report bugs to: http://github.com/cloudhead/less.js/issues"); + sys.puts("Report bugs to: http://github.com/less/less.js/issues"); sys.puts("Home page: "); } - - -} +}; // 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 98df712b..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)) { @@ -78,24 +63,47 @@ less.Parser = function Parser(env) { contents: env.contents, // Holds the imported file contents mime: env.mime, // MIME type of .less files error: null, // Error in parsing/evaluating an import - push: function (path, currentFileInfo, callback) { - var parserImporter = this; + push: function (path, currentFileInfo, importOptions, callback) { + var parserImports = this; this.queue.push(path); - // - // Import a file asynchronously - // - less.Parser.importer(path, currentFileInfo, function (e, root, fullPath) { - parserImporter.queue.splice(parserImporter.queue.indexOf(path), 1); // Remove the path from the queue + var fileParsedFunc = function (e, root, fullPath) { + parserImports.queue.splice(parserImports.queue.indexOf(path), 1); // Remove the path from the queue - var imported = fullPath in parserImporter.files; + var importedPreviously = fullPath in parserImports.files; - parserImporter.files[fullPath] = root; // Store the root + parserImports.files[fullPath] = root; // Store the root - if (e && !parserImporter.error) { parserImporter.error = e; } - - callback(e, root, imported); - }, env); + if (e && !parserImports.error) { parserImports.error = e; } + + callback(e, root, importedPreviously); + }; + + if (less.Parser.importer) { + less.Parser.importer(path, currentFileInfo, fileParsedFunc, env); + } else { + less.Parser.fileLoader(path, currentFileInfo, function(e, contents, fullPath, newFileInfo) { + if (e) {fileParsedFunc(e); return;} + + var newEnv = new tree.parseEnv(env); + + newEnv.currentFileInfo = newFileInfo; + newEnv.processImports = false; + newEnv.contents[fullPath] = contents; + + if (currentFileInfo.reference || importOptions.reference) { + newFileInfo.reference = true; + } + + if (importOptions.inline) { + fileParsedFunc(null, contents, fullPath); + } else { + new(less.Parser)(newEnv).parse(contents, function (e, root) { + fileParsedFunc(e, root, fullPath); + }); + } + }, env); + } } }; @@ -117,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 @@ -166,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; } @@ -200,11 +208,7 @@ less.Parser = function Parser(env) { if (typeof(tok) === 'string') { return input.charAt(i) === tok; } else { - if (tok.test(chunks[j])) { - return true; - } else { - return false; - } + return tok.test(chunks[j]); } } @@ -216,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) { @@ -242,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'; @@ -249,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 = [ @@ -282,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'); @@ -290,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, @@ -334,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', @@ -352,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) { @@ -372,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); // @@ -402,13 +444,13 @@ less.Parser = function Parser(env) { } value = new(tree.Value)([value]); } - return new(tree.Rule)('@' + k, value, false, 0); + return new(tree.Rule)('@' + k, value, false, null, 0); }); evalEnv.frames = [new(tree.Ruleset)(null, variables)]; } try { - var evaldRoot = evaluate.call(this, evalEnv); + evaldRoot = evaluate.call(this, evalEnv); new(tree.joinSelectorVisitor)() .run(evaldRoot); @@ -416,7 +458,24 @@ less.Parser = function Parser(env) { new(tree.processExtendsVisitor)() .run(evaldRoot); - var css = evaldRoot.toCSS({ + new(tree.toCSSVisitor)({compress: Boolean(options.compress)}) + .run(evaldRoot); + + 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)}); @@ -424,10 +483,10 @@ less.Parser = function Parser(env) { throw new(LessError)(e, env); } - if (options.yuicompress && less.mode === 'node') { - return require('ycssmin').cssmin(css, options.maxLineLen); + if (options.cleancss && less.mode === 'node') { + return require('clean-css').process(css); } else if (options.compress) { - return css.replace(/(\s)+/g, "$1"); + return css.replace(/(^(\s)+)|((\s)+$)/g, ""); } else { return css; } @@ -444,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", @@ -455,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], @@ -549,15 +607,25 @@ 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); + return new(tree.Comment)($(/^\/\/.*/), true, i, env.currentFileInfo); } else if (comment = $(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/)) { - return new(tree.Comment)(comment); + return new(tree.Comment)(comment, false, i, env.currentFileInfo); } }, + comments: function () { + var comment, comments = []; + + while(comment = $(this.comment)) { + comments.push(comment); + } + + return comments; + }, + // // Entities are tokens which can be found inside an Expression // @@ -570,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 && $('~'); @@ -611,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); @@ -641,7 +709,9 @@ less.Parser = function Parser(env) { while (arg = $(this.entities.assignment) || $(this.expression)) { args.push(arg); - if (! $(',')) { break } + if (! $(',')) { + break; + } } return args; }, @@ -675,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); }, @@ -703,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); @@ -733,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]); @@ -761,10 +837,13 @@ less.Parser = function Parser(env) { javascript: function () { var str, j = i, e; - if (input.charAt(j) === '~') { j++, e = true } // Escaped strings - if (input.charAt(j) !== '`') { return } + if (input.charAt(j) === '~') { j++; e = true; } // Escaped strings + if (input.charAt(j) !== '`') { return; } + if (env.javascriptEnabled !== undefined && !env.javascriptEnabled) { + error("You are using JavaScript, which has been disabled."); + } - e && $('~'); + if (e) { $('~'); } if (str = $(/^`([^`]*)`/)) { return new(tree.JavaScript)(str[1], i, e); @@ -780,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]; } }, // @@ -806,7 +885,7 @@ less.Parser = function Parser(env) { extendList.push(new(tree.Extend)(new(tree.Selector)(elements), option, index)); - } while($(",")) + } while($(",")); expect(/^\)/); @@ -840,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 ($('(')) { @@ -874,7 +953,7 @@ less.Parser = function Parser(env) { if (isCall) { arg = $(this.expression); } else { - $(this.comment); + $(this.comments); if (input.charAt(i) === '.' && $(/^\.{3}/)) { returner.variadic = true; if ($(";") && !isSemiColonSeperated) { @@ -902,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; @@ -951,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 }); @@ -984,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(); @@ -1004,7 +1085,7 @@ less.Parser = function Parser(env) { restore(); } - $(this.comment); + $(this.comments); if ($(/^when/)) { // Guard cond = expect(this.conditions, 'expected condition'); @@ -1048,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); @@ -1068,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); @@ -1084,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); } }, // @@ -1101,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)(" "); @@ -1109,7 +1190,13 @@ less.Parser = function Parser(env) { return new(tree.Combinator)(null); } }, - + // + // A CSS selector (see selector below) + // with less extensions e.g. the ability to extend and guard + // + lessSelector: function () { + return this.selector(true); + }, // // A CSS Selector // @@ -1118,30 +1205,36 @@ less.Parser = function Parser(env) { // // Selectors are made out of one or more Elements, see above. // - selector: function () { - var sel, e, elements = [], c, extend, extendList = []; + selector: function (isLess) { + var e, elements = [], c, extend, extendList = [], when, condition; - while ((extend = $(this.extend)) || (e = $(this.element))) { - if (extend) { + while ((isLess && (extend = $(this.extend))) || (isLess && (when = $(/^when/))) || (e = $(this.element))) { + if (when) { + condition = expect(this.conditions, 'expected condition'); + } else if (condition) { + error("CSS guard can only be used at the end of selector"); + } else if (extend) { extendList.push.apply(extendList, extend); } else { if (extendList.length) { 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); } + 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-]|\\.)+/); @@ -1175,20 +1268,25 @@ less.Parser = function Parser(env) { save(); - if (env.dumpLineNumbers) + if (env.dumpLineNumbers) { debugInfo = getDebugInfo(i, input, env); + } - while (s = $(this.selector)) { + while (s = $(this.lessSelector)) { selectors.push(s); - $(this.comment); - if (! $(',')) { break } - $(this.comment); + $(this.comments); + if (! $(',')) { break; } + if (s.condition) { + error("Guards are only currently allowed on a single selector"); + } + $(this.comments); } 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 @@ -1197,22 +1295,27 @@ less.Parser = function Parser(env) { } }, rule: function (tryAnonymous) { - var name, value, c = input.charAt(i), important; + 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.property)) { + if (name = $(this.variable) || $(this.ruleProperty)) { // prefer to try to parse first if its a variable or we are compressing // but always fallback on the other one value = !tryAnonymous && (env.compress || (name.charAt(0) === '@')) ? ($(this.value) || $(this.anonymousValue)) : ($(this.anonymousValue) || $(this.value)); + important = $(this.important); + if (name[name.length-1] === "+") { + merge = true; + name = name.substr(0, name.length - 1); + } if (value && $(this.end)) { - return new(tree.Rule)(name, value, important, memo, env.currentFileInfo); + return new (tree.Rule)(name, value, important, merge, memo, env.currentFileInfo); } else { furthest = i; restore(); @@ -1280,7 +1383,7 @@ less.Parser = function Parser(env) { break; } options[optionName] = value; - if (! $(',')) { break } + if (! $(',')) { break; } } } while (o); expect(')'); @@ -1288,7 +1391,7 @@ less.Parser = function Parser(env) { }, importOption: function() { - var opt = $(/^(less|css|multiple|once)/); + var opt = $(/^(less|css|multiple|once|inline|reference)/); if (opt) { return opt[1]; } @@ -1305,13 +1408,13 @@ less.Parser = function Parser(env) { e = $(this.value); if ($(')')) { if (p && e) { - nodes.push(new(tree.Paren)(new(tree.Rule)(p, e, null, i, env.currentFileInfo, true))); + nodes.push(new(tree.Paren)(new(tree.Rule)(p, e, null, null, i, env.currentFileInfo, true))); } else if (e) { nodes.push(new(tree.Paren)(e)); } else { return null; } - } else { return null } + } else { return null; } } } while (e); @@ -1326,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); @@ -1339,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); - if(env.dumpLineNumbers) + media = new(tree.Media)(rules, features, i, env.currentFileInfo); + if (env.dumpLineNumbers) { media.debugInfo = debugInfo; + } return media; } } @@ -1360,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; @@ -1373,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) { @@ -1422,11 +1527,11 @@ less.Parser = function Parser(env) { if (hasBlock) { if (rules = $(this.block)) { - return new(tree.Directive)(name, rules); + return new(tree.Directive)(name, rules, i, env.currentFileInfo); } } else { if ((value = hasExpression ? $(this.expression) : $(this.entity)) && $(';')) { - var directive = new(tree.Directive)(name, value); + var directive = new(tree.Directive)(name, value, i, env.currentFileInfo); if (env.dumpLineNumbers) { directive.debugInfo = getDebugInfo(i, input, env); } @@ -1446,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) { @@ -1475,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 = ($('/') || $('*')))) { @@ -1518,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 = $(/^(?:>=|=<|[<=>])/)) { @@ -1542,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); @@ -1563,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); @@ -1582,30 +1687,15 @@ less.Parser = function Parser(env) { if (name = $(/^(\*?-?[_a-zA-Z0-9-]+)\s*:/)) { return name[1]; } + }, + ruleProperty: function () { + var name; + + if (name = $(/^(\*?-?[_a-zA-Z0-9-]+)\s*(\+?)\s*:/)) { + return name[1] + (name[2] || ""); + } } } }; }; -if (less.mode === 'browser' || less.mode === 'rhino') { - // - // Used by `@import` directives - // - less.Parser.importer = function (path, currentFileInfo, callback, env) { - if (!/^([a-z-]+:)?\//.test(path) && currentFileInfo.currentDirectory) { - path = currentFileInfo.currentDirectory + path; - } - var sheetEnv = env.toSheet(path); - sheetEnv.processImports = false; - sheetEnv.currentFileInfo = currentFileInfo; - - // We pass `true` as 3rd argument, to force the reload of the import. - // This is so we can get the syntax tree as opposed to just the CSS output, - // as we need this to evaluate the current stylesheet. - loadStyleSheet(sheetEnv, - function (e, root, data, sheet, _, path) { - callback.call(null, e, root, path); - }, true); - }; -} - diff --git a/lib/less/rhino.js b/lib/less/rhino.js index fe917a2b..08f31102 100644 --- a/lib/less/rhino.js +++ b/lib/less/rhino.js @@ -1,5 +1,36 @@ +/*jshint rhino:true, unused: false */ +/*global name:true, less, loadStyleSheet */ var name; +function error(e, filename) { + + var content = "Error : " + filename + "\n"; + + filename = e.filename || filename; + + if (e.message) { + content += e.message + "\n"; + } + + var errorline = function (e, i, classname) { + if (e.extract[i]) { + content += + String(parseInt(e.line, 10) + (i - 1)) + + ":" + e.extract[i] + "\n"; + } + }; + + if (e.stack) { + content += e.stack; + } else if (e.extract) { + content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':\n'; + errorline(e, 0); + errorline(e, 1); + errorline(e, 2); + } + print(content); +} + function loadStyleSheet(sheet, callback, reload, remaining) { var endOfPath = Math.max(name.lastIndexOf('/'), name.lastIndexOf('\\')), sheetName = name.slice(0, endOfPath + 1) + sheet.href, @@ -61,7 +92,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); @@ -94,33 +125,4 @@ function writeFile(filename, content) { quit(1); } print("done"); -}(arguments)); - -function error(e, filename) { - - var content = "Error : " + filename + "\n"; - - filename = e.filename || filename; - - if (e.message) { - content += e.message + "\n"; - } - - var errorline = function (e, i, classname) { - if (e.extract[i]) { - content += - String(parseInt(e.line) + (i - 1)) + - ":" + e.extract[i] + "\n"; - } - }; - - if (e.stack) { - content += e.stack; - } else if (e.extract) { - content += 'on line ' + e.line + ', column ' + (e.column + 1) + ':\n'; - errorline(e, 0); - errorline(e, 1); - errorline(e, 2); - } - print(content); -} \ No newline at end of file +}(arguments)); \ 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 new file mode 100644 index 00000000..a9988c6f --- /dev/null +++ b/lib/less/to-css-visitor.js @@ -0,0 +1,199 @@ +(function (tree) { + tree.toCSSVisitor = function(env) { + this._visitor = new tree.visitor(this); + this._env = env; + }; + + tree.toCSSVisitor.prototype = { + isReplacing: true, + run: function (root) { + return this._visitor.visit(root); + }, + + visitRule: function (ruleNode, visitArgs) { + if (ruleNode.variable) { + return []; + } + return ruleNode; + }, + + visitMixinDefinition: function (mixinNode, visitArgs) { + return []; + }, + + visitExtend: function (extendNode, visitArgs) { + return []; + }, + + visitComment: function (commentNode, visitArgs) { + if (commentNode.isSilent(this._env)) { + return []; + } + 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 + // be considered illegal css as it has to be on the first line + if (this.charset) { + if (directiveNode.debugInfo) { + var comment = new tree.Comment("/* " + directiveNode.toCSS(this._env).replace(/\n/g, "")+" */\n"); + comment.debugInfo = directiveNode.debugInfo; + return this._visitor.visit(comment); + } + return []; + } + this.charset = true; + } + return directiveNode; + }, + + checkPropertiesInRoot: function(rules) { + var ruleNode; + for(var i = 0; i < rules.length; i++) { + ruleNode = rules[i]; + if (ruleNode instanceof tree.Rule && !ruleNode.variable) { + throw { message: "properties must be inside selector blocks, they cannot be in the root.", + index: ruleNode.index, filename: ruleNode.currentFileInfo ? ruleNode.currentFileInfo.filename : null}; + } + } + }, + + visitRuleset: function (rulesetNode, visitArgs) { + var rule, rulesets = []; + if (rulesetNode.firstRoot) { + this.checkPropertiesInRoot(rulesetNode.rules); + } + if (! rulesetNode.root) { + + 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; + } + return false; + } + }); + + // Compile rules and rulesets + for (var i = 0; i < rulesetNode.rules.length; i++) { + rule = rulesetNode.rules[i]; + + if (rule.rules) { + // visit because we are moving them out from being a child + rulesets.push(this._visitor.visit(rule)); + rulesetNode.rules.splice(i, 1); + i--; + continue; + } + } + // 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.accept(this._visitor); + } + visitArgs.visitDeeper = false; + + this._mergeRules(rulesetNode.rules); + this._removeDuplicateRules(rulesetNode.rules); + + // now decide whether we keep the ruleset + 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 === 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; + })); + } + }); + } + }; + +})(require('./tree')); \ No newline at end of file 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 2b10dd42..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 95d5a101..c5ad64fc 100644 --- a/lib/less/tree/color.js +++ b/lib/less/tree/color.js @@ -24,40 +24,36 @@ 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); }).concat(this.alpha).join(',' + (compress ? '' : ' ')) + ")"; } else { - var color = this.rgb.map(function (i) { - i = Math.round(i); - i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16); - return i.length === 1 ? '0' + i : i; - }).join(''); + var color = this.toRGB(); if (compress) { - color = color.split(''); + var splitcolor = color.split(''); // Convert color to short format - if (color[0] == color[1] && color[2] == color[3] && color[4] == color[5]) { - color = color[0] + color[2] + color[4]; - } else { - color = color.join(''); + if (splitcolor[1] === splitcolor[2] && splitcolor[3] === splitcolor[4] && splitcolor[5] === splitcolor[6]) { + color = '#' + splitcolor[1] + splitcolor[3] + splitcolor[5]; } } - return '#' + color; + return color; } }, @@ -80,6 +76,14 @@ tree.Color.prototype = { return new(tree.Color)(result, this.alpha + other.alpha); }, + toRGB: function () { + return '#' + this.rgb.map(function (i) { + i = Math.round(i); + i = (i > 255 ? 255 : (i < 0 ? 0 : i)).toString(16); + return i.length === 1 ? '0' + i : i; + }).join(''); + }, + toHSL: function () { var r = this.rgb[0] / 255, g = this.rgb[1] / 255, diff --git a/lib/less/tree/comment.js b/lib/less/tree/comment.js index 2123b4b9..c39606e0 100644 --- a/lib/less/tree/comment.js +++ b/lib/less/tree/comment.js @@ -1,15 +1,28 @@ (function (tree) { -tree.Comment = function (value, silent) { +tree.Comment = function (value, silent, index, currentFileInfo) { this.value = value; this.silent = !!silent; + this.currentFileInfo = currentFileInfo; }; tree.Comment.prototype = { type: "Comment", - toCSS: function (env) { - return env.compress ? '' : this.value; + genCSS: function (env, output) { + if (this.debugInfo) { + output.add(tree.debugInfo(env, this), this.currentFileInfo, this.index); + } + output.add(this.value.trim()); //TODO shouldn't need to trim, we shouldn't grab the \n }, - eval: function () { return this } + toCSS: tree.toCSS, + isSilent: function(env) { + var isReference = (this.currentFileInfo && this.currentFileInfo.reference && !this.isReferenced), + isCompressed = env.compress && !this.value.match(/^\/\*!/); + return this.silent || isReference || isCompressed; + }, + eval: function () { return this; }, + markReferenced: function () { + this.isReferenced = true; + } }; })(require('../tree')); 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..c64cd89a 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()); } @@ -35,8 +35,9 @@ tree.Dimension.prototype = { if (env && env.compress) { // Zero values doesn't need a unit - if (value === 0 && !this.unit.isAngle()) { - return strValue; + if (value === 0 && this.unit.isLength()) { + 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; - }, + isLength: function () { + return Boolean(this.toCSS().match(/px|em|%|in|cm|mm|pc|pt|ex/)); + }, - 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 3fb33e8d..64082593 100644 --- a/lib/less/tree/directive.js +++ b/lib/less/tree/directive.js @@ -1,44 +1,60 @@ (function (tree) { -tree.Directive = function (name, value) { +tree.Directive = function (name, value, index, currentFileInfo) { this.name = name; if (Array.isArray(value)) { - this.ruleset = new(tree.Ruleset)([], value); - this.ruleset.allowImports = true; + this.rules = [new(tree.Ruleset)([], value)]; + this.rules[0].allowImports = true; } else { this.value = value; } + this.currentFileInfo = currentFileInfo; + }; tree.Directive.prototype = { type: "Directive", accept: function (visitor) { - this.ruleset = visitor.visit(this.ruleset); + this.rules = visitor.visit(this.rules); this.value = visitor.visit(this.value); }, - toCSS: function (env) { - if (this.ruleset) { - this.ruleset.root = true; - return this.name + (env.compress ? '{' : ' {\n ') + - this.ruleset.toCSS(env).trim().replace(/\n/g, '\n ') + - (env.compress ? '}': '\n}\n'); + genCSS: function (env, output) { + output.add(this.name, this.currentFileInfo, this.index); + if (this.rules) { + 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.ruleset) { + if (this.rules) { env.frames.unshift(this); - evaldDirective = new(tree.Directive)(this.name); - evaldDirective.ruleset = this.ruleset.eval(env); + evaldDirective = new(tree.Directive)(this.name, null, this.index, this.currentFileInfo); + evaldDirective.rules = [this.rules[0].eval(env)]; + evaldDirective.rules[0].root = true; env.frames.shift(); } return evaldDirective; }, - variable: function (name) { return tree.Ruleset.prototype.variable.call(this.ruleset, name) }, - find: function () { return tree.Ruleset.prototype.find.apply(this.ruleset, arguments) }, - rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.ruleset) } + 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]); }, + markReferenced: function () { + var i, rules; + this.isReferenced = true; + if (this.rules) { + rules = this.rules[0].rules; + for (i = 0; i < rules.length; i++) { + if (rules[i].markReferenced) { + rules[i].markReferenced(); + } + } + } + } }; })(require('../tree')); diff --git a/lib/less/tree/element.js b/lib/less/tree/element.js index 79cf6d18..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 ? '>' : ' > ', - '|' : 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 9a93baaf..a2ab3d76 100644 --- a/lib/less/tree/import.js +++ b/lib/less/tree/import.js @@ -12,16 +12,14 @@ // 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; this.features = features; this.currentFileInfo = currentFileInfo; - if (this.options.less !== undefined) { - this.css = !this.options.less; + if (this.options.less !== undefined || this.options.inline) { + this.css = !this.options.less || this.options.inline; } else { var pathValue = this.getPath(); if (pathValue && /css([\?;].*)?$/.test(pathValue)) { @@ -44,17 +42,22 @@ tree.Import.prototype = { accept: function (visitor) { this.features = visitor.visit(this.features); this.path = visitor.visit(this.path); - this.root = visitor.visit(this.root); - }, - toCSS: function (env) { - var features = this.features ? ' ' + this.features.toCSS(env) : ''; - - if (this.css) { - return "@import " + this.path.toCSS() + features + ';\n'; - } else { - return ""; + if (!this.options.inline) { + this.root = visitor.visit(this.root); } }, + genCSS: function (env, output) { + if (this.css) { + 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; @@ -70,13 +73,18 @@ tree.Import.prototype = { evalPath: function (env) { var path = this.path.eval(env); var rootpath = this.currentFileInfo && this.currentFileInfo.rootpath; - if (rootpath && !(path instanceof tree.URL)) { - var pathValue = path.value; - // Add the base path if the import is relative - if (pathValue && env.isPathRelative(pathValue)) { - path.value = rootpath + pathValue; + + if (!(path instanceof tree.URL)) { + if (rootpath) { + var pathValue = path.value; + // Add the base path if the import is relative + if (pathValue && env.isPathRelative(pathValue)) { + path.value = rootpath + pathValue; + } } + path.value = env.normalizePath(path.value); } + return path; }, eval: function (env) { @@ -84,7 +92,10 @@ tree.Import.prototype = { if (this.skip) { return []; } - if (this.css) { + if (this.options.inline) { + var contents = new(tree.Anonymous)(this.root); + return this.features ? new(tree.Media)([contents], this.features.value) : [contents]; + } else if (this.css) { var newImport = new(tree.Import)(this.evalPath(env), features, this.options, this.index); if (!newImport.css && this.error) { throw this.error; diff --git a/lib/less/tree/javascript.js b/lib/less/tree/javascript.js index eaa9c0fe..dc7910c4 100644 --- a/lib/less/tree/javascript.js +++ b/lib/less/tree/javascript.js @@ -19,11 +19,12 @@ tree.JavaScript.prototype = { try { expression = new(Function)('return (' + expression + ')'); } catch (e) { - throw { message: "JavaScript evaluation error: `" + expression + "`" , + throw { message: "JavaScript evaluation error: " + e.message + " from `" + expression + "`" , index: this.index }; } 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 0fea084e..c5cdbd78 100644 --- a/lib/less/tree/media.js +++ b/lib/less/tree/media.js @@ -1,34 +1,36 @@ (function (tree) { -tree.Media = function (value, features) { +tree.Media = function (value, features, index, currentFileInfo) { + this.index = index; + this.currentFileInfo = currentFileInfo; + var selectors = this.emptySelectors(); this.features = new(tree.Value)(features); - this.ruleset = new(tree.Ruleset)(selectors, value); - this.ruleset.allowImports = true; + this.rules = [new(tree.Ruleset)(selectors, value)]; + this.rules[0].allowImports = true; }; tree.Media.prototype = { type: "Media", accept: function (visitor) { this.features = visitor.visit(this.features); - this.ruleset = visitor.visit(this.ruleset); + this.rules = visitor.visit(this.rules); }, - toCSS: function (env) { - var features = this.features.toCSS(env); - - return '@media ' + features + (env.compress ? '{' : ' {\n ') + - this.ruleset.toCSS(env).trim().replace(/\n/g, '\n ') + - (env.compress ? '}': '\n}\n'); + 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 = []; env.mediaPath = []; } - var media = new(tree.Media)([], []); + var media = new(tree.Media)([], [], this.index, this.currentFileInfo); if(this.debugInfo) { - this.ruleset.debugInfo = this.debugInfo; + this.rules[0].debugInfo = this.debugInfo; media.debugInfo = this.debugInfo; } var strictMathBypass = false; @@ -48,21 +50,30 @@ tree.Media.prototype = { env.mediaPath.push(media); env.mediaBlocks.push(media); - env.frames.unshift(this.ruleset); - media.ruleset = this.ruleset.eval(env); + env.frames.unshift(this.rules[0]); + media.rules = [this.rules[0].eval(env)]; env.frames.shift(); 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.ruleset, name) }, - find: function () { return tree.Ruleset.prototype.find.apply(this.ruleset, arguments) }, - rulesets: function () { return tree.Ruleset.prototype.rulesets.apply(this.ruleset) }, + 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); - return [new(tree.Selector)([el])]; + var el = new(tree.Element)('', '&', this.index, this.currentFileInfo); + return [new(tree.Selector)([el], null, null, this.index, this.currentFileInfo)]; + }, + markReferenced: function () { + var i, rules = this.rules[0].rules; + this.isReferenced = true; + for (i = 0; i < rules.length; i++) { + if (rules[i].markReferenced) { + rules[i].markReferenced(); + } + } }, evalTop: function (env) { @@ -130,7 +141,7 @@ tree.Media.prototype = { } }, bubbleSelectors: function (selectors) { - this.ruleset = new(tree.Ruleset)(selectors.slice(0), [this.ruleset]); + this.rules = [new(tree.Ruleset)(selectors.slice(0), [this.rules[0]])]; } }; diff --git a/lib/less/tree/mixin.js b/lib/less/tree/mixin.js index 09f08731..ebd32036 100644 --- a/lib/less/tree/mixin.js +++ b/lib/less/tree/mixin.js @@ -15,7 +15,7 @@ tree.mixin.Call.prototype = { this.arguments = visitor.visit(this.arguments); }, eval: function (env) { - var mixins, mixin, args, rules = [], match = false, i, m, f, isRecursive, isOneFound; + var mixins, mixin, args, rules = [], match = false, i, m, f, isRecursive, isOneFound, rule; args = this.arguments && this.arguments.map(function (a) { return { name: a.name, value: a.value.eval(env) }; @@ -39,6 +39,9 @@ tree.mixin.Call.prototype = { if (mixin.matchArgs(args, env)) { if (!mixin.matchCondition || mixin.matchCondition(args, env)) { try { + if (!(mixin instanceof tree.mixin.Definition)) { + mixin = new tree.mixin.Definition("", [], mixin.rules, null, false); + } Array.prototype.push.apply( rules, mixin.eval(env, args, this.important).rules); } catch (e) { @@ -49,6 +52,14 @@ tree.mixin.Call.prototype = { } } if (match) { + if (!this.currentFileInfo || !this.currentFileInfo.reference) { + for (i = 0; i < rules.length; i++) { + rule = rules[i]; + if (rule.markReferenced) { + rule.markReferenced(); + } + } + } return rules; } } @@ -80,7 +91,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; @@ -88,8 +99,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 = []; @@ -101,13 +112,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), @@ -143,7 +154,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]; @@ -185,7 +196,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))); @@ -208,12 +219,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 876149f8..20562094 100644 --- a/lib/less/tree/rule.js +++ b/lib/less/tree/rule.js @@ -1,16 +1,14 @@ (function (tree) { -tree.Rule = function (name, value, important, index, currentFileInfo, inline) { +tree.Rule = function (name, value, important, merge, index, currentFileInfo, inline) { this.name = name; this.value = (value instanceof tree.Value) ? value : new(tree.Value)([value]); this.important = important ? ' ' + important.trim() : ''; + this.merge = merge; this.index = index; this.currentFileInfo = currentFileInfo; this.inline = inline || false; - - if (name.charAt(0) === '@') { - this.variable = true; - } else { this.variable = false } + this.variable = (name.charAt(0) === '@'); }; tree.Rule.prototype = { @@ -18,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) { @@ -43,6 +39,7 @@ tree.Rule.prototype = { return new(tree.Rule)(this.name, this.value.eval(env), this.important, + this.merge, this.index, this.currentFileInfo, this.inline); } finally { @@ -55,6 +52,7 @@ tree.Rule.prototype = { return new(tree.Rule)(this.name, this.value, "!important", + this.merge, this.index, this.currentFileInfo, this.inline); } }; diff --git a/lib/less/tree/ruleset.js b/lib/less/tree/ruleset.js index 76c7ec41..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); } } @@ -116,13 +125,23 @@ tree.Ruleset.prototype = { matchArgs: function (args) { return !args || args.length === 0; }, + matchCondition: function (args, env) { + var lastSelector = this.selectors[this.selectors.length-1]; + if (lastSelector.condition && + !lastSelector.condition.eval( + new(tree.evalEnv)(env, + env.frames))) { + return false; + } + return true; + }, resetCache: function () { this._rulesets = null; this._variables = null; 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) { @@ -142,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) { @@ -164,106 +183,104 @@ 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; - // Compile rules and rulesets - for (var i = 0; i < this.rules.length; i++) { - rule = this.rules[i]; + env.tabLevel = (env.tabLevel || 0); - if (rule.rules || (rule instanceof tree.Media)) { - rulesets.push(rule.toCSS(env)); - } else if (rule instanceof tree.Directive) { - var cssValue = rule.toCSS(env); - // Output only the first @charset definition as such - convert the others - // to comments in case debug is enabled - if (rule.name === "@charset") { - // Only output the debug info together with subsequent @charset definitions - // a comment (or @media statement) before the actual @charset directive would - // be considered illegal css as it has to be on the first line - if (env.charset) { - if (rule.debugInfo) { - rulesets.push(tree.debugInfo(env, rule)); - rulesets.push(new tree.Comment("/* "+cssValue.replace(/\n/g, "")+" */\n").toCSS(env)); - } - continue; - } - env.charset = true; - } - rulesets.push(cssValue); - } else if (rule instanceof tree.Comment) { - if (!rule.silent) { - if (this.root) { - rulesets.push(rule.toCSS(env)); - } else { - rules.push(rule.toCSS(env)); - } - } - } else { - if (rule.toCSS && !rule.variable) { - if (this.firstRoot && rule instanceof tree.Rule) { - throw { message: "properties must be inside selector blocks, they cannot be in the root.", - index: rule.index, filename: rule.currentFileInfo ? rule.currentFileInfo.filename : null}; - } - rules.push(rule.toCSS(env)); - } else if (rule.value && !rule.variable) { - 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); - // 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]); - } + if (debugInfo) { + output.add(debugInfo); + output.add(tabSetStr); + } + + 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; } - rules = _rules; + if (i + 1 < this.paths.length) { + output.add(env.compress ? ',' : (',\n' + tabSetStr)); + } + } - css.push(debugInfo + selector + - (env.compress ? '{' : ' {\n ') + - rules.join(env.compress ? '' : '\n ') + - (env.compress ? '}' : '\n}\n')); + output.add((env.compress ? '{' : ' {\n') + tabRuleStr); + } + + // 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; } } - css.push(rulesets); - return css.join('') + (env.compress ? '\n' : ''); + 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' : ''); + }, + + toCSS: tree.toCSS, + + markReferenced: function () { + for (var s = 0; s < this.selectors.length; s++) { + this.selectors[s].markReferenced(); + } }, joinSelectors: function (paths, context, selectors) { @@ -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 @@ -364,11 +381,11 @@ tree.Ruleset.prototype = { if (sel.length > 0) { newSelectorPath = sel.slice(0); lastSelector = newSelectorPath.pop(); - newJoinedSelector = new(tree.Selector)(lastSelector.elements.slice(0), selector.extendList); + newJoinedSelector = selector.createDerived(lastSelector.elements.slice(0)); newJoinedSelectorEmpty = false; } else { - newJoinedSelector = new(tree.Selector)([], selector.extendList); + newJoinedSelector = selector.createDerived([]); } //put together the parent selectors after the join @@ -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,19 +435,19 @@ 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 if (sel.length > 0) { - sel[sel.length - 1] = new(tree.Selector)(sel[sel.length - 1].elements.concat(elements), sel[sel.length - 1].extendList); + sel[sel.length - 1] = sel[sel.length - 1].createDerived(sel[sel.length - 1].elements.concat(elements)); } else { sel.push(new(tree.Selector)(elements)); diff --git a/lib/less/tree/selector.js b/lib/less/tree/selector.js index f122e55f..ac38b993 100644 --- a/lib/less/tree/selector.js +++ b/lib/less/tree/selector.js @@ -1,14 +1,28 @@ (function (tree) { -tree.Selector = function (elements, extendList) { +tree.Selector = function (elements, extendList, condition, index, currentFileInfo, isReferenced) { this.elements = elements; this.extendList = extendList || []; + this.condition = condition; + this.currentFileInfo = currentFileInfo || {}; + this.isReferenced = isReferenced; + if (!condition) { + this.evaldCondition = true; + } }; tree.Selector.prototype = { type: "Selector", accept: function (visitor) { this.elements = visitor.visit(this.elements); - this.extendList = visitor.visit(this.extendList) + this.extendList = visitor.visit(this.extendList); + 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; + return newSelector; }, match: function (other) { var elements = this.elements, @@ -32,30 +46,36 @@ tree.Selector.prototype = { return true; }, eval: function (env) { - return new(tree.Selector)(this.elements.map(function (e) { + var evaldCondition = this.condition && this.condition.eval(env); + + return this.createDerived(this.elements.map(function (e) { return e.eval(env); }), this.extendList.map(function(extend) { 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; + }, + getIsReferenced: function() { + return !this.currentFileInfo.reference || this.isReferenced; + }, + getIsOutput: function() { + return this.evaldCondition; } }; 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..79c2879c 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; @@ -24,6 +27,8 @@ tree.URL.prototype = { val.value = rootpath + val.value; } + val.value = ctx.normalizePath(val.value); + return new(tree.URL)(val, null); } }; diff --git a/lib/less/tree/value.js b/lib/less/tree/value.js index 5aae88eb..93630c3c 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 && 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 7323281e..52f734f1 100644 --- a/lib/less/visitor.js +++ b/lib/less/visitor.js @@ -39,6 +39,7 @@ for(i = 0; i < nodes.length; i++) { var evald = this.visit(nodes[i]); if (evald instanceof Array) { + evald = this.flatten(evald); newNodes = newNodes.concat(evald); } else { newNodes.push(evald); @@ -48,6 +49,20 @@ return newNodes; } return nodes; + }, + doAccept: function (node) { + node.accept(this); + }, + flatten: function(arr, master) { + return arr.reduce(this.flattenReduce.bind(this), master || []); + }, + flattenReduce: function(sum, element) { + if (element instanceof Array) { + sum = this.flatten(element, sum); + } else { + sum.push(element); + } + return sum; } }; diff --git a/package.json b/package.json index b20dba56..db7423c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "less", - "version": "1.4.2", + "version": "1.5.0-b1", "description": "Leaner CSS", "homepage": "http://lesscss.org", "author": "Alexis Sellier ", @@ -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", - "ycssmin": ">=1.0.1" + "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 0272279b..b8724641 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'; @@ -15,11 +14,11 @@ var createTestRunnerPage = function(dir, exclude, testSuiteName, dir2) { readDirFilesSync(path.join("test", dir, 'less', dir2 || ""), /\.less$/, function (file) { var name = path.basename(file, '.less'), id = (dir ? dir + '-' : "") + 'less-' + (dir2 ? dir2 + "-" : "") + name; - + if (exclude && name.match(exclude)) { return; } - - output += '\n'; - output += '\n'; + + output += '\n'; + output += '\n'; }); output += String(fs.readFileSync(path.join('test/browser', 'template.htm'))).replace("{runner-name}", testSuiteName); @@ -33,14 +32,17 @@ var removeFiles = function(dir, regex) { console.log("Failed to delete " + file); }); }); -} +}; removeFiles("test/browser", /test-runner-[a-zA-Z-]*\.htm$/); createTestRunnerPage("", /javascript|urls/, "main"); createTestRunnerPage("", null, "legacy", "legacy"); createTestRunnerPage("", /javascript/, "errors", "errors"); +createTestRunnerPage("", null, "no-js-errors", "no-js-errors"); +createTestRunnerPage("browser", null, "console-errors", "console-errors"); createTestRunnerPage("browser", null, "browser"); createTestRunnerPage("browser", null, "relative-urls", "relative-urls"); createTestRunnerPage("browser", null, "rootpath", "rootpath"); createTestRunnerPage("browser", null, "rootpath-relative", "rootpath-relative"); -createTestRunnerPage("browser", null, "production"); \ No newline at end of file +createTestRunnerPage("browser", null, "production"); +createTestRunnerPage("browser", null, "modify-vars", "modify-vars"); diff --git a/test/browser/common.js b/test/browser/common.js index 74f12fe0..5460fd44 100644 --- a/test/browser/common.js +++ b/test/browser/common.js @@ -13,8 +13,8 @@ var testLessEqualsInDocument = function() { testLessInDocument(testSheet); }; -var testLessErrorsInDocument = function() { - testLessInDocument(testErrorSheet); +var testLessErrorsInDocument = function(isConsole) { + testLessInDocument(isConsole ? testErrorSheetConsole : testErrorSheet); }; var testLessInDocument = function(testFunc) { @@ -43,7 +43,7 @@ var testSheet = function(sheet) { runs(function() { // use sheet to do testing - expect(lessOutput).toEqual(expectedOutput.text); + expect(expectedOutput.text).toEqual(lessOutput); }); }); }; @@ -76,7 +76,7 @@ var testErrorSheet = function(sheet) { .replace("{pathrel}", "") .replace("{pathhref}", "http://localhost:8081/less/errors/") .replace("{404status}", " (404)"); - expect(actualErrorMsg).toEqual(errorTxt); + expect(errorTxt).toEqual(actualErrorMsg); if (errorTxt == actualErrorMsg) { actualErrorElement.style.display = "none"; } @@ -84,6 +84,41 @@ var testErrorSheet = function(sheet) { }); }; +var testErrorSheetConsole = function(sheet) { + it(sheet.id + " should match an error", function() { + var lessHref = sheet.href, + id = sheet.id.replace(/^original-less:/, "less-error-message:"), + errorHref = lessHref.replace(/.less$/, ".txt"), + errorFile = loadFile(errorHref), + actualErrorElement = document.getElementById(id), + actualErrorMsg = logMessages[logMessages.length - 1]; + + describe("the error", function() { + expect(actualErrorElement).toBe(null); + + }); + + /*actualErrorMsg = actualErrorElement.innerText + .replace(/\n\d+/g, function(lineNo) { return lineNo + " "; }) + .replace(/\n\s*in /g, " in ") + .replace("\n\n", "\n");*/ + + waitsFor(function() { + return errorFile.loaded; + }, "failed to load expected outout", 10000); + + runs(function() { + var errorTxt = errorFile.text + .replace("{path}", "") + .replace("{pathrel}", "") + .replace("{pathhref}", "http://localhost:8081/browser/less/") + .replace("{404status}", " (404)") + .trim(); + expect(errorTxt).toEqual(actualErrorMsg); + }); + }); +}; + var loadFile = function(href) { var request = new XMLHttpRequest(), response = { loaded: false, text: ""}; diff --git a/test/browser/css/modify-vars/simple.css b/test/browser/css/modify-vars/simple.css new file mode 100644 index 00000000..4cb81bac --- /dev/null +++ b/test/browser/css/modify-vars/simple.css @@ -0,0 +1,7 @@ +.testisimported { + color: gainsboro; +} +.test { + color1: #008000; + color2: #800080; +} diff --git a/test/browser/css/relative-urls/urls.css b/test/browser/css/relative-urls/urls.css index d0da4990..96bbf604 100644 --- a/test/browser/css/relative-urls/urls.css +++ b/test/browser/css/relative-urls/urls.css @@ -1,5 +1,4 @@ @import "http://localhost:8081/browser/less/imports/modify-this.css"; - @import "http://localhost:8081/browser/less/imports/modify-again.css"; .modify { my-url: url("http://localhost:8081/browser/less/imports/a.png"); 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/rootpath/urls.css b/test/browser/css/rootpath/urls.css index a3ef5d9e..9618e8e5 100644 --- a/test/browser/css/rootpath/urls.css +++ b/test/browser/css/rootpath/urls.css @@ -1,5 +1,4 @@ @import "https://www.github.com/modify-this.css"; - @import "https://www.github.com/modify-again.css"; .modify { my-url: url("https://www.github.com/a.png"); diff --git a/test/browser/css/urls.css b/test/browser/css/urls.css index 19ba27e6..9a9f99e3 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"); @@ -35,15 +34,20 @@ url: url('http://localhost:8081/browser/less/Trebuchet'); } #data-uri { - uri: url('http://localhost:8081/browser/less/../../data/image.jpg'); + uri: url('http://localhost:8081/data/image.jpg'); } #data-uri-guess { - uri: url('http://localhost:8081/browser/less/../../data/image.jpg'); + uri: url('http://localhost:8081/data/image.jpg'); } #data-uri-ascii { - uri-1: url('http://localhost:8081/browser/less/../../data/page.html'); - uri-2: url('http://localhost:8081/browser/less/../../data/page.html'); + uri-1: url('http://localhost:8081/data/page.html'); + uri-2: url('http://localhost:8081/data/page.html'); } #data-uri-toobig { - uri: url('http://localhost:8081/browser/less/../../data/data-uri-fail.png'); + uri: url('http://localhost:8081/data/data-uri-fail.png'); +} +#svg-functions { + background-image: url('data:image/svg+xml,'); + background-image: url('data:image/svg+xml,'); + background-image: url('data:image/svg+xml,'); } diff --git a/test/browser/es5.js b/test/browser/es5.js new file mode 100644 index 00000000..c4fcc802 --- /dev/null +++ b/test/browser/es5.js @@ -0,0 +1,27 @@ +/* + PhantomJS does not implement bind. this is from + https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind + */ +if (!Function.prototype.bind) { + Function.prototype.bind = function (oThis) { + if (typeof this !== "function") { + // closest thing possible to the ECMAScript 5 internal IsCallable function + throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable"); + } + + var aArgs = Array.prototype.slice.call(arguments, 1), + fToBind = this, + fNOP = function () {}, + fBound = function () { + return fToBind.apply(this instanceof fNOP && oThis + ? this + : oThis, + aArgs.concat(Array.prototype.slice.call(arguments))); + }; + + fNOP.prototype = this.prototype; + fBound.prototype = new fNOP(); + + return fBound; + }; +} \ No newline at end of file diff --git a/test/browser/less/console-errors/test-error.less b/test/browser/less/console-errors/test-error.less new file mode 100644 index 00000000..7c60c004 --- /dev/null +++ b/test/browser/less/console-errors/test-error.less @@ -0,0 +1,3 @@ +.a { + prop: (3 / #fff); +} \ No newline at end of file diff --git a/test/browser/less/console-errors/test-error.txt b/test/browser/less/console-errors/test-error.txt new file mode 100644 index 00000000..643bd593 --- /dev/null +++ b/test/browser/less/console-errors/test-error.txt @@ -0,0 +1,2 @@ +less: OperationError: Can't substract or divide a color from a number in {pathhref}console-errors/test-error.less on line null, column 0: +1 prop: (3 / #fff); diff --git a/test/browser/less/modify-vars/imports/simple2.less b/test/browser/less/modify-vars/imports/simple2.less new file mode 100644 index 00000000..49fcf875 --- /dev/null +++ b/test/browser/less/modify-vars/imports/simple2.less @@ -0,0 +1,4 @@ +@var2: blue; +.testisimported { + color: gainsboro; +} \ No newline at end of file diff --git a/test/browser/less/modify-vars/simple.less b/test/browser/less/modify-vars/simple.less new file mode 100644 index 00000000..64d99302 --- /dev/null +++ b/test/browser/less/modify-vars/simple.less @@ -0,0 +1,6 @@ +@import "imports/simple2"; +@var1: red; +.test { + color1: @var1; + color2: @var2; +} \ No newline at end of file diff --git a/test/browser/less/urls.less b/test/browser/less/urls.less index 1f39a921..e640693b 100644 --- a/test/browser/less/urls.less +++ b/test/browser/less/urls.less @@ -47,3 +47,11 @@ #data-uri-toobig { uri: data-uri('../../data/data-uri-fail.png'); } +#svg-functions { + background-image: svg-gradient(to bottom, black, white); + background-image: svg-gradient(to bottom, black, orange 3%, white); + @green_5: green 5%; + @orange_percentage: 3%; + @orange_color: orange; + background-image: svg-gradient(to bottom, (mix(black, white) + #444) 1%, @orange_color @orange_percentage, ((@green_5)), white 95%); +} diff --git a/test/browser/runner-browser.js b/test/browser/runner-browser.js index 79c5e1c9..e34b1790 100644 --- a/test/browser/runner-browser.js +++ b/test/browser/runner-browser.js @@ -1,7 +1,42 @@ describe("less.js browser behaviour", function() { testLessEqualsInDocument(); - + it("has some log messages", function() { expect(logMessages.length).toBeGreaterThan(0); }); + + // test inline less in style tags by grabbing an assortment of less files and doing `@import`s + var testFiles = ['charsets', 'colors', 'comments', 'css-3', 'strings', 'media', 'mixins'], + testSheets = []; + + // setup style tags with less and link tags pointing to expected css output + for (var i = 0; i < testFiles.length; i++) { + var file = testFiles[i], + lessPath = '/less/' + file + '.less', + cssPath = '/css/' + file + '.css', + lessStyle = document.createElement('style'), + cssLink = document.createElement('link'), + lessText = '@import "' + lessPath + '";'; + lessStyle.type = 'text/less'; + lessStyle.id = file; + if (lessStyle.styleSheet) { + lessStyle.styleSheet.cssText = lessText; + } else { + lessStyle.innerHTML = lessText; + } + cssLink.rel = 'stylesheet'; + cssLink.type = 'text/css'; + cssLink.href = cssPath; + cssLink.id = 'expected-' + file; + + var head = document.getElementsByTagName('head')[0]; + head.appendChild(lessStyle); + head.appendChild(cssLink); + testSheets[i] = lessStyle; + } + + for (var i = 0; i < testFiles.length; i++) { + var sheet = testSheets[i]; + testSheet(sheet); + } }); \ No newline at end of file diff --git a/test/browser/runner-console-errors.js b/test/browser/runner-console-errors.js new file mode 100644 index 00000000..7af7bdb1 --- /dev/null +++ b/test/browser/runner-console-errors.js @@ -0,0 +1,5 @@ +less.errorReporting = 'console'; + +describe("less.js error reporting console test", function() { + testLessErrorsInDocument(true); +}); \ No newline at end of file diff --git a/test/browser/runner-modify-vars.js b/test/browser/runner-modify-vars.js new file mode 100644 index 00000000..4918d8f8 --- /dev/null +++ b/test/browser/runner-modify-vars.js @@ -0,0 +1,14 @@ + +setTimeout(function(){ + less.modifyVars({var1: "green", var2: "purple"}); +}, 1000); + +describe("less.js modify vars", function() { + testLessEqualsInDocument(); + it("Should log only 2 XHR requests", function() { + var xhrLogMessages = logMessages.filter(function(item) { + return /XHR: Getting '/.test(item); + }) + expect(xhrLogMessages.length).toEqual(2); + }); +}); \ No newline at end of file diff --git a/test/browser/runner-no-js-errors.js b/test/browser/runner-no-js-errors.js new file mode 100644 index 00000000..7c53aa98 --- /dev/null +++ b/test/browser/runner-no-js-errors.js @@ -0,0 +1,6 @@ +less.strictUnits = true; +less.javascriptEnabled = false; + +describe("less.js javascript disabled error tests", function() { + testLessErrorsInDocument(); +}); \ No newline at end of file diff --git a/test/browser/template.htm b/test/browser/template.htm index c000fdc2..41be7c0e 100644 --- a/test/browser/template.htm +++ b/test/browser/template.htm @@ -1,9 +1,10 @@ + - + diff --git a/test/css/comments.css b/test/css/comments.css index 03e82de4..b85f5b4f 100644 --- a/test/css/comments.css +++ b/test/css/comments.css @@ -26,12 +26,12 @@ */ /* @group Variables ------------------- */ -#comments { +#comments, +.comments { /**/ color: red; /* A C-style comment */ /* A C-style comment */ - background-color: orange; font-size: 12px; /* lost comment */ @@ -53,12 +53,17 @@ -moz-border-radius: 8px /* moz only with operation */; } .test { - color: 1px //put in @b - causes problems! --->; + color: 1px; } #last { color: #0000ff; } -/* *//* { *//* *//* *//* */#div { +/* */ +/* { */ +/* */ +/* */ +/* */ +#div { color: #A33; } /* } */ diff --git a/test/css/compression/compression.css b/test/css/compression/compression.css index 2f831a9d..ada7d3f3 100644 --- a/test/css/compression/compression.css +++ b/test/css/compression/compression.css @@ -1,2 +1,4 @@ -#colours{color1:#fea;color2:#fea;color3:rgba(255,238,170,0.1);string:"#ffeeaa"} -dimensions{val:.1px;val:0;val:4cm;val:.2;val:5;angles-must-have-unit:0deg;width:auto\9} +#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;durations-must-have-unit:0s;length-doesnt-have-unit:0;width:auto\9} \ No newline at end of file diff --git a/test/css/css-3.css b/test/css/css-3.css index 66df4516..c2b98d46 100644 --- a/test/css/css-3.css +++ b/test/css/css-3.css @@ -97,16 +97,16 @@ p::before { font-size: 10px; } @namespace foo url(http://www.example.com); -foo | h1 { +foo|h1 { color: blue; } -foo | * { +foo|* { color: yellow; } -| h1 { +|h1 { color: red; } -* | h1 { +*|h1 { color: green; } h1 { diff --git a/test/css/css-guards.css b/test/css/css-guards.css new file mode 100644 index 00000000..dbb27dae --- /dev/null +++ b/test/css/css-guards.css @@ -0,0 +1,18 @@ +.light { + color: green; +} +.see-the { + color: orange; +} +.hide-the { + color: green; +} +.multiple-conditions-1 { + color: red; +} +.inheritance .test { + color: black; +} +.inheritance:hover { + color: pink; +} diff --git a/test/css/extend-selector.css b/test/css/extend-selector.css index dc03d1cd..da47254b 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; } @@ -70,3 +70,11 @@ div.ext7, .attributes .attributes .attribute-test { extend: attributes2; } +.header .header-nav, +.footer .footer-nav { + background: red; +} +.header .header-nav:before, +.footer .footer-nav:before { + background: blue; +} diff --git a/test/css/functions.css b/test/css/functions.css index eacebd8d..c1781b6c 100644 --- a/test/css/functions.css +++ b/test/css/functions.css @@ -26,6 +26,7 @@ luma-cyan: 79%; luma-white-alpha: 50%; contrast-filter: contrast(30%); + saturate-filter: saturate(5%); contrast-white: #000000; contrast-black: #ffffff; contrast-red: #ffffff; @@ -80,6 +81,11 @@ pow: 64px; pow: 64; pow: 27; + min: 0; + min: min("junk", 5); + min: 3pt; + max: 3; + max: max(8%, 1cm); percentage: 20%; color: #ff0011; tint: #898989; diff --git a/test/css/import-inline.css b/test/css/import-inline.css new file mode 100644 index 00000000..f198d3c1 --- /dev/null +++ b/test/css/import-inline.css @@ -0,0 +1,5 @@ +this isn't very valid CSS. +@media (min-width: 600px) { + #css { color: yellow; } + +} diff --git a/test/css/import-reference.css b/test/css/import-reference.css new file mode 100644 index 00000000..c2d055a7 --- /dev/null +++ b/test/css/import-reference.css @@ -0,0 +1,55 @@ +/* + The media statement above is invalid (no selector) + We should ban invalid media queries with properties and no selector? +*/ +.visible { + color: red; +} +.visible .c { + color: green; +} +.visible { + color: green; +} +.visible:hover { + color: green; +} +.visible { + color: green; +} +.only-with-visible + .visible, +.visible + .only-with-visible, +.visible + .visible { + color: green; +} +.only-with-visible + .visible .sub, +.visible + .only-with-visible .sub, +.visible + .visible .sub { + color: green; +} +.b { + color: red; + color: green; +} +.b .c { + color: green; +} +.b:hover { + color: green; +} +.b { + color: green; +} +.b + .b { + color: green; +} +.b + .b .sub { + color: green; +} +.y { + pulled-in: yes; +} +/* comment pulled in */ +.visible { + extend: test; +} 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 5aa1d4e8..437292ba 100644 --- a/test/css/media.css +++ b/test/css/media.css @@ -52,9 +52,6 @@ .sidebar { width: 500px; } -} -@media a { - } @media a and b { .first .second .third { @@ -106,7 +103,9 @@ } } @media only screen and (max-width: 200px) { - width: 480px; + body { + width: 480px; + } } @media print { @page :left { @@ -119,7 +118,8 @@ margin: 1cm; } @page :first { - size: 8.5in 11in;@top-left { + size: 8.5in 11in; + @top-left { margin: 1cm; } @top-left-corner { @@ -201,3 +201,13 @@ body { font-size: 11px; } } +@media (min-width: 480px) { + .nav-justified > li { + display: table-cell; + } +} +@media (min-width: 768px) and (min-width: 480px) { + .menu > li { + display: table-cell; + } +} diff --git a/test/css/merge.css b/test/css/merge.css new file mode 100644 index 00000000..4cf8c579 --- /dev/null +++ b/test/css/merge.css @@ -0,0 +1,22 @@ +.test1 { + transform: rotate(90deg), skew(30deg), scale(2, 4); +} +.test2 { + transform: rotate(90deg), skew(30deg); + transform: scaleX(45deg); +} +.test3 { + transform: scaleX(45deg); + background: url(data://img1.png); +} +.test4 { + transform: rotate(90deg), skew(30deg); + transform: scale(2, 4) !important; +} +.test5 { + transform: rotate(90deg), skew(30deg); + transform: scale(2, 4) !important; +} +.test6 { + transform: scale(2, 4); +} diff --git a/test/css/static-urls/urls.css b/test/css/static-urls/urls.css index b5a690e9..95f04fbb 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 "css/background.css"; @import "folder (1)/import-test-d.css"; @font-face { src: url("/fonts/garamond-pro.ttf"); @@ -31,11 +30,11 @@ #logo { width: 100px; height: 100px; - background: url('folder (1)/../assets/logo.png'); + background: url('assets/logo.png'); } @font-face { font-family: xecret; - src: url('folder (1)/../assets/xecret.ttf'); + src: url('assets/xecret.ttf'); } #secret { font-family: xecret, sans-serif; diff --git a/test/css/strings.css b/test/css/strings.css index 522bae5e..cd6d6020 100644 --- a/test/css/strings.css +++ b/test/css/strings.css @@ -38,3 +38,6 @@ color: #000000; color: #ffa500; } +.watermark { + family: Univers, Arial, Verdana, San-Serif; +} diff --git a/test/css/urls.css b/test/css/urls.css index f736962c..a0618689 100644 --- a/test/css/urls.css +++ b/test/css/urls.css @@ -1,7 +1,5 @@ -@import "import/../css/background.css"; - +@import "css/background.css"; @import "import/import-test-d.css"; - @import "file.css"; @font-face { src: url("/fonts/garamond-pro.ttf"); @@ -35,11 +33,11 @@ #logo { width: 100px; height: 100px; - background: url('import/imports/../assets/logo.png'); + background: url('import/assets/logo.png'); } @font-face { font-family: xecret; - src: url('import/imports/../assets/xecret.ttf'); + src: url('import/assets/xecret.ttf'); } #secret { font-family: xecret, sans-serif; @@ -57,3 +55,8 @@ #data-uri-toobig { uri: url('../data/data-uri-fail.png'); } +#svg-functions { + background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjEiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIHZpZXdCb3g9IjAgMCAxIDEiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiPjxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZGllbnQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4MT0iMCUiIHkxPSIwJSIgeDI9IjAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwMDAwMCIvPjxzdG9wIG9mZnNldD0iMTAwJSIgc3RvcC1jb2xvcj0iI2ZmZmZmZiIvPjwvbGluZWFyR3JhZGllbnQ+PHJlY3QgeD0iMCIgeT0iMCIgd2lkdGg9IjEiIGhlaWdodD0iMSIgZmlsbD0idXJsKCNncmFkaWVudCkiIC8+PC9zdmc+'); + background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjEiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIHZpZXdCb3g9IjAgMCAxIDEiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiPjxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZGllbnQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4MT0iMCUiIHkxPSIwJSIgeDI9IjAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIwJSIgc3RvcC1jb2xvcj0iIzAwMDAwMCIvPjxzdG9wIG9mZnNldD0iMyUiIHN0b3AtY29sb3I9IiNmZmE1MDAiLz48c3RvcCBvZmZzZXQ9IjEwMCUiIHN0b3AtY29sb3I9IiNmZmZmZmYiLz48L2xpbmVhckdyYWRpZW50PjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiIGZpbGw9InVybCgjZ3JhZGllbnQpIiAvPjwvc3ZnPg=='); + background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB2ZXJzaW9uPSIxLjEiIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjEwMCUiIHZpZXdCb3g9IjAgMCAxIDEiIHByZXNlcnZlQXNwZWN0UmF0aW89Im5vbmUiPjxsaW5lYXJHcmFkaWVudCBpZD0iZ3JhZGllbnQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiB4MT0iMCUiIHkxPSIwJSIgeDI9IjAlIiB5Mj0iMTAwJSI+PHN0b3Agb2Zmc2V0PSIxJSIgc3RvcC1jb2xvcj0iI2M0YzRjNCIvPjxzdG9wIG9mZnNldD0iMyUiIHN0b3AtY29sb3I9IiNmZmE1MDAiLz48c3RvcCBvZmZzZXQ9IjUlIiBzdG9wLWNvbG9yPSIjMDA4MDAwIi8+PHN0b3Agb2Zmc2V0PSI5NSUiIHN0b3AtY29sb3I9IiNmZmZmZmYiLz48L2xpbmVhckdyYWRpZW50PjxyZWN0IHg9IjAiIHk9IjAiIHdpZHRoPSIxIiBoZWlnaHQ9IjEiIGZpbGw9InVybCgjZ3JhZGllbnQpIiAvPjwvc3ZnPg=='); +} diff --git a/test/less-test.js b/test/less-test.js index 307e7d59..30e62c2b 100644 --- a/test/less-test.js +++ b/test/less-test.js @@ -1,3 +1,4 @@ +/*jshint latedef: nofunc */ var path = require('path'), fs = require('fs'), sys = require('util'); @@ -9,6 +10,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,14 +23,60 @@ 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"); runTestSet({strictMath: true, relativeUrls: true, silent: true}); +runTestSet({strictMath: true, strictUnits: true}, "errors/", + testErrors, null, getErrorPathReplacementFunction("errors")); +runTestSet({strictMath: true, strictUnits: true, javascriptEnabled: false}, "no-js-errors/", + testErrors, null, getErrorPathReplacementFunction("no-js-errors")); +runTestSet({strictMath: true, dumpLineNumbers: 'comments'}, "debug/", null, + function(name) { return name + '-comments'; }); +runTestSet({strictMath: true, dumpLineNumbers: 'mediaquery'}, "debug/", null, + function(name) { return name + '-mediaquery'; }); +runTestSet({strictMath: true, dumpLineNumbers: 'all'}, "debug/", null, + function(name) { return name + '-all'; }); +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'; }); -runTestSet({strictMath: true, strictUnits: true}, "errors/", function(name, err, compiledLess, doReplacements) { +testNoOptions(); + +function getErrorPathReplacementFunction(dir) { + return function(input) { + return input.replace( + "{path}", path.join(process.cwd(), "/test/less/" + dir + "/")) + .replace("{pathrel}", path.join("test", "less", dir + "/")) + .replace("{pathhref}", "") + .replace("{404status}", "") + .replace(/\r\n/g, '\n'); + }; +} + +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 + ": "); expectedErr = doReplacements(expectedErr, 'test/less/errors/'); @@ -40,31 +89,14 @@ runTestSet({strictMath: true, strictUnits: true}, "errors/", function(name, err, } else { var errMessage = less.formatError(err); if (errMessage === expectedErr) { - ok('OK'); + ok('OK'); } else { difference("FAIL", expectedErr, errMessage); } } sys.puts(""); - });}, null, function(input, directory) { - return input.replace( - "{path}", path.join(process.cwd(), "/test/less/errors/")) - .replace("{pathrel}", path.join("test", "less", "errors/")) - .replace("{pathhref}", "") - .replace("{404status}", "") - .replace(/\r\n/g, '\n'); }); - -runTestSet({strictMath: true, dumpLineNumbers: 'comments'}, "debug/", null, - function(name) { return name + '-comments'; }); -runTestSet({strictMath: true, dumpLineNumbers: 'mediaquery'}, "debug/", null, - function(name) { return name + '-mediaquery'; }); -runTestSet({strictMath: true, dumpLineNumbers: 'all'}, "debug/", null, - function(name) { return name + '-all'; }); -runTestSet({strictMath: true, relativeUrls: false, rootpath: "folder (1)/"}, "static-urls/"); -runTestSet({strictMath: true, compress: true}, "compression/"); -runTestSet({ }, "legacy/"); -testNoOptions(); +} function globalReplacements(input, directory) { var p = path.join(process.cwd(), directory), @@ -85,14 +117,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'); @@ -100,20 +132,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); } @@ -127,7 +173,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); } @@ -177,10 +224,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); @@ -205,7 +252,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/comments.less b/test/less/comments.less index b803ff75..7859911e 100644 --- a/test/less/comments.less +++ b/test/less/comments.less @@ -34,7 +34,13 @@ /* @group Variables ------------------- */ -#comments /* boo */ { +#comments /* boo *//* boo again*/, +//.commented_out1 +//.commented_out2 +//.commented_out3 +.comments //end of comments1 +//end of comments2 +{ /**/ // An empty comment color: red; /* A C-style comment */ /* A C-style comment */ background-color: orange; // A little comment diff --git a/test/less/compression/compression.less b/test/less/compression/compression.less index c1a10e02..83155cc2 100644 --- a/test/less/compression/compression.less +++ b/test/less/compression/compression.less @@ -4,6 +4,11 @@ color3: rgba(255, 238, 170, 0.1); @color1: #fea; string: "@{color1}"; + /* comments are stripped */ + // both types! + /*! but not this type + Note preserved whitespace + */ } dimensions { val: 0.1px; @@ -12,5 +17,7 @@ dimensions { val: 0.2; val: 5; angles-must-have-unit: 0deg; + durations-must-have-unit: 0s; + length-doesnt-have-unit: 0px; width: auto\9; } \ No newline at end of file diff --git a/test/less/css-guards.less b/test/less/css-guards.less new file mode 100644 index 00000000..41fbfbf5 --- /dev/null +++ b/test/less/css-guards.less @@ -0,0 +1,64 @@ + +.light when (lightness(@a) > 50%) { + color: green; +} +.dark when (lightness(@a) < 50%) { + color: orange; +} +@a: #ddd; + +.see-the { + @a: #444; // this mirrors what mixins do - they evaluate guards at the point of execution + .light(); + .dark(); +} + +.hide-the { + .light(); + .dark(); +} + +.multiple-conditions-1 when (@b = 1), (@c = 2), (@d = 3) { + color: red; +} + +.multiple-conditions-2 when (@b = 1), (@c = 2), (@d = 2) { + color: blue; +} + +@b: 2; +@c: 3; +@d: 3; + +.inheritance when (@b = 2) { + .test { + color: black; + } + &:hover { + color: pink; + } + .hideme when (@b = 1) { + color: green; + } + & when (@b = 1) { + hideme: green; + } +} + +.hideme when (@b = 1) { + .test { + color: black; + } + &:hover { + color: pink; + } + .hideme when (@b = 1) { + color: green; + } +} + +& when (@b = 1) { + .hideme { + color: red; + } +} \ No newline at end of file diff --git a/test/less/errors/svg-gradient1.less b/test/less/errors/svg-gradient1.less new file mode 100644 index 00000000..c069ff72 --- /dev/null +++ b/test/less/errors/svg-gradient1.less @@ -0,0 +1,3 @@ +.a { + a: svg-gradient(horizontal, black, white); +} \ No newline at end of file diff --git a/test/less/errors/svg-gradient1.txt b/test/less/errors/svg-gradient1.txt new file mode 100644 index 00000000..ec662fe6 --- /dev/null +++ b/test/less/errors/svg-gradient1.txt @@ -0,0 +1,4 @@ +ArgumentError: error evaluating function `svg-gradient`: svg-gradient direction must be 'to bottom', 'to right', 'to bottom right', 'to top right' or 'ellipse at center' in {path}svg-gradient1.less on line 2, column 6: +1 .a { +2 a: svg-gradient(horizontal, black, white); +3 } diff --git a/test/less/errors/svg-gradient2.less b/test/less/errors/svg-gradient2.less new file mode 100644 index 00000000..ff14ef11 --- /dev/null +++ b/test/less/errors/svg-gradient2.less @@ -0,0 +1,3 @@ +.a { + a: svg-gradient(to bottom, black, orange, 45%, white); +} \ No newline at end of file diff --git a/test/less/errors/svg-gradient2.txt b/test/less/errors/svg-gradient2.txt new file mode 100644 index 00000000..7004115f --- /dev/null +++ b/test/less/errors/svg-gradient2.txt @@ -0,0 +1,4 @@ +ArgumentError: error evaluating function `svg-gradient`: svg-gradient expects direction, start_color [start_position], [color position,]..., end_color [end_position] in {path}svg-gradient2.less on line 2, column 6: +1 .a { +2 a: svg-gradient(to bottom, black, orange, 45%, white); +3 } diff --git a/test/less/errors/svg-gradient3.less b/test/less/errors/svg-gradient3.less new file mode 100644 index 00000000..8f185246 --- /dev/null +++ b/test/less/errors/svg-gradient3.less @@ -0,0 +1,3 @@ +.a { + a: svg-gradient(black, orange); +} \ No newline at end of file diff --git a/test/less/errors/svg-gradient3.txt b/test/less/errors/svg-gradient3.txt new file mode 100644 index 00000000..eb0b483e --- /dev/null +++ b/test/less/errors/svg-gradient3.txt @@ -0,0 +1,4 @@ +ArgumentError: error evaluating function `svg-gradient`: svg-gradient expects direction, start_color [start_position], [color position,]..., end_color [end_position] in {path}svg-gradient3.less on line 2, column 6: +1 .a { +2 a: svg-gradient(black, orange); +3 } diff --git a/test/less/extend-selector.less b/test/less/extend-selector.less index 7d6f044c..c7588ee0 100644 --- a/test/less/extend-selector.less +++ b/test/less/extend-selector.less @@ -81,4 +81,19 @@ div.ext5, .attribute-test { &:extend([data="test3"] all); } +} + +.header { + .header-nav { + background: red; + &:before { + background: blue; + } + } +} + +.footer { + .footer-nav { + &:extend( .header .header-nav all ); + } } \ No newline at end of file diff --git a/test/less/functions.less b/test/less/functions.less index f60dab47..32fe44b0 100644 --- a/test/less/functions.less +++ b/test/less/functions.less @@ -30,6 +30,7 @@ luma-cyan: luma(#00ffff); luma-white-alpha: luma(rgba(255,255,255,0.5)); contrast-filter: contrast(30%); + saturate-filter: saturate(5%); contrast-white: contrast(#fff); contrast-black: contrast(#000); contrast-red: contrast(#ff0000); @@ -86,6 +87,11 @@ pow: pow(8px, 2); pow: pow(4, 3); pow: pow(3, 3em); + min: min(0); + min: min("junk", 6, 5); + min: min(1pc, 3pt); + max: max(1, 3); + max: max(3%, 1cm, 8%, 2mm); percentage: percentage((10px / 50)); color: color("#ff0011"); tint: tint(#777777, 13); diff --git a/test/less/import-inline.less b/test/less/import-inline.less new file mode 100644 index 00000000..95a11896 --- /dev/null +++ b/test/less/import-inline.less @@ -0,0 +1,2 @@ +@import (inline) url("import/import-test-d.css") (min-width:600px); +@import (inline, css) url("import/invalid-css.less"); \ No newline at end of file diff --git a/test/less/import-reference.less b/test/less/import-reference.less new file mode 100644 index 00000000..cf9da168 --- /dev/null +++ b/test/less/import-reference.less @@ -0,0 +1,18 @@ +@import (reference) url("import-once.less"); +@import (reference) url("css-3.less"); +@import (reference) url("media.less"); +/* + The media statement above is invalid (no selector) + We should ban invalid media queries with properties and no selector? +*/ +@import (reference) url("import/import-reference.less"); + +.b { + .z(); +} + +.zz(); + +.visible:extend(.z all) { + extend: test; +} \ No newline at end of file diff --git a/test/less/import/import-reference.less b/test/less/import/import-reference.less new file mode 100644 index 00000000..9eac45fc --- /dev/null +++ b/test/less/import/import-reference.less @@ -0,0 +1,43 @@ +.z { + color: red; + .c { + color: green; + } +} +.only-with-visible, +.z { + color: green; + &:hover { + color: green; + } + & { + color: green; + } + & + & { + color: green; + .sub { + color: green; + } + } +} + +& { + .hidden { + hidden: true; + } +} + +@media tv { + .hidden { + hidden: true; + } +} + +/* comment is not output */ + +.zz { + .y { + pulled-in: yes; + } + /* comment pulled in */ +} \ No newline at end of file diff --git a/test/less/import/invalid-css.less b/test/less/import/invalid-css.less new file mode 100644 index 00000000..ed585d63 --- /dev/null +++ b/test/less/import/invalid-css.less @@ -0,0 +1 @@ +this isn't very valid CSS. \ No newline at end of file diff --git a/test/less/media.less b/test/less/media.less index 3d55d31d..f4e83161 100644 --- a/test/less/media.less +++ b/test/less/media.less @@ -109,7 +109,9 @@ body { } @smartphone: ~"only screen and (max-width: 200px)"; @media @smartphone { - width: 480px; + body { + width: 480px; + } } @media print { @@ -208,3 +210,18 @@ body { body { font-size: 11px; } } } + +.nav-justified { + @media (min-width: 480px) { + > li { + display: table-cell; + } + } +} + +.menu +{ + @media (min-width: 768px) { + .nav-justified(); + } +} diff --git a/test/less/merge.less b/test/less/merge.less new file mode 100644 index 00000000..52d0796e --- /dev/null +++ b/test/less/merge.less @@ -0,0 +1,51 @@ +.first-transform() { + transform+: rotate(90deg), skew(30deg); +} +.second-transform() { + transform+: scale(2,4); +} +.third-transform() { + transform: scaleX(45deg); +} +.fourth-transform() { + transform+: scaleX(45deg); +} +.fifth-transform() { + transform+: scale(2,4) !important; +} +.first-background() { + background+: url(data://img1.png); +} +.second-background() { + background+: url(data://img2.png); +} + +.test1 { + // Can merge values + .first-transform(); + .second-transform(); +} +.test2 { + // Wont merge values without +: merge directive, for backwards compatibility with css + .first-transform(); + .third-transform(); +} +.test3 { + // Wont merge values from two sources with different properties + .fourth-transform(); + .first-background(); +} +.test4 { + // Wont merge values from sources that merked as !important, for backwards compatibility with css + .first-transform(); + .fifth-transform(); +} +.test5 { + // Wont merge values from mixins that merked as !important, for backwards compatibility with css + .first-transform(); + .second-transform() !important; +} +.test6 { + // Ignores !merge if no peers found + .second-transform(); +} \ No newline at end of file diff --git a/test/less/no-js-errors/no-js-errors.less b/test/less/no-js-errors/no-js-errors.less new file mode 100644 index 00000000..15ef8a45 --- /dev/null +++ b/test/less/no-js-errors/no-js-errors.less @@ -0,0 +1,3 @@ +.a { + a: `1 + 1`; +} \ No newline at end of file diff --git a/test/less/no-js-errors/no-js-errors.txt b/test/less/no-js-errors/no-js-errors.txt new file mode 100644 index 00000000..d81dd2bd --- /dev/null +++ b/test/less/no-js-errors/no-js-errors.txt @@ -0,0 +1,4 @@ +SyntaxError: You are using JavaScript, which has been disabled. in {path}no-js-errors.less on line 2, column 6: +1 .a { +2 a: `1 + 1`; +3 } 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/less/strings.less b/test/less/strings.less index 32fad721..c43e368d 100644 --- a/test/less/strings.less +++ b/test/less/strings.less @@ -49,3 +49,9 @@ .mix-mul(black); .mix-mul(orange); } + +@test: Arial, Verdana, San-Serif; +.watermark { + @family: ~"Univers, @{test}"; + family: @family; +} diff --git a/test/less/urls.less b/test/less/urls.less index 82f2df07..3b1e81cf 100644 --- a/test/less/urls.less +++ b/test/less/urls.less @@ -55,3 +55,12 @@ } .add_an_import("file.css"); + +#svg-functions { + background-image: svg-gradient(to bottom, black, white); + background-image: svg-gradient(to bottom, black, orange 3%, white); + @green_5: green 5%; + @orange_percentage: 3%; + @orange_color: orange; + background-image: svg-gradient(to bottom, (mix(black, white) + #444) 1%, @orange_color @orange_percentage, ((@green_5)), white 95%); +} 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