less
Version:
Leaner CSS
1,290 lines (1,124 loc) • 104 kB
JavaScript
import LessError from '../less-error.js';
import tree from '../tree/index.js';
import visitors from '../visitors/index.js';
import getParserInput from './parser-input.js';
import * as utils from '../utils.js';
import functionRegistry from '../functions/function-registry.js';
import { ContainerSyntaxOptions, MediaSyntaxOptions } from '../tree/atrule-syntax.js';
import logger from '../logger.js';
import { DeprecationHandler } from '../deprecation.js';
import Selector from '../tree/selector.js';
import Anonymous from '../tree/anonymous.js';
//
// 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.
//
const Parser = function Parser(context, imports, fileInfo, currentIndex) {
currentIndex = currentIndex || 0;
let parsers;
const parserInput = getParserInput();
function error(msg, type) {
throw new LessError(
{
index: parserInput.i,
filename: fileInfo.filename,
type: type || 'Syntax',
message: msg
},
imports
);
}
const deprecationHandler = new DeprecationHandler();
/**
* @param {string} msg
* @param {number} index
* @param {string} type
* @param {string} [deprecationId] - stable deprecation ID for repetition limiting
*/
function warn(msg, index, type, deprecationId) {
if (context.quiet) { return; }
if (deprecationId && context.quietDeprecations) { return; }
if (deprecationId && !deprecationHandler.shouldWarn(deprecationId)) { return; }
logger.warn(
(new LessError(
{
index: index ?? parserInput.i,
filename: fileInfo.filename,
type: type ? `${type.toUpperCase()} WARNING` : 'WARNING',
message: msg
},
imports
)).toString()
);
}
function expect(arg, msg) {
// some older browsers return typeof 'function' for RegExp
const result = (arg instanceof Function) ? arg.call(parsers) : parserInput.$re(arg);
if (result) {
return result;
}
error(msg || (typeof arg === 'string'
? `expected '${arg}' got '${parserInput.currentChar()}'`
: 'unexpected token'));
}
// Specialization of expect()
function expectChar(arg, msg) {
if (parserInput.$char(arg)) {
return arg;
}
error(msg || `expected '${arg}' got '${parserInput.currentChar()}'`);
}
function getDebugInfo(index) {
const 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) {
let result;
const returnNodes = [];
const parser = parserInput;
try {
parser.start(str);
for (let x = 0, p; (p = parseList[x]); x++) {
result = parsers[p]();
returnNodes.push(result || null);
}
const endInfo = parser.end();
if (endInfo.isFinished) {
callback(null, returnNodes);
}
else {
callback(true, null);
}
} catch (e) {
throw new LessError({
index: e.index + currentIndex,
message: e.message
}, imports, fileInfo.filename);
}
}
//
// The Parser
//
return {
parserInput,
imports,
fileInfo,
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) {
let root;
let err = null;
let globalVars;
let modifyVars;
let ignored;
let 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) ? `${Parser.serializeVars(additionalData.globalVars)}\n` : '';
modifyVars = (additionalData && additionalData.modifyVars) ? `\n${Parser.serializeVars(additionalData.modifyVars)}` : '';
if (context.pluginManager) {
const preProcessors = context.pluginManager.getPreProcessors();
for (let i = 0; i < preProcessors.length; i++) {
str = preProcessors[i].process(str, { context, imports, 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);
tree.Node.prototype.parse = this;
root = new tree.Ruleset(null, this.parsers.primary());
tree.Node.prototype.rootNode = root;
root.root = true;
root.firstRoot = true;
root.functionRegistry = functionRegistry.inherit();
} catch (e) {
return callback(new LessError(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.
const endInfo = parserInput.end();
if (!endInfo.isFinished) {
let 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 LessError({
type: 'Parse',
message,
index: endInfo.furthest,
filename: fileInfo.filename
}, imports);
}
const finish = e => {
e = err || e || imports.error;
if (e) {
if (!(e instanceof LessError)) {
e = new LessError(e, imports, fileInfo.filename);
}
return callback(e);
}
else {
return callback(null, root);
}
};
if (context.processImports !== false) {
new visitors.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 () {
const mixin = this.mixin;
let root = [];
let 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 {
let 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) {
const comment = parserInput.commentStore.shift();
return new(tree.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) {
let str;
const index = parserInput.i;
let 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.Quoted)(str.charAt(0), str.slice(1, -1), isEscaped, index + currentIndex, fileInfo);
},
//
// A catch-all word, such as:
//
// black border-collapse
//
keyword: function () {
const k = parserInput.$char('%') || parserInput.$re(/^\[?(?:[\w-]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+\]?/);
if (k) {
return tree.Color.fromKeyword(k) || new(tree.Keyword)(k);
}
},
//
// A function call
//
// rgb(255, 0, 255)
//
// The arguments are parsed with the `entities.arguments` parser.
//
call: function () {
let name;
let args;
let func;
const 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.Call)(name, args, index + currentIndex, fileInfo);
},
declarationCall: function () {
let validCall;
let args;
const index = parserInput.i;
parserInput.save();
validCall = parserInput.$re(/^[\w]+\(/);
if (!validCall) {
parserInput.forget();
return;
}
validCall = validCall.substring(0, validCall.length - 1);
// CSS at-rule keywords should never be parsed as declaration calls
if (/^(and|or|not|only|layer)$/i.test(validCall)) {
parserInput.restore();
return;
}
let rule = this.ruleProperty();
let value;
if (rule) {
value = this.value();
}
if (rule && value) {
args = [new (tree.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.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, // parsing function
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) {
let argsComma = prevArgs || [];
const argsSemiColon = [];
let isSemiColonSeparated;
let 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.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 () {
let key;
let 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.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 () {
let value;
const 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.URL)((value.value !== undefined ||
value instanceof tree.Variable ||
value instanceof tree.Property) ?
value : new(tree.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 () {
let ch;
let name;
const index = parserInput.i;
parserInput.save();
if (parserInput.currentChar() === '@' && (name = parserInput.$re(/^@@?[\w-]+/))) {
ch = parserInput.currentChar();
if ((ch === '(' && !parserInput.prevChar().match(/^\s/))
|| (ch === '[' && !parserInput.prevChar().match(/^\s/))) {
// this may be a VariableCall lookup
const result = parsers.variableCall(name);
if (result) {
parserInput.forget();
return result;
}
}
parserInput.forget();
return new(tree.Variable)(name, index + currentIndex, fileInfo);
}
parserInput.restore();
},
// A variable entity using the protective {} e.g. @{var}
variableCurly: function () {
let curly;
const index = parserInput.i;
if (parserInput.currentChar() === '@' && (curly = parserInput.$re(/^@\{([\w-]+)\}/))) {
return new(tree.Variable)(`@${curly[1]}`, index + currentIndex, fileInfo);
}
},
//
// A Property accessor, such as `$color`, in
//
// background-color: $color
//
property: function () {
let name;
const index = parserInput.i;
if (parserInput.currentChar() === '$' && (name = parserInput.$re(/^\$[\w-]+/))) {
return new(tree.Property)(name, index + currentIndex, fileInfo);
}
},
//
// A Hexadecimal color
//
// #4F3C2F
//
// `rgb` and `hsl` colors are parsed through the `entities.call` parser.
//
color: function () {
let 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.Color)(rgb[1], undefined, rgb[0]);
}
}
parserInput.restore();
},
colorKeyword: function () {
parserInput.save();
const autoCommentAbsorb = parserInput.autoCommentAbsorb;
parserInput.autoCommentAbsorb = false;
const k = parserInput.$re(/^[_A-Za-z-][_A-Za-z0-9-]+/);
parserInput.autoCommentAbsorb = autoCommentAbsorb;
if (!k) {
parserInput.forget();
return;
}
parserInput.restore();
const color = tree.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;
}
const value = parserInput.$re(/^([+-]?\d*\.?\d+)(%|[a-z_]+)?/i);
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 () {
let ud;
ud = parserInput.$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 () {
let js;
const index = parserInput.i;
parserInput.save();
const escape = parserInput.$char('~');
const jsQuote = parserInput.$char('`');
if (!jsQuote) {
parserInput.restore();
return;
}
js = parserInput.$re(/^[^`]*`/);
if (js) {
warn('Inline JavaScript evaluation (backtick expressions) is deprecated and will be removed in Less 5.x. Use Less functions or custom plugins instead.', index, 'DEPRECATED', 'js-eval');
parserInput.forget();
return new(tree.JavaScript)(js.slice(0, -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 () {
let 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) {
let lookups;
const i = parserInput.i;
const inValue = !!parsedName;
let 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];
}
const call = new tree.VariableCall(name, i, fileInfo);
if (!inValue && parsers.end()) {
parserInput.forget();
return call;
}
else {
parserInput.forget();
return new tree.NamespaceValue(call, lookups, i, fileInfo);
}
}
parserInput.restore();
},
//
// extend syntax - used to extend selectors
//
extend: function(isRule) {
let elements;
let e;
const index = parserInput.i;
let option;
let extendList;
let extend;
if (!parserInput.$str(isRule ? '&:extend(' : ':extend(')) {
return;
}
do {
option = null;
elements = null;
let 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.Extend)(new(tree.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) {
const s = parserInput.currentChar();
let important = false;
let lookups;
const index = parserInput.i;
let elements;
let args;
let hasParens;
let parensIndex;
let parensWS = false;
if (s !== '.' && s !== '#') { return; }
parserInput.save(); // stop us absorbing part of an invalid selector
elements = this.elements();
if (elements) {
parensIndex = parserInput.i;
parensWS = parserInput.isWhitespace(-1);
if (parserInput.$char('(')) {
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', 'mixin-call-whitespace');
}
}
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();
const mixin = new(tree.mixin.Call)(elements, args, index + currentIndex, fileInfo, !lookups && important);
if (lookups) {
return new tree.NamespaceValue(mixin, lookups);
}
else {
if (!hasParens) {
warn('Calling a mixin without parentheses is deprecated', parensIndex, 'DEPRECATED', 'mixin-call-no-parens');
}
return mixin;
}
}
}
parserInput.restore();
},
/**
* Matching elements for mixins
* (Start with . or # and can have > )
*/
elements: function() {
let elements;
let e;
let c;
let elem;
let elemIndex;
const 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.Element)(c, e, false, elemIndex + currentIndex, fileInfo);
if (elements) {
elements.push(elem);
} else {
elements = [ elem ];
}
c = parserInput.$char('>');
}
return elements;
},
args: function (isCall) {
const entities = parsers.entities;
const returner = { args:null, variadic: false };
let expressions = [];
const argsSemiColon = [];
const argsComma = [];
let isSemiColonSeparated;
let expressionContainsNamed;
let name;
let nameLoop;
let value;
let arg;
let expand;
let 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;
let 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 || val instanceof tree.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, 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.Value)(expressions);
}
argsSemiColon.push({ name, value, 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 () {
let name;
let params = [];
let match;
let ruleset;
let cond;
let 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];
const 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.mixin.Definition)(name, params, ruleset, cond, variadic);
} else {
parserInput.restore();
}
} else {
parserInput.restore();
}
},
ruleLookups: function() {
let rule;
const lookups = [];
if (parserInput.currentChar() !== '[') {
return;
}
while (true) {
parserInput.save();
rule = this.lookupValue();
if (!rule && rule !== '') {
parserInput.restore();
break;
}
lookups.push(rule);
parserInput.forget();
}
if (lookups.length > 0) {
return lookups;
}
},
lookupValue: function() {
parserInput.save();
if (!parserInput.$char('[')) {
parserInput.restore();
return;
}
const name = parserInput.$re(/^(?:[@$]{0,2})[_a-zA-Z0-9-]*/);
if (!parserInput.$char(']')) {
parserInput.restore();
return;
}
if (name || name === '') {
parserInput.forget();
return name;
}
parserInput.restore();
}
},
//
// Entities are the smallest recognized token,
// and can be found inside a rule's value.
//
entity: function () {
const entities = this.entities;
return this.comment() || entities.literal() || entities.va