/node_modules/less-middleware/node_modules/less/lib/less/parser.js
JavaScript | 2109 lines | 1538 code | 204 blank | 367 comment | 374 complexity | 99f7597ba6c9fb5a2e1a0080cb867ba1 MD5 | raw file
Possible License(s): Apache-2.0, MIT, BSD-3-Clause, MPL-2.0-no-copyleft-exception, 0BSD
Large files files are truncated, but you can click here to view the full file
- 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 `currentPos` 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
- saveStack = [], // holds state for backtracking
- furthest, // furthest index the parser has gone to
- chunks, // chunkified input
- current, // current chunk
- currentPos, // index of current chunk, in `input`
- parser,
- parsers,
- rootFilename = env && env.filename;
- // 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
- contentsIgnoredChars: env.contentsIgnoredChars, // lines inserted, not in the original less
- 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 === rootFilename;
- parserImports.files[fullPath] = root; // Store the root
- if (e && !parserImports.error) { parserImports.error = e; }
- callback(e, root, importedPreviously, fullPath);
- };
- 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() { currentPos = i; saveStack.push( { current: current, i: i, j: j }); }
- function restore() { var state = saveStack.pop(); current = state.current; currentPos = i = state.i; j = state.j; }
- function forget() { saveStack.pop(); }
- function sync() {
- if (i > currentPos) {
- current = current.slice(i - currentPos);
- currentPos = i;
- }
- }
- function isWhitespace(str, pos) {
- var code = str.charCodeAt(pos | 0);
- return (code <= 32) && (code === 32 || code === 10 || code === 9);
- }
- //
- // Parse from a token, regexp or string, and move forward if match
- //
- function $(tok) {
- var tokType = typeof tok,
- match, length;
- // Either match a single character in the input,
- // or match a regexp in the current chunk (`current`).
- //
- if (tokType === "string") {
- if (input.charAt(i) !== tok) {
- return null;
- }
- skipWhitespace(1);
- return tok;
- }
- // regexp
- sync ();
- if (! (match = tok.exec(current))) {
- return null;
- }
- length = match[0].length;
- // 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.
- //
- skipWhitespace(length);
- if(typeof(match) === 'string') {
- return match;
- } else {
- return match.length === 1 ? match[0] : match;
- }
- }
- // Specialization of $(tok)
- function $re(tok) {
- if (i > currentPos) {
- current = current.slice(i - currentPos);
- currentPos = i;
- }
- var m = tok.exec(current);
- if (!m) {
- return null;
- }
- skipWhitespace(m[0].length);
- if(typeof m === "string") {
- return m;
- }
- return m.length === 1 ? m[0] : m;
- }
- var _$re = $re;
- // Specialization of $(tok)
- function $char(tok) {
- if (input.charAt(i) !== tok) {
- return null;
- }
- skipWhitespace(1);
- return tok;
- }
- function skipWhitespace(length) {
- var oldi = i, oldj = j,
- curr = i - currentPos,
- endIndex = i + current.length - curr,
- mem = (i += length),
- inp = input,
- c;
- for (; i < endIndex; i++) {
- c = inp.charCodeAt(i);
- if (c > 32) {
- break;
- }
- if ((c !== 32) && (c !== 10) && (c !== 9) && (c !== 13)) {
- break;
- }
- }
- current = current.slice(length + i - mem + curr);
- currentPos = i;
- if (!current.length && (j < chunks.length - 1)) {
- current = chunks[++j];
- skipWhitespace(0); // skip space at the beginning of a chunk
- return true; // things changed
- }
- return oldi !== i || oldj !== j;
- }
- function expect(arg, msg, index) {
- // some older browsers return typeof 'function' for RegExp
- var result = (Object.prototype.toString.call(arg) === '[object Function]') ? arg.call(parsers) : $(arg);
- if (result) {
- return result;
- }
- error(msg || (typeof(arg) === 'string' ? "expected '" + arg + "' got '" + input.charAt(i) + "'"
- : "unexpected token"));
- }
- // Specialization of expect()
- function expectChar(arg, msg) {
- if (input.charAt(i) === arg) {
- skipWhitespace(1);
- return arg;
- }
- error(msg || "expected '" + arg + "' got '" + input.charAt(i) + "'");
- }
- 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(current);
- }
- }
- // Specialization of peek()
- function peekChar(tok) {
- return input.charAt(i) === tok;
- }
- 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
- //
- parser = {
- imports: imports,
- //
- // Parse an input string into an abstract syntax tree,
- // @param str A string containing 'less' markup
- // @param callback call `callback` when done.
- // @param [additionalData] An optional map which can contains vars - a map (key, value) of variables to apply
- //
- parse: function (str, callback, additionalData) {
- var root, line, lines, error = null, globalVars, modifyVars, preText = "";
- i = j = currentPos = furthest = 0;
- globalVars = (additionalData && additionalData.globalVars) ? less.Parser.serializeVars(additionalData.globalVars) + '\n' : '';
- modifyVars = (additionalData && additionalData.modifyVars) ? '\n' + less.Parser.serializeVars(additionalData.modifyVars) : '';
- if (globalVars || (additionalData && additionalData.banner)) {
- preText = ((additionalData && additionalData.banner) ? additionalData.banner : "") + globalVars;
- parser.imports.contentsIgnoredChars[env.currentFileInfo.filename] = preText.length;
- }
- str = str.replace(/\r\n/g, '\n');
- // Remove potential UTF Byte Order Mark
- input = str = preText + str.replace(/^\uFEFF/, '') + modifyVars;
- parser.imports.contents[env.currentFileInfo.filename] = str;
- // Split the input into chunks.
- chunks = (function (input) {
- var len = input.length, level = 0, parenLevel = 0,
- lastOpening, lastOpeningParen, lastMultiComment, lastMultiCommentEndBrace,
- chunks = [], emitFrom = 0,
- parserCurrentIndex, currentChunkStartIndex, cc, cc2, matched;
- function fail(msg, index) {
- error = new(LessError)({
- index: index || parserCurrentIndex,
- type: 'Parse',
- message: msg,
- filename: env.currentFileInfo.filename
- }, env);
- }
- function emitChunk(force) {
- var len = parserCurrentIndex - emitFrom;
- if (((len < 512) && !force) || !len) {
- return;
- }
- chunks.push(input.slice(emitFrom, parserCurrentIndex + 1));
- emitFrom = parserCurrentIndex + 1;
- }
- for (parserCurrentIndex = 0; parserCurrentIndex < len; parserCurrentIndex++) {
- cc = input.charCodeAt(parserCurrentIndex);
- if (((cc >= 97) && (cc <= 122)) || (cc < 34)) {
- // a-z or whitespace
- continue;
- }
- switch (cc) {
- case 40: // (
- parenLevel++;
- lastOpeningParen = parserCurrentIndex;
- continue;
- case 41: // )
- if (--parenLevel < 0) {
- return fail("missing opening `(`");
- }
- continue;
- case 59: // ;
- if (!parenLevel) { emitChunk(); }
- continue;
- case 123: // {
- level++;
- lastOpening = parserCurrentIndex;
- continue;
- case 125: // }
- if (--level < 0) {
- return fail("missing opening `{`");
- }
- if (!level && !parenLevel) { emitChunk(); }
- continue;
- case 92: // \
- if (parserCurrentIndex < len - 1) { parserCurrentIndex++; continue; }
- return fail("unescaped `\\`");
- case 34:
- case 39:
- case 96: // ", ' and `
- matched = 0;
- currentChunkStartIndex = parserCurrentIndex;
- for (parserCurrentIndex = parserCurrentIndex + 1; parserCurrentIndex < len; parserCurrentIndex++) {
- cc2 = input.charCodeAt(parserCurrentIndex);
- if (cc2 > 96) { continue; }
- if (cc2 == cc) { matched = 1; break; }
- if (cc2 == 92) { // \
- if (parserCurrentIndex == len - 1) {
- return fail("unescaped `\\`");
- }
- parserCurrentIndex++;
- }
- }
- if (matched) { continue; }
- return fail("unmatched `" + String.fromCharCode(cc) + "`", currentChunkStartIndex);
- case 47: // /, check for comment
- if (parenLevel || (parserCurrentIndex == len - 1)) { continue; }
- cc2 = input.charCodeAt(parserCurrentIndex + 1);
- if (cc2 == 47) {
- // //, find lnfeed
- for (parserCurrentIndex = parserCurrentIndex + 2; parserCurrentIndex < len; parserCurrentIndex++) {
- cc2 = input.charCodeAt(parserCurrentIndex);
- if ((cc2 <= 13) && ((cc2 == 10) || (cc2 == 13))) { break; }
- }
- } else if (cc2 == 42) {
- // /*, find */
- lastMultiComment = currentChunkStartIndex = parserCurrentIndex;
- for (parserCurrentIndex = parserCurrentIndex + 2; parserCurrentIndex < len - 1; parserCurrentIndex++) {
- cc2 = input.charCodeAt(parserCurrentIndex);
- if (cc2 == 125) { lastMultiCommentEndBrace = parserCurrentIndex; }
- if (cc2 != 42) { continue; }
- if (input.charCodeAt(parserCurrentIndex + 1) == 47) { break; }
- }
- if (parserCurrentIndex == len - 1) {
- return fail("missing closing `*/`", currentChunkStartIndex);
- }
- parserCurrentIndex++;
- }
- continue;
- case 42: // *, check for unmatched */
- if ((parserCurrentIndex < len - 1) && (input.charCodeAt(parserCurrentIndex + 1) == 47)) {
- return fail("unmatched `/*`");
- }
- continue;
- }
- }
- if (level !== 0) {
- if ((lastMultiComment > lastOpening) && (lastMultiCommentEndBrace > lastMultiComment)) {
- return fail("missing closing `}` or `*/`", lastOpening);
- } else {
- return fail("missing closing `}`", lastOpening);
- }
- } else if (parenLevel !== 0) {
- return fail("missing closing `)`", lastOpeningParen);
- }
- emitChunk(true);
- return chunks;
- })(str);
- if (error) {
- return callback(new(LessError)(error, env));
- }
- current = chunks[0];
- // 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)(null, 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 {
- var preEvalVisitors = [],
- visitors = [
- new(tree.joinSelectorVisitor)(),
- new(tree.processExtendsVisitor)(),
- new(tree.toCSSVisitor)({compress: Boolean(options.compress)})
- ], i, root = this;
- if (options.plugins) {
- for(i =0; i < options.plugins.length; i++) {
- if (options.plugins[i].isPreEvalVisitor) {
- preEvalVisitors.push(options.plugins[i]);
- } else {
- if (options.plugins[i].isPreVisitor) {
- visitors.splice(0, 0, options.plugins[i]);
- } else {
- visitors.push(options.plugins[i]);
- }
- }
- }
- }
- for(i = 0; i < preEvalVisitors.length; i++) {
- preEvalVisitors[i].run(root);
- }
- evaldRoot = evaluate.call(root, evalEnv);
- for(i = 0; i < visitors.length; i++) {
- visitors[i].run(evaldRoot);
- }
- if (options.sourceMap) {
- evaldRoot = new tree.sourceMapOutput(
- {
- contentsIgnoredCharsMap: parser.imports.contentsIgnoredChars,
- writeSourceMap: options.writeSourceMap,
- rootNode: evaldRoot,
- contentsMap: parser.imports.contents,
- sourceMapFilename: options.sourceMapFilename,
- sourceMapURL: options.sourceMapURL,
- outputFilename: options.sourceMapOutputFilename,
- sourceMapBasepath: options.sourceMapBasepath,
- sourceMapRootpath: options.sourceMapRootpath,
- outputSourceFiles: options.outputSourceFiles,
- sourceMapGenerator: options.sourceMapGenerator
- });
- }
- css = evaldRoot.toCSS({
- compress: Boolean(options.compress),
- dumpLineNumbers: env.dumpLineNumbers,
- strictUnits: Boolean(options.strictUnits),
- numPrecision: 8});
- } catch (e) {
- throw new(LessError)(e, env);
- }
- if (options.cleancss && less.mode === 'node') {
- var CleanCSS = require('clean-css'),
- cleancssOptions = options.cleancssOptions || {};
- if (cleancssOptions.keepSpecialComments === undefined) {
- cleancssOptions.keepSpecialComments = "*";
- }
- cleancssOptions.processImport = false;
- cleancssOptions.noRebase = true;
- if (cleancssOptions.noAdvanced === undefined) {
- cleancssOptions.noAdvanced = true;
- }
- return new CleanCSS(cleancssOptions).minify(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);
- }
- return callback(e);
- }
- else {
- return callback(null, root);
- }
- };
- if (env.processImports !== false) {
- new tree.importVisitor(this.imports, finish)
- .run(root);
- } else {
- return 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: 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 mixin = this.mixin, $re = _$re, root = [], node;
- while (current)
- {
- node = this.extendRule() || mixin.definition() || this.rule() || this.ruleset() ||
- mixin.call() || this.comment() || this.rulesetCall() || this.directive();
- if (node) {
- root.push(node);
- } else {
- if (!($re(/^[\s\n]+/) || $re(/^;+/))) {
- break;
- }
- }
- if (peekChar('}')) {
- break;
- }
- }
- 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)($re(/^\/\/.*/), true, i, env.currentFileInfo);
- }
- comment = $re(/^\/\*(?:[^*]|\*+[^\/*])*\*+\/\n?/);
- if (comment) {
- return new(tree.Comment)(comment, false, i, env.currentFileInfo);
- }
- },
- comments: function () {
- var comment, comments = [];
- while(true) {
- comment = this.comment();
- if (!comment) {
- break;
- }
- 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; }
- if (e) { $char('~'); }
- str = $re(/^"((?:[^"\\\r\n]|\\.)*)"|'((?:[^'\\\r\n]|\\.)*)'/);
- if (str) {
- 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;
- k = $re(/^%|^[_A-Za-z-][_A-Za-z0-9-]*/);
- if (k) {
- var color = tree.Color.fromKeyword(k);
- if (color) {
- return color;
- }
- 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;
- name = /^([\w-]+|%|progid:[\w\.]+)\(/.exec(current);
- if (!name) { return; }
- name = name[1];
- nameLC = name.toLowerCase();
- if (nameLC === 'url') {
- return null;
- }
- i += name.length;
- if (nameLC === 'alpha') {
- alpha_ret = parsers.alpha();
- if(typeof alpha_ret !== 'undefined') {
- return alpha_ret;
- }
- }
- $char('('); // Parse the '(' and consume whitespace.
- args = this.arguments();
- if (! $char(')')) {
- return;
- }
- if (name) { return new(tree.Call)(name, args, index, env.currentFileInfo); }
- },
- arguments: function () {
- var args = [], arg;
- while (true) {
- arg = this.assignment() || parsers.expression();
- if (!arg) {
- break;
- }
- args.push(arg);
- if (! $char(',')) {
- break;
- }
- }
- return args;
- },
- literal: function () {
- return this.dimension() ||
- this.color() ||
- this.quoted() ||
- this.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;
- key = $re(/^\w+(?=\s?=)/i);
- if (!key) {
- return;
- }
- if (!$char('=')) {
- return;
- }
- value = parsers.entity();
- if (value) {
- 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' || !$re(/^url\(/)) {
- return;
- }
- value = this.quoted() || this.variable() ||
- $re(/^(?:(?:\\[\(\)'"])|[^\(\)'"])+/) || "";
- expectChar(')');
- 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 = $re(/^@@?[\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 = $re(/^@\{([\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 = $re(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/))) {
- var colorCandidateString = rgb.input.match(/^#([\w]+).*/); // strip colons, brackets, whitespaces and other characters that should not definitely be part of color string
- colorCandidateString = colorCandidateString[1];
- if (!colorCandidateString.match(/^[A-Fa-f0-9]+$/)) { // verify if candidate consists only of allowed HEX characters
- error("Invalid HEX color code");
- }
- 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;
- }
- value = $re(/^([+-]?\d*\.?\d+)(%|[a-z]+)?/);
- if (value) {
- 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;
- ud = $re(/^U\+[0-9a-fA-F?]+(\-[0-9a-fA-F?]+)?/);
- if (ud) {
- 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) { $char('~'); }
- str = $re(/^`([^`]*)`/);
- 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 = $re(/^(@[\w-]+)\s*:/))) { return name[1]; }
- },
- //
- // The variable part of a variable definition. Used in the `rule` parser
- //
- // @fink();
- //
- rulesetCall: function () {
- var name;
- if (input.charAt(i) === '@' && (name = $re(/^(@[\w-]+)\s*\(\s*\)\s*;/))) {
- return new tree.RulesetCall(name[1]);
- }
- },
- //
- // extend syntax - used to extend selectors
- //
- extend: function(isRule) {
- var elements, e, index = i, option, extendList, extend;
- if (!(isRule ? $re(/^&:extend\(/) : $re(/^:extend\(/))) { return; }
- do {
- option = null;
- elements = null;
- while (! (option = $re(/^(all)(?=\s*(\)|,))/))) {
- e = this.element();
- if (!e) { break; }
- if (elements) { elements.push(e); } else { elements = [ e ]; }
- }
- option = option && option[1];
- if (!elements)
- error("Missing target selector for :extend().");
- extend = new(tree.Extend)(new(tree.Selector)(elements), option, index);
- if (extendList) { extendList.push(extend); } else { extendList = [ extend ]; }
- } while($char(","));
- 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 s = input.charAt(i), important = false, index = i, elemIndex,
- elements, elem, e, c, args;
- if (s !== '.' && s !== '#') { return; }
- save(); // stop us absorbing part of an invalid selector
- while (true) {
- elemIndex = i;
- e = $re(/^[#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/);
- if (!e) {
- break;
- }
- elem = new(tree.Element)(c, e, elemIndex, env.currentFileInfo);
- if (elements) { elements.push(elem); } else { elements = [ elem ]; }
- c = $char('>');
- }
- if (elements) {
- if ($char('(')) {
- args = this.args(true).args;
- expectChar(')');
- }
- if (parsers.important()) {
- important = true;
- }
- if (parsers.end()) {
- forget();
- return new(tree.mixin.Call)(elements, args, index, env.currentFileInfo, important);
- }
- }
- restore();
- },
- args: function (isCall) {
- var parsers = parser.parsers, entities = parsers.entities,
- returner = { args:null, variadic: false },
- expressions = [], argsSemiColon = [], argsComma = [],
- isSemiColonSeperated, expressionContainsNamed, name, nameLoop, value, arg;
- save();
- while (true) {
- if (isCall) {
- arg = parsers.detachedRuleset() || parsers.expression();
- } else {
- parsers.comments();
- if (input.charAt(i) === '.' && $re(/^\.{3}/)) {
- returner.variadic = true;
- if ($char(";") && !isSemiColonSeperated) {
- isSemiColonSeperated = true;
- }
- (isSemiColonSeperated ? argsSemiColon : argsComma)
- .push({ variadic: true });
- break;
- }
- arg = entities.variable() || entities.literal() || entities.keyword();
- }
- if (!arg) {
- break;
- }
- nameLoop = null;
- if (arg.throwAwayComments) {
- arg.throwAwayComments();
- }
- value = arg;
- var val = null;
- if (isCall) {
- // Variable
- if (arg.value && arg.value.length == 1) {
- val = arg.value[0];
- }
- } else {
- val = arg;
- }
- if (val && val instanceof tree.Variable) {
- if ($char(':')) {
- if (expressions.length > 0) {
- if (isSemiColonSeperated) {
- error("Cannot mix ; and , as delimiter types");
- }
- expressionContainsNamed = true;
- }
- // we do not support setting a ruleset as a default variable - it doesn't make sense
- // However if we do want to add it, there is nothing blocking it, just don't error
- // and remove isCall dependency below
- value = (isCall && parsers.detachedRuleset()) || parsers.expression();
- if (!value) {
- if (isCall) {
- error("could not understand value for named argument");
- } else {
- restore();
- returner.args = [];
- return returner;
- }
- }
- nameLoop = (name = val.name);
- } else if (!isCall && $re(/^\.{3}/)) {
- returner.variadic = true;
- if ($char(";") && !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 ($char(',')) {
- continue;
- }
- if ($char(';') || 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;
- }
- }
- forget();
- 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…
Large files files are truncated, but you can click here to view the full file