less-openui5
Version:
Build OpenUI5 themes with Less.js
1,281 lines (1,107 loc) • 76.2 kB
JavaScript
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
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, // 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() { temp = current; memo = currentPos = i; }
function restore() { current = temp; currentPos = i = memo; }
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) {
// 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) { 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.directive();
if (node) {
root.push(node);
} else {
if (!($re(/^[\s\n]+/) || $re(/^;+/))) {
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})/))) {
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]);
}
},
/* BEGIN MODIFICATION */
// Removed support for javascript
//
// JavaScript code (disabled)
//
// `window.location.href`
//
javascript: function () {
var j = i, e;
if (input.charAt(j) === '~') { j++; e = true; } // Escaped strings
if (input.charAt(j) !== '`') { return; }
error("You are using JavaScript, which has been disabled.");
}
/* END MODIFICATION */
},
//
// 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]; }
},
//
// 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];
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()) {
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;
while (true) {
if (isCall) {
arg = 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.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;
}
value = expect(parsers.expression);
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;
}
}
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();
match = $re(/^([#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+)\s*\(/);
if (match) {
name = match[1];
var argInfo = this.args(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 (!$char(')')) {
furthest = i;
restore();
}
parsers.comments();
if ($re(/^when/)) { // Guard
cond = expect(parsers.conditions, 'expected condition');
}
ruleset = parsers.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 () {