less
Version:
Leaner CSS
1,125 lines (1,124 loc) • 103 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var tslib_1 = require("tslib");
var less_error_1 = tslib_1.__importDefault(require("../less-error"));
var tree_1 = tslib_1.__importDefault(require("../tree"));
var visitors_1 = tslib_1.__importDefault(require("../visitors"));
var parser_input_1 = tslib_1.__importDefault(require("./parser-input"));
var utils = tslib_1.__importStar(require("../utils"));
var function_registry_1 = tslib_1.__importDefault(require("../functions/function-registry"));
var atrule_syntax_1 = require("../tree/atrule-syntax");
var logger_1 = tslib_1.__importDefault(require("../logger"));
var selector_1 = tslib_1.__importDefault(require("../tree/selector"));
var anonymous_1 = tslib_1.__importDefault(require("../tree/anonymous"));
//
// 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.
//
var Parser = function Parser(context, imports, fileInfo, currentIndex) {
currentIndex = currentIndex || 0;
var parsers;
var parserInput = (0, parser_input_1.default)();
function error(msg, type) {
throw new less_error_1.default({
index: parserInput.i,
filename: fileInfo.filename,
type: type || 'Syntax',
message: msg
}, imports);
}
/**
*
* @param {string} msg
* @param {number} index
* @param {string} type
*/
function warn(msg, index, type) {
if (!context.quiet) {
logger_1.default.warn((new less_error_1.default({
index: index !== null && index !== void 0 ? index : parserInput.i,
filename: fileInfo.filename,
type: type ? "".concat(type.toUpperCase(), " WARNING") : 'WARNING',
message: msg
}, imports)).toString());
}
}
function expect(arg, msg) {
// some older browsers return typeof 'function' for RegExp
var result = (arg instanceof Function) ? arg.call(parsers) : parserInput.$re(arg);
if (result) {
return result;
}
error(msg || (typeof arg === 'string'
? "expected '".concat(arg, "' got '").concat(parserInput.currentChar(), "'")
: 'unexpected token'));
}
// Specialization of expect()
function expectChar(arg, msg) {
if (parserInput.$char(arg)) {
return arg;
}
error(msg || "expected '".concat(arg, "' got '").concat(parserInput.currentChar(), "'"));
}
function getDebugInfo(index) {
var filename = fileInfo.filename;
return {
lineNumber: utils.getLocation(index, parserInput.getInput()).line + 1,
fileName: filename
};
}
/**
* Used after initial parsing to create nodes on the fly
*
* @param {String} str - string to parse
* @param {Array} parseList - array of parsers to run input through e.g. ["value", "important"]
* @param {Number} currentIndex - start number to begin indexing
* @param {Object} fileInfo - fileInfo to attach to created nodes
*/
function parseNode(str, parseList, callback) {
var result;
var returnNodes = [];
var parser = parserInput;
try {
parser.start(str, false, function fail(msg, index) {
callback({
message: msg,
index: index + currentIndex
});
});
for (var x = 0, p = void 0; (p = parseList[x]); x++) {
result = parsers[p]();
returnNodes.push(result || null);
}
var endInfo = parser.end();
if (endInfo.isFinished) {
callback(null, returnNodes);
}
else {
callback(true, null);
}
}
catch (e) {
throw new less_error_1.default({
index: e.index + currentIndex,
message: e.message
}, imports, fileInfo.filename);
}
}
//
// The Parser
//
return {
parserInput: parserInput,
imports: imports,
fileInfo: fileInfo,
parseNode: parseNode,
//
// 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;
var err = null;
var globalVars;
var modifyVars;
var ignored;
var preText = '';
// Optionally disable @plugin parsing
if (additionalData && additionalData.disablePluginRule) {
parsers.plugin = function () {
var dir = parserInput.$re(/^@plugin?\s+/);
if (dir) {
error('@plugin statements are not allowed when disablePluginRule is set to true');
}
};
}
globalVars = (additionalData && additionalData.globalVars) ? "".concat(Parser.serializeVars(additionalData.globalVars), "\n") : '';
modifyVars = (additionalData && additionalData.modifyVars) ? "\n".concat(Parser.serializeVars(additionalData.modifyVars)) : '';
if (context.pluginManager) {
var preProcessors = context.pluginManager.getPreProcessors();
for (var i = 0; i < preProcessors.length; i++) {
str = preProcessors[i].process(str, { context: context, imports: imports, fileInfo: fileInfo });
}
}
if (globalVars || (additionalData && additionalData.banner)) {
preText = ((additionalData && additionalData.banner) ? additionalData.banner : '') + globalVars;
ignored = imports.contentsIgnoredChars;
ignored[fileInfo.filename] = ignored[fileInfo.filename] || 0;
ignored[fileInfo.filename] += preText.length;
}
str = str.replace(/\r\n?/g, '\n');
// Remove potential UTF Byte Order Mark
str = preText + str.replace(/^\uFEFF/, '') + modifyVars;
imports.contents[fileInfo.filename] = str;
// 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 {
parserInput.start(str, context.chunkInput, function fail(msg, index) {
throw new less_error_1.default({
index: index,
type: 'Parse',
message: msg,
filename: fileInfo.filename
}, imports);
});
tree_1.default.Node.prototype.parse = this;
root = new tree_1.default.Ruleset(null, this.parsers.primary());
tree_1.default.Node.prototype.rootNode = root;
root.root = true;
root.firstRoot = true;
root.functionRegistry = function_registry_1.default.inherit();
}
catch (e) {
return callback(new less_error_1.default(e, imports, fileInfo.filename));
}
// 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 occurred.
// We split it up into two parts (the part which parsed,
// and the part which didn't), so we can color them differently.
var endInfo = parserInput.end();
if (!endInfo.isFinished) {
var message = endInfo.furthestPossibleErrorMessage;
if (!message) {
message = 'Unrecognised input';
if (endInfo.furthestChar === '}') {
message += '. Possibly missing opening \'{\'';
}
else if (endInfo.furthestChar === ')') {
message += '. Possibly missing opening \'(\'';
}
else if (endInfo.furthestReachedEnd) {
message += '. Possibly missing something';
}
}
err = new less_error_1.default({
type: 'Parse',
message: message,
index: endInfo.furthest,
filename: fileInfo.filename
}, imports);
}
var finish = function (e) {
e = err || e || imports.error;
if (e) {
if (!(e instanceof less_error_1.default)) {
e = new less_error_1.default(e, imports, fileInfo.filename);
}
return callback(e);
}
else {
return callback(null, root);
}
};
if (context.processImports !== false) {
new visitors_1.default.ImportVisitor(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 -> Declaration -> 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', [
// Declaration ("color", Value ([Expression [Color #fff]]))
// Declaration ("border", Value ([Expression [Dimension 1px][Keyword "solid"][Color #000]]))
// Declaration ("width", Value ([Expression [Operation " + " [Variable "@w"][Dimension 4px]]]))
// Ruleset (Selector [Element '>', '.child'], [...])
// ])
//
// In general, most rules will try to parse a token with the `$re()` 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 | declaration)+
// 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;
var root = [];
var node;
while (true) {
while (true) {
node = this.comment();
if (!node) {
break;
}
root.push(node);
}
// always process comments before deciding if finished
if (parserInput.finished) {
break;
}
if (parserInput.peek('}')) {
break;
}
node = this.extendRule();
if (node) {
root = root.concat(node);
continue;
}
node = mixin.definition() || this.declaration() || mixin.call(false, false) ||
this.ruleset() || this.variableCall() || this.entities.call() || this.atrule();
if (node) {
root.push(node);
}
else {
var foundSemiColon = false;
while (parserInput.$char(';')) {
foundSemiColon = true;
}
if (!foundSemiColon) {
break;
}
}
}
return root;
},
// comments are collected by the main parsing mechanism and then assigned to nodes
// where the current structure allows it
comment: function () {
if (parserInput.commentStore.length) {
var comment = parserInput.commentStore.shift();
return new (tree_1.default.Comment)(comment.text, comment.isLineComment, comment.index + currentIndex, fileInfo);
}
},
//
// Entities are tokens which can be found inside an Expression
//
entities: {
mixinLookup: function () {
return parsers.mixin.call(true, true);
},
//
// A string, which supports escaping " and '
//
// "milky way" 'he\'s the one!'
//
quoted: function (forceEscaped) {
var str;
var index = parserInput.i;
var isEscaped = false;
parserInput.save();
if (parserInput.$char('~')) {
isEscaped = true;
}
else if (forceEscaped) {
parserInput.restore();
return;
}
str = parserInput.$quoted();
if (!str) {
parserInput.restore();
return;
}
parserInput.forget();
return new (tree_1.default.Quoted)(str.charAt(0), str.substr(1, str.length - 2), isEscaped, index + currentIndex, fileInfo);
},
//
// A catch-all word, such as:
//
// black border-collapse
//
keyword: function () {
var k = parserInput.$char('%') || parserInput.$re(/^\[?(?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+\]?/);
if (k) {
return tree_1.default.Color.fromKeyword(k) || new (tree_1.default.Keyword)(k);
}
},
//
// A function call
//
// rgb(255, 0, 255)
//
// The arguments are parsed with the `entities.arguments` parser.
//
call: function () {
var name;
var args;
var func;
var index = parserInput.i;
// http://jsperf.com/case-insensitive-regex-vs-strtolower-then-regex/18
if (parserInput.peek(/^url\(/i)) {
return;
}
parserInput.save();
name = parserInput.$re(/^([\w-]+|%|~|progid:[\w.]+)\(/);
if (!name) {
parserInput.forget();
return;
}
name = name[1];
func = this.customFuncCall(name);
if (func) {
args = func.parse();
if (args && func.stop) {
parserInput.forget();
return args;
}
}
args = this.arguments(args);
if (!parserInput.$char(')')) {
parserInput.restore('Could not parse call arguments or missing \')\'');
return;
}
parserInput.forget();
return new (tree_1.default.Call)(name, args, index + currentIndex, fileInfo);
},
declarationCall: function () {
var validCall;
var args;
var index = parserInput.i;
parserInput.save();
validCall = parserInput.$re(/^[\w]+\(/);
if (!validCall) {
parserInput.forget();
return;
}
validCall = validCall.substring(0, validCall.length - 1);
var rule = this.ruleProperty();
var value;
if (rule) {
value = this.value();
}
if (rule && value) {
args = [new (tree_1.default.Declaration)(rule, value, null, null, parserInput.i + currentIndex, fileInfo, true)];
}
if (!parserInput.$char(')')) {
parserInput.restore('Could not parse call arguments or missing \')\'');
return;
}
parserInput.forget();
return new (tree_1.default.Call)(validCall, args, index + currentIndex, fileInfo);
},
//
// Parsing rules for functions with non-standard args, e.g.:
//
// boolean(not(2 > 1))
//
// This is a quick prototype, to be modified/improved when
// more custom-parsed funcs come (e.g. `selector(...)`)
//
customFuncCall: function (name) {
/* Ideally the table is to be moved out of here for faster perf.,
but it's quite tricky since it relies on all these `parsers`
and `expect` available only here */
return {
alpha: f(parsers.ieAlpha, true),
boolean: f(condition),
'if': f(condition)
}[name.toLowerCase()];
function f(parse, stop) {
return {
parse: parse,
stop: stop // when true - stop after parse() and return its result,
// otherwise continue for plain args
};
}
function condition() {
return [expect(parsers.condition, 'expected condition')];
}
},
arguments: function (prevArgs) {
var argsComma = prevArgs || [];
var argsSemiColon = [];
var isSemiColonSeparated;
var value;
parserInput.save();
while (true) {
if (prevArgs) {
prevArgs = false;
}
else {
value = parsers.detachedRuleset() || this.assignment() || parsers.expression();
if (!value) {
break;
}
if (value.value && value.value.length == 1) {
value = value.value[0];
}
argsComma.push(value);
}
if (parserInput.$char(',')) {
continue;
}
if (parserInput.$char(';') || isSemiColonSeparated) {
isSemiColonSeparated = true;
value = (argsComma.length < 1) ? argsComma[0]
: new tree_1.default.Value(argsComma);
argsSemiColon.push(value);
argsComma = [];
}
}
parserInput.forget();
return isSemiColonSeparated ? argsSemiColon : argsComma;
},
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;
var value;
parserInput.save();
key = parserInput.$re(/^\w+(?=\s?=)/i);
if (!key) {
parserInput.restore();
return;
}
if (!parserInput.$char('=')) {
parserInput.restore();
return;
}
value = parsers.entity();
if (value) {
parserInput.forget();
return new (tree_1.default.Assignment)(key, value);
}
else {
parserInput.restore();
}
},
//
// 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;
var index = parserInput.i;
parserInput.autoCommentAbsorb = false;
if (!parserInput.$str('url(')) {
parserInput.autoCommentAbsorb = true;
return;
}
value = this.quoted() || this.variable() || this.property() ||
parserInput.$re(/^(?:(?:\\[()'"])|[^()'"])+/) || '';
parserInput.autoCommentAbsorb = true;
expectChar(')');
return new (tree_1.default.URL)((value.value !== undefined ||
value instanceof tree_1.default.Variable ||
value instanceof tree_1.default.Property) ?
value : new (tree_1.default.Anonymous)(value, index), index + currentIndex, fileInfo);
},
//
// A Variable entity, such as `@fink`, in
//
// width: @fink + 2px
//
// We use a different parser for variable definitions,
// see `parsers.variable`.
//
variable: function () {
var ch;
var name;
var index = parserInput.i;
parserInput.save();
if (parserInput.currentChar() === '@' && (name = parserInput.$re(/^@@?[\w-]+/))) {
ch = parserInput.currentChar();
if (ch === '(' || ch === '[' && !parserInput.prevChar().match(/^\s/)) {
// this may be a VariableCall lookup
var result = parsers.variableCall(name);
if (result) {
parserInput.forget();
return result;
}
}
parserInput.forget();
return new (tree_1.default.Variable)(name, index + currentIndex, fileInfo);
}
parserInput.restore();
},
// A variable entity using the protective {} e.g. @{var}
variableCurly: function () {
var curly;
var index = parserInput.i;
if (parserInput.currentChar() === '@' && (curly = parserInput.$re(/^@\{([\w-]+)\}/))) {
return new (tree_1.default.Variable)("@".concat(curly[1]), index + currentIndex, fileInfo);
}
},
//
// A Property accessor, such as `$color`, in
//
// background-color: $color
//
property: function () {
var name;
var index = parserInput.i;
if (parserInput.currentChar() === '$' && (name = parserInput.$re(/^\$[\w-]+/))) {
return new (tree_1.default.Property)(name, index + currentIndex, fileInfo);
}
},
// A property entity useing the protective {} e.g. ${prop}
propertyCurly: function () {
var curly;
var index = parserInput.i;
if (parserInput.currentChar() === '$' && (curly = parserInput.$re(/^\$\{([\w-]+)\}/))) {
return new (tree_1.default.Property)("$".concat(curly[1]), index + currentIndex, fileInfo);
}
},
//
// A Hexadecimal color
//
// #4F3C2F
//
// `rgb` and `hsl` colors are parsed through the `entities.call` parser.
//
color: function () {
var rgb;
parserInput.save();
if (parserInput.currentChar() === '#' && (rgb = parserInput.$re(/^#([A-Fa-f0-9]{8}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{3,4})([\w.#[])?/))) {
if (!rgb[2]) {
parserInput.forget();
return new (tree_1.default.Color)(rgb[1], undefined, rgb[0]);
}
}
parserInput.restore();
},
colorKeyword: function () {
parserInput.save();
var autoCommentAbsorb = parserInput.autoCommentAbsorb;
parserInput.autoCommentAbsorb = false;
var k = parserInput.$re(/^[_A-Za-z-][_A-Za-z0-9-]+/);
parserInput.autoCommentAbsorb = autoCommentAbsorb;
if (!k) {
parserInput.forget();
return;
}
parserInput.restore();
var color = tree_1.default.Color.fromKeyword(k);
if (color) {
parserInput.$str(k);
return color;
}
},
//
// A Dimension, that is, a number and a unit
//
// 0.5em 95%
//
dimension: function () {
if (parserInput.peekNotNumeric()) {
return;
}
var value = parserInput.$re(/^([+-]?\d*\.?\d+)(%|[a-z_]+)?/i);
if (value) {
return new (tree_1.default.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 = parserInput.$re(/^U\+[0-9a-fA-F?]+(-[0-9a-fA-F?]+)?/);
if (ud) {
return new (tree_1.default.UnicodeDescriptor)(ud[0]);
}
},
//
// JavaScript code to be evaluated
//
// `window.location.href`
//
javascript: function () {
var js;
var index = parserInput.i;
parserInput.save();
var escape = parserInput.$char('~');
var jsQuote = parserInput.$char('`');
if (!jsQuote) {
parserInput.restore();
return;
}
js = parserInput.$re(/^[^`]*`/);
if (js) {
parserInput.forget();
return new (tree_1.default.JavaScript)(js.substr(0, js.length - 1), Boolean(escape), index + currentIndex, fileInfo);
}
parserInput.restore('invalid javascript definition');
}
},
//
// The variable part of a variable definition. Used in the `rule` parser
//
// @fink:
//
variable: function () {
var name;
if (parserInput.currentChar() === '@' && (name = parserInput.$re(/^(@[\w-]+)\s*:/))) {
return name[1];
}
},
//
// Call a variable value to retrieve a detached ruleset
// or a value from a detached ruleset's rules.
//
// @fink();
// @fink;
// color: @fink[@color];
//
variableCall: function (parsedName) {
var lookups;
var i = parserInput.i;
var inValue = !!parsedName;
var name = parsedName;
parserInput.save();
if (name || (parserInput.currentChar() === '@'
&& (name = parserInput.$re(/^(@[\w-]+)(\(\s*\))?/)))) {
lookups = this.mixin.ruleLookups();
if (!lookups && ((inValue && parserInput.$str('()') !== '()') || (name[2] !== '()'))) {
parserInput.restore('Missing \'[...]\' lookup in variable call');
return;
}
if (!inValue) {
name = name[1];
}
var call = new tree_1.default.VariableCall(name, i, fileInfo);
if (!inValue && parsers.end()) {
parserInput.forget();
return call;
}
else {
parserInput.forget();
return new tree_1.default.NamespaceValue(call, lookups, i, fileInfo);
}
}
parserInput.restore();
},
//
// extend syntax - used to extend selectors
//
extend: function (isRule) {
var elements;
var e;
var index = parserInput.i;
var option;
var extendList;
var extend;
if (!parserInput.$str(isRule ? '&:extend(' : ':extend(')) {
return;
}
do {
option = null;
elements = null;
var first = true;
while (!(option = parserInput.$re(/^(!?all)(?=\s*(\)|,))/))) {
e = this.element();
if (!e) {
break;
}
/**
* @note - This will not catch selectors in pseudos like :is() and :where() because
* they don't currently parse their contents as selectors.
*/
if (!first && e.combinator.value) {
warn('Targeting complex selectors can have unexpected behavior, and this behavior may change in the future.', index);
}
first = false;
if (elements) {
elements.push(e);
}
else {
elements = [e];
}
}
option = option && option[1];
if (!elements) {
error('Missing target selector for :extend().');
}
extend = new (tree_1.default.Extend)(new (tree_1.default.Selector)(elements), option, index + currentIndex, fileInfo);
if (extendList) {
extendList.push(extend);
}
else {
extendList = [extend];
}
} while (parserInput.$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);
// #mixins.square(#fff);
// .rounded(4px, black);
// .button;
//
// We can lookup / return a value using the lookup syntax:
//
// color: #mixin.square(#fff)[@color];
//
// The `while` loop is there because mixins can be
// namespaced, but we only support the child and descendant
// selector for now.
//
call: function (inValue, getLookup) {
var s = parserInput.currentChar();
var important = false;
var lookups;
var index = parserInput.i;
var elements;
var args;
var hasParens;
var parensIndex;
var parensWS = false;
if (s !== '.' && s !== '#') {
return;
}
parserInput.save(); // stop us absorbing part of an invalid selector
elements = this.elements();
if (elements) {
parensIndex = parserInput.i;
if (parserInput.$char('(')) {
parensWS = parserInput.isWhitespace(-2);
args = this.args(true).args;
expectChar(')');
hasParens = true;
if (parensWS) {
warn('Whitespace between a mixin name and parentheses for a mixin call is deprecated', parensIndex, 'DEPRECATED');
}
}
if (getLookup !== false) {
lookups = this.ruleLookups();
}
if (getLookup === true && !lookups) {
parserInput.restore();
return;
}
if (inValue && !lookups && !hasParens) {
// This isn't a valid in-value mixin call
parserInput.restore();
return;
}
if (!inValue && parsers.important()) {
important = true;
}
if (inValue || parsers.end()) {
parserInput.forget();
var mixin = new (tree_1.default.mixin.Call)(elements, args, index + currentIndex, fileInfo, !lookups && important);
if (lookups) {
return new tree_1.default.NamespaceValue(mixin, lookups);
}
else {
if (!hasParens) {
warn('Calling a mixin without parentheses is deprecated', parensIndex, 'DEPRECATED');
}
return mixin;
}
}
}
parserInput.restore();
},
/**
* Matching elements for mixins
* (Start with . or # and can have > )
*/
elements: function () {
var elements;
var e;
var c;
var elem;
var elemIndex;
var re = /^[#.](?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/;
while (true) {
elemIndex = parserInput.i;
e = parserInput.$re(re);
if (!e) {
break;
}
elem = new (tree_1.default.Element)(c, e, false, elemIndex + currentIndex, fileInfo);
if (elements) {
elements.push(elem);
}
else {
elements = [elem];
}
c = parserInput.$char('>');
}
return elements;
},
args: function (isCall) {
var entities = parsers.entities;
var returner = { args: null, variadic: false };
var expressions = [];
var argsSemiColon = [];
var argsComma = [];
var isSemiColonSeparated;
var expressionContainsNamed;
var name;
var nameLoop;
var value;
var arg;
var expand;
var hasSep = true;
parserInput.save();
while (true) {
if (isCall) {
arg = parsers.detachedRuleset() || parsers.expression();
}
else {
parserInput.commentStore.length = 0;
if (parserInput.$str('...')) {
returner.variadic = true;
if (parserInput.$char(';') && !isSemiColonSeparated) {
isSemiColonSeparated = true;
}
(isSemiColonSeparated ? argsSemiColon : argsComma)
.push({ variadic: true });
break;
}
arg = entities.variable() || entities.property() || entities.literal() || entities.keyword() || this.call(true);
}
if (!arg || !hasSep) {
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_1.default.Variable || val instanceof tree_1.default.Property)) {
if (parserInput.$char(':')) {
if (expressions.length > 0) {
if (isSemiColonSeparated) {
error('Cannot mix ; and , as delimiter types');
}
expressionContainsNamed = true;
}
value = parsers.detachedRuleset() || parsers.expression();
if (!value) {
if (isCall) {
error('could not understand value for named argument');
}
else {
parserInput.restore();
returner.args = [];
return returner;
}
}
nameLoop = (name = val.name);
}
else if (parserInput.$str('...')) {
if (!isCall) {
returner.variadic = true;
if (parserInput.$char(';') && !isSemiColonSeparated) {
isSemiColonSeparated = true;
}
(isSemiColonSeparated ? argsSemiColon : argsComma)
.push({ name: arg.name, variadic: true });
break;
}
else {
expand = true;
}
}
else if (!isCall) {
name = nameLoop = val.name;
value = null;
}
}
if (value) {
expressions.push(value);
}
argsComma.push({ name: nameLoop, value: value, expand: expand });
if (parserInput.$char(',')) {
hasSep = true;
continue;
}
hasSep = parserInput.$char(';') === ';';
if (hasSep || isSemiColonSeparated) {
if (expressionContainsNamed) {
error('Cannot mix ; and , as delimiter types');
}
isSemiColonSeparated = true;
if (expressions.length > 1) {
value = new (tree_1.default.Value)(expressions);
}
argsSemiColon.push({ name: name, value: value, expand: expand });
name = null;
expressions = [];
expressionContainsNamed = false;
}
}
parserInput.forget();
returner.args = isSemiColonSeparated ? 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;
var params = [];
var match;
var ruleset;
var cond;
var variadic = false;
if ((parserInput.currentChar() !== '.' && parserInput.currentChar() !== '#') ||
parserInput.peek(/^[^{]*\}/)) {
return;
}
parserInput.save();
match = parserInput.$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..
// also
// .mixincall(@a: {rule: set;});
// so we have to be nice and restore
if (!parserInput.$char(')')) {
parserInput.restore('Missing closing \')\'');
return;
}
parserInput.commentStore.length = 0;
if (parserInput.$str('when')) { // Guard
cond = expect(parsers.conditions, 'expected condition');
}
ruleset = parsers.block();
if (ruleset) {
parserInput.forget();
return new (tree_1.default.mixin.Definition)(name, params, ruleset, cond, variadic);
}
else {
parserInput.restore();
}
}
else {
parserInput.restore();
}
},
ruleLookups: function () {
var rule;
var lookups = [];
if (parserInput.currentChar() !== '[') {
return;
}
while (true) {
parserInput.save();
rule = this.lookupValue();
if (!rule && rule !== '') {
parserInput.restore();
break;
}
lookups.push(rule);