654b297e2e
- Connexion API via proxy Angular (résolution CORS, base path /api) - Import CSS ng-zorro global pour les modales et composants - Filtres Camion/Show câblés sur l'affichage de la grille - Camions affichés via TrucksService (linkés au show du même créneau) - Panneau de détails : spectacles + camions du jour sélectionné - Modale de création de spectacle stylisée avec fond et centrage - Positionnement précis des events à la minute dans leur créneau - Auto-scroll vers l'heure courante au chargement - Ligne "maintenant" sur la colonne du jour actuel - Régénération des services OpenAPI (nouveaux noms de types) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2476 lines
104 KiB
JavaScript
2476 lines
104 KiB
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);
|
|
parserInput.forget();
|
|
}
|
|
if (lookups.length > 0) {
|
|
return lookups;
|
|
}
|
|
},
|
|
lookupValue: function () {
|
|
parserInput.save();
|
|
if (!parserInput.$char('[')) {
|
|
parserInput.restore();
|
|
return;
|
|
}
|
|
var 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 () {
|
|
var entities = this.entities;
|
|
return this.comment() || entities.literal() || entities.variable() || entities.url() ||
|
|
entities.property() || entities.call() || entities.keyword() || this.mixin.call(true) ||
|
|
entities.javascript();
|
|
},
|
|
//
|
|
// A Declaration terminator. Note that we use `peek()` to check for '}',
|
|
// because the `block` rule will be expecting it, but we still need to make sure
|
|
// it's there, if ';' was omitted.
|
|
//
|
|
end: function () {
|
|
return parserInput.$char(';') || parserInput.peek('}');
|
|
},
|
|
//
|
|
// IE's alpha function
|
|
//
|
|
// alpha(opacity=88)
|
|
//
|
|
ieAlpha: function () {
|
|
var value;
|
|
// http://jsperf.com/case-insensitive-regex-vs-strtolower-then-regex/18
|
|
if (!parserInput.$re(/^opacity=/i)) {
|
|
return;
|
|
}
|
|
value = parserInput.$re(/^\d+/);
|
|
if (!value) {
|
|
value = expect(parsers.entities.variable, 'Could not parse alpha');
|
|
value = "@{".concat(value.name.slice(1), "}");
|
|
}
|
|
expectChar(')');
|
|
return new tree_1.default.Quoted('', "alpha(opacity=".concat(value, ")"));
|
|
},
|
|
/**
|
|
* A Selector Element
|
|
*
|
|
* div
|
|
* + h1
|
|
* #socks
|
|
* input[type="text"]
|
|
*
|
|
* Elements are the building blocks for Selectors,
|
|
* they are made out of a `Combinator` (see combinator rule),
|
|
* and an element name, such as a tag a class, or `*`.
|
|
*/
|
|
element: function () {
|
|
var e;
|
|
var c;
|
|
var v;
|
|
var index = parserInput.i;
|
|
c = this.combinator();
|
|
/** This selector parser is quite simplistic and will pass a number of invalid selectors. */
|
|
e = parserInput.$re(/^(?:\d+\.\d+|\d+)%/) ||
|
|
// eslint-disable-next-line no-control-regex
|
|
parserInput.$re(/^(?:[.#]?|:*)(?:[\w-]|[^\x00-\x9f]|\\(?:[A-Fa-f0-9]{1,6} ?|[^A-Fa-f0-9]))+/) ||
|
|
parserInput.$char('*') || parserInput.$char('&') || this.attribute() ||
|
|
parserInput.$re(/^\([^&()@]+\)/) || parserInput.$re(/^[.#:](?=@)/) ||
|
|
this.entities.variableCurly();
|
|
if (!e) {
|
|
parserInput.save();
|
|
if (parserInput.$char('(')) {
|
|
if ((v = this.selector(false))) {
|
|
var selectors = [];
|
|
while (parserInput.$char(',')) {
|
|
selectors.push(v);
|
|
selectors.push(new anonymous_1.default(','));
|
|
v = this.selector(false);
|
|
}
|
|
selectors.push(v);
|
|
if (parserInput.$char(')')) {
|
|
if (selectors.length > 1) {
|
|
e = new (tree_1.default.Paren)(new selector_1.default(selectors));
|
|
}
|
|
else {
|
|
e = new (tree_1.default.Paren)(v);
|
|
}
|
|
parserInput.forget();
|
|
}
|
|
else {
|
|
parserInput.restore('Missing closing \')\'');
|
|
}
|
|
}
|
|
else {
|
|
parserInput.restore('Missing closing \')\'');
|
|
}
|
|
}
|
|
else {
|
|
parserInput.forget();
|
|
}
|
|
}
|
|
if (e) {
|
|
return new (tree_1.default.Element)(c, e, e instanceof tree_1.default.Variable, index + currentIndex, fileInfo);
|
|
}
|
|
},
|
|
//
|
|
// Combinators combine elements together, in a Selector.
|
|
//
|
|
// Because our parser isn't white-space sensitive, special care
|
|
// has to be taken, when parsing the descendant combinator, ` `,
|
|
// as it's an empty space. We have to check the previous character
|
|
// in the input, to see if it's a ` ` character. More info on how
|
|
// we deal with this in *combinator.js*.
|
|
//
|
|
combinator: function () {
|
|
var c = parserInput.currentChar();
|
|
if (c === '/') {
|
|
parserInput.save();
|
|
var slashedCombinator = parserInput.$re(/^\/[a-z]+\//i);
|
|
if (slashedCombinator) {
|
|
parserInput.forget();
|
|
return new (tree_1.default.Combinator)(slashedCombinator);
|
|
}
|
|
parserInput.restore();
|
|
}
|
|
if (c === '>' || c === '+' || c === '~' || c === '|' || c === '^') {
|
|
parserInput.i++;
|
|
if (c === '^' && parserInput.currentChar() === '^') {
|
|
c = '^^';
|
|
parserInput.i++;
|
|
}
|
|
while (parserInput.isWhitespace()) {
|
|
parserInput.i++;
|
|
}
|
|
return new (tree_1.default.Combinator)(c);
|
|
}
|
|
else if (parserInput.isWhitespace(-1)) {
|
|
return new (tree_1.default.Combinator)(' ');
|
|
}
|
|
else {
|
|
return new (tree_1.default.Combinator)(null);
|
|
}
|
|
},
|
|
//
|
|
// A CSS Selector
|
|
// with less extensions e.g. the ability to extend and guard
|
|
//
|
|
// .class > div + h1
|
|
// li a:hover
|
|
//
|
|
// Selectors are made out of one or more Elements, see above.
|
|
//
|
|
selector: function (isLess) {
|
|
var index = parserInput.i;
|
|
var elements;
|
|
var extendList;
|
|
var c;
|
|
var e;
|
|
var allExtends;
|
|
var when;
|
|
var condition;
|
|
isLess = isLess !== false;
|
|
while ((isLess && (extendList = this.extend())) || (isLess && (when = parserInput.$str('when'))) || (e = this.element())) {
|
|
if (when) {
|
|
condition = expect(this.conditions, 'expected condition');
|
|
}
|
|
else if (condition) {
|
|
error('CSS guard can only be used at the end of selector');
|
|
}
|
|
else if (extendList) {
|
|
if (allExtends) {
|
|
allExtends = allExtends.concat(extendList);
|
|
}
|
|
else {
|
|
allExtends = extendList;
|
|
}
|
|
}
|
|
else {
|
|
if (allExtends) {
|
|
error('Extend can only be used at the end of selector');
|
|
}
|
|
c = parserInput.currentChar();
|
|
if (Array.isArray(e)) {
|
|
e.forEach(function (ele) { return elements.push(ele); });
|
|
}
|
|
if (elements) {
|
|
elements.push(e);
|
|
}
|
|
else {
|
|
elements = [e];
|
|
}
|
|
e = null;
|
|
}
|
|
if (c === '{' || c === '}' || c === ';' || c === ',' || c === ')') {
|
|
break;
|
|
}
|
|
}
|
|
if (elements) {
|
|
return new (tree_1.default.Selector)(elements, allExtends, condition, index + currentIndex, fileInfo);
|
|
}
|
|
if (allExtends) {
|
|
error('Extend must be used to extend a selector, it cannot be used on its own');
|
|
}
|
|
},
|
|
selectors: function () {
|
|
var s;
|
|
var selectors;
|
|
while (true) {
|
|
s = this.selector();
|
|
if (!s) {
|
|
break;
|
|
}
|
|
if (selectors) {
|
|
selectors.push(s);
|
|
}
|
|
else {
|
|
selectors = [s];
|
|
}
|
|
parserInput.commentStore.length = 0;
|
|
if (s.condition && selectors.length > 1) {
|
|
error('Guards are only currently allowed on a single selector.');
|
|
}
|
|
if (!parserInput.$char(',')) {
|
|
break;
|
|
}
|
|
if (s.condition) {
|
|
error('Guards are only currently allowed on a single selector.');
|
|
}
|
|
parserInput.commentStore.length = 0;
|
|
}
|
|
return selectors;
|
|
},
|
|
attribute: function () {
|
|
if (!parserInput.$char('[')) {
|
|
return;
|
|
}
|
|
var entities = this.entities;
|
|
var key;
|
|
var val;
|
|
var op;
|
|
//
|
|
// case-insensitive flag
|
|
// e.g. [attr operator value i]
|
|
//
|
|
var cif;
|
|
if (!(key = entities.variableCurly())) {
|
|
key = expect(/^(?:[_A-Za-z0-9-*]*\|)?(?:[_A-Za-z0-9-]|\\.)+/);
|
|
}
|
|
op = parserInput.$re(/^[|~*$^]?=/);
|
|
if (op) {
|
|
val = entities.quoted() || parserInput.$re(/^[0-9]+%/) || parserInput.$re(/^[\w-]+/) || entities.variableCurly();
|
|
if (val) {
|
|
cif = parserInput.$re(/^[iIsS]/);
|
|
}
|
|
}
|
|
expectChar(']');
|
|
return new (tree_1.default.Attribute)(key, op, val, cif);
|
|
},
|
|
//
|
|
// The `block` rule is used by `ruleset` and `mixin.definition`.
|
|
// It's a wrapper around the `primary` rule, with added `{}`.
|
|
//
|
|
block: function () {
|
|
var content;
|
|
if (parserInput.$char('{') && (content = this.primary()) && parserInput.$char('}')) {
|
|
return content;
|
|
}
|
|
},
|
|
blockRuleset: function () {
|
|
var block = this.block();
|
|
if (block) {
|
|
block = new tree_1.default.Ruleset(null, block);
|
|
}
|
|
return block;
|
|
},
|
|
detachedRuleset: function () {
|
|
var argInfo;
|
|
var params;
|
|
var variadic;
|
|
parserInput.save();
|
|
if (parserInput.$re(/^[.#]\(/)) {
|
|
/**
|
|
* DR args currently only implemented for each() function, and not
|
|
* yet settable as `@dr: #(@arg) {}`
|
|
* This should be done when DRs are merged with mixins.
|
|
* See: https://github.com/less/less-meta/issues/16
|
|
*/
|
|
argInfo = this.mixin.args(false);
|
|
params = argInfo.args;
|
|
variadic = argInfo.variadic;
|
|
if (!parserInput.$char(')')) {
|
|
parserInput.restore();
|
|
return;
|
|
}
|
|
}
|
|
var blockRuleset = this.blockRuleset();
|
|
if (blockRuleset) {
|
|
parserInput.forget();
|
|
if (params) {
|
|
return new tree_1.default.mixin.Definition(null, params, blockRuleset, null, variadic);
|
|
}
|
|
return new tree_1.default.DetachedRuleset(blockRuleset);
|
|
}
|
|
parserInput.restore();
|
|
},
|
|
//
|
|
// div, .class, body > p {...}
|
|
//
|
|
ruleset: function () {
|
|
var selectors;
|
|
var rules;
|
|
var debugInfo;
|
|
parserInput.save();
|
|
if (context.dumpLineNumbers) {
|
|
debugInfo = getDebugInfo(parserInput.i);
|
|
}
|
|
selectors = this.selectors();
|
|
if (selectors && (rules = this.block())) {
|
|
parserInput.forget();
|
|
var ruleset = new (tree_1.default.Ruleset)(selectors, rules, context.strictImports);
|
|
if (context.dumpLineNumbers) {
|
|
ruleset.debugInfo = debugInfo;
|
|
}
|
|
return ruleset;
|
|
}
|
|
else {
|
|
parserInput.restore();
|
|
}
|
|
},
|
|
declaration: function () {
|
|
var name;
|
|
var value;
|
|
var index = parserInput.i;
|
|
var hasDR;
|
|
var c = parserInput.currentChar();
|
|
var important;
|
|
var merge;
|
|
var isVariable;
|
|
if (c === '.' || c === '#' || c === '&' || c === ':') {
|
|
return;
|
|
}
|
|
parserInput.save();
|
|
name = this.variable() || this.ruleProperty();
|
|
if (name) {
|
|
isVariable = typeof name === 'string';
|
|
if (isVariable) {
|
|
value = this.detachedRuleset();
|
|
if (value) {
|
|
hasDR = true;
|
|
}
|
|
}
|
|
parserInput.commentStore.length = 0;
|
|
if (!value) {
|
|
// a name returned by this.ruleProperty() is always an array of the form:
|
|
// [string-1, ..., string-n, ""] or [string-1, ..., string-n, "+"]
|
|
// where each item is a tree.Keyword or tree.Variable
|
|
merge = !isVariable && name.length > 1 && name.pop().value;
|
|
// Custom property values get permissive parsing
|
|
if (name[0].value && name[0].value.slice(0, 2) === '--') {
|
|
if (parserInput.$char(';')) {
|
|
value = new anonymous_1.default('');
|
|
}
|
|
else {
|
|
value = this.permissiveValue(/[;}]/, true);
|
|
}
|
|
}
|
|
// Try to store values as anonymous
|
|
// If we need the value later we'll re-parse it in ruleset.parseValue
|
|
else {
|
|
value = this.anonymousValue();
|
|
}
|
|
if (value) {
|
|
parserInput.forget();
|
|
// anonymous values absorb the end ';' which is required for them to work
|
|
return new (tree_1.default.Declaration)(name, value, false, merge, index + currentIndex, fileInfo);
|
|
}
|
|
if (!value) {
|
|
value = this.value();
|
|
}
|
|
if (value) {
|
|
important = this.important();
|
|
}
|
|
else if (isVariable) {
|
|
/**
|
|
* As a last resort, try permissiveValue
|
|
*
|
|
* @todo - This has created some knock-on problems of not
|
|
* flagging incorrect syntax or detecting user intent.
|
|
*/
|
|
value = this.permissiveValue();
|
|
}
|
|
}
|
|
if (value && (this.end() || hasDR)) {
|
|
parserInput.forget();
|
|
return new (tree_1.default.Declaration)(name, value, important, merge, index + currentIndex, fileInfo);
|
|
}
|
|
else {
|
|
parserInput.restore();
|
|
}
|
|
}
|
|
else {
|
|
parserInput.restore();
|
|
}
|
|
},
|
|
anonymousValue: function () {
|
|
var index = parserInput.i;
|
|
var match = parserInput.$re(/^([^.#@$+/'"*`(;{}-]*);/);
|
|
if (match) {
|
|
return new (tree_1.default.Anonymous)(match[1], index + currentIndex);
|
|
}
|
|
},
|
|
/**
|
|
* Used for custom properties, at-rules, and variables (as fallback)
|
|
* Parses almost anything inside of {} [] () "" blocks
|
|
* until it reaches outer-most tokens.
|
|
*
|
|
* First, it will try to parse comments and entities to reach
|
|
* the end. This is mostly like the Expression parser except no
|
|
* math is allowed.
|
|
*
|
|
* @param {RexExp} untilTokens - Characters to stop parsing at
|
|
*/
|
|
permissiveValue: function (untilTokens) {
|
|
var i;
|
|
var e;
|
|
var done;
|
|
var value;
|
|
var tok = untilTokens || ';';
|
|
var index = parserInput.i;
|
|
var result = [];
|
|
function testCurrentChar() {
|
|
var char = parserInput.currentChar();
|
|
if (typeof tok === 'string') {
|
|
return char === tok;
|
|
}
|
|
else {
|
|
return tok.test(char);
|
|
}
|
|
}
|
|
if (testCurrentChar()) {
|
|
return;
|
|
}
|
|
value = [];
|
|
do {
|
|
e = this.comment();
|
|
if (e) {
|
|
value.push(e);
|
|
continue;
|
|
}
|
|
e = this.entity();
|
|
if (e) {
|
|
value.push(e);
|
|
}
|
|
if (parserInput.peek(',')) {
|
|
value.push(new (tree_1.default.Anonymous)(',', parserInput.i));
|
|
parserInput.$char(',');
|
|
}
|
|
} while (e);
|
|
done = testCurrentChar();
|
|
if (value.length > 0) {
|
|
value = new (tree_1.default.Expression)(value);
|
|
if (done) {
|
|
return value;
|
|
}
|
|
else {
|
|
result.push(value);
|
|
}
|
|
// Preserve space before $parseUntil as it will not
|
|
if (parserInput.prevChar() === ' ') {
|
|
result.push(new tree_1.default.Anonymous(' ', index));
|
|
}
|
|
}
|
|
parserInput.save();
|
|
value = parserInput.$parseUntil(tok);
|
|
if (value) {
|
|
if (typeof value === 'string') {
|
|
error("Expected '".concat(value, "'"), 'Parse');
|
|
}
|
|
if (value.length === 1 && value[0] === ' ') {
|
|
parserInput.forget();
|
|
return new tree_1.default.Anonymous('', index);
|
|
}
|
|
/** @type {string} */
|
|
var item = void 0;
|
|
for (i = 0; i < value.length; i++) {
|
|
item = value[i];
|
|
if (Array.isArray(item)) {
|
|
// Treat actual quotes as normal quoted values
|
|
result.push(new tree_1.default.Quoted(item[0], item[1], true, index, fileInfo));
|
|
}
|
|
else {
|
|
if (i === value.length - 1) {
|
|
item = item.trim();
|
|
}
|
|
// Treat like quoted values, but replace vars like unquoted expressions
|
|
var quote = new tree_1.default.Quoted('\'', item, true, index, fileInfo);
|
|
var variableRegex = /@([\w-]+)/g;
|
|
var propRegex = /\$([\w-]+)/g;
|
|
if (variableRegex.test(item)) {
|
|
warn('@[ident] in unknown values will not be evaluated as variables in the future. Use @{[ident]}', index, 'DEPRECATED');
|
|
}
|
|
if (propRegex.test(item)) {
|
|
warn('$[ident] in unknown values will not be evaluated as property references in the future. Use ${[ident]}', index, 'DEPRECATED');
|
|
}
|
|
quote.variableRegex = /@([\w-]+)|@{([\w-]+)}/g;
|
|
quote.propRegex = /\$([\w-]+)|\${([\w-]+)}/g;
|
|
result.push(quote);
|
|
}
|
|
}
|
|
parserInput.forget();
|
|
return new tree_1.default.Expression(result, true);
|
|
}
|
|
parserInput.restore();
|
|
},
|
|
//
|
|
// An @import atrule
|
|
//
|
|
// @import "lib";
|
|
//
|
|
// Depending on our environment, importing is done differently:
|
|
// In the browser, it's an XHR request, in Node, it would be a
|
|
// file-system operation. The function used for importing is
|
|
// stored in `import`, which we pass to the Import constructor.
|
|
//
|
|
'import': function () {
|
|
var path;
|
|
var features;
|
|
var index = parserInput.i;
|
|
var dir = parserInput.$re(/^@import\s+/);
|
|
if (dir) {
|
|
var options = (dir ? this.importOptions() : null) || {};
|
|
if ((path = this.entities.quoted() || this.entities.url())) {
|
|
features = this.mediaFeatures({});
|
|
if (!parserInput.$char(';')) {
|
|
parserInput.i = index;
|
|
error('missing semi-colon or unrecognised media features on import');
|
|
}
|
|
features = features && new (tree_1.default.Value)(features);
|
|
return new (tree_1.default.Import)(path, features, options, index + currentIndex, fileInfo);
|
|
}
|
|
else {
|
|
parserInput.i = index;
|
|
error('malformed import statement');
|
|
}
|
|
}
|
|
},
|
|
importOptions: function () {
|
|
var o;
|
|
var options = {};
|
|
var optionName;
|
|
var value;
|
|
// list of options, surrounded by parens
|
|
if (!parserInput.$char('(')) {
|
|
return null;
|
|
}
|
|
do {
|
|
o = this.importOption();
|
|
if (o) {
|
|
optionName = o;
|
|
value = true;
|
|
switch (optionName) {
|
|
case 'css':
|
|
optionName = 'less';
|
|
value = false;
|
|
break;
|
|
case 'once':
|
|
optionName = 'multiple';
|
|
value = false;
|
|
break;
|
|
}
|
|
options[optionName] = value;
|
|
if (!parserInput.$char(',')) {
|
|
break;
|
|
}
|
|
}
|
|
} while (o);
|
|
expectChar(')');
|
|
return options;
|
|
},
|
|
importOption: function () {
|
|
var opt = parserInput.$re(/^(less|css|multiple|once|inline|reference|optional)/);
|
|
if (opt) {
|
|
return opt[1];
|
|
}
|
|
},
|
|
mediaFeature: function (syntaxOptions) {
|
|
var entities = this.entities;
|
|
var nodes = [];
|
|
var e;
|
|
var p;
|
|
var rangeP;
|
|
var spacing = false;
|
|
parserInput.save();
|
|
do {
|
|
parserInput.save();
|
|
if (parserInput.$re(/^[0-9a-z-]*\s+\(/)) {
|
|
spacing = true;
|
|
}
|
|
parserInput.restore();
|
|
e = entities.declarationCall.bind(this)() || entities.keyword() || entities.variable() || entities.mixinLookup();
|
|
if (e) {
|
|
nodes.push(e);
|
|
}
|
|
else if (parserInput.$char('(')) {
|
|
p = this.property();
|
|
parserInput.save();
|
|
if (!p && syntaxOptions.queryInParens && parserInput.$re(/^[0-9a-z-]*\s*([<>]=|<=|>=|[<>]|=)/)) {
|
|
parserInput.restore();
|
|
p = this.condition();
|
|
parserInput.save();
|
|
rangeP = this.atomicCondition(null, p.rvalue);
|
|
if (!rangeP) {
|
|
parserInput.restore();
|
|
}
|
|
}
|
|
else {
|
|
parserInput.restore();
|
|
e = this.value();
|
|
}
|
|
if (parserInput.$char(')')) {
|
|
if (p && !e) {
|
|
nodes.push(new (tree_1.default.Paren)(new (tree_1.default.QueryInParens)(p.op, p.lvalue, p.rvalue, rangeP ? rangeP.op : null, rangeP ? rangeP.rvalue : null, p._index)));
|
|
e = p;
|
|
}
|
|
else if (p && e) {
|
|
nodes.push(new (tree_1.default.Paren)(new (tree_1.default.Declaration)(p, e, null, null, parserInput.i + currentIndex, fileInfo, true)));
|
|
if (!spacing) {
|
|
nodes[nodes.length - 1].noSpacing = true;
|
|
}
|
|
spacing = false;
|
|
}
|
|
else if (e) {
|
|
nodes.push(new (tree_1.default.Paren)(e));
|
|
spacing = false;
|
|
}
|
|
else {
|
|
error('badly formed media feature definition');
|
|
}
|
|
}
|
|
else {
|
|
error('Missing closing \')\'', 'Parse');
|
|
}
|
|
}
|
|
} while (e);
|
|
parserInput.forget();
|
|
if (nodes.length > 0) {
|
|
return new (tree_1.default.Expression)(nodes);
|
|
}
|
|
},
|
|
mediaFeatures: function (syntaxOptions) {
|
|
var entities = this.entities;
|
|
var features = [];
|
|
var e;
|
|
do {
|
|
e = this.mediaFeature(syntaxOptions);
|
|
if (e) {
|
|
features.push(e);
|
|
if (!parserInput.$char(',')) {
|
|
break;
|
|
}
|
|
else if (!features[features.length - 1].noSpacing) {
|
|
features[features.length - 1].noSpacing = false;
|
|
}
|
|
}
|
|
else {
|
|
e = entities.variable() || entities.mixinLookup();
|
|
if (e) {
|
|
features.push(e);
|
|
if (!parserInput.$char(',')) {
|
|
break;
|
|
}
|
|
else if (!features[features.length - 1].noSpacing) {
|
|
features[features.length - 1].noSpacing = false;
|
|
}
|
|
}
|
|
}
|
|
} while (e);
|
|
return features.length > 0 ? features : null;
|
|
},
|
|
prepareAndGetNestableAtRule: function (treeType, index, debugInfo, syntaxOptions) {
|
|
var features = this.mediaFeatures(syntaxOptions);
|
|
var rules = this.block();
|
|
if (!rules) {
|
|
error('media definitions require block statements after any features');
|
|
}
|
|
parserInput.forget();
|
|
var atRule = new (treeType)(rules, features, index + currentIndex, fileInfo);
|
|
if (context.dumpLineNumbers) {
|
|
atRule.debugInfo = debugInfo;
|
|
}
|
|
return atRule;
|
|
},
|
|
nestableAtRule: function () {
|
|
var debugInfo;
|
|
var index = parserInput.i;
|
|
if (context.dumpLineNumbers) {
|
|
debugInfo = getDebugInfo(index);
|
|
}
|
|
parserInput.save();
|
|
if (parserInput.$peekChar('@')) {
|
|
if (parserInput.$str('@media')) {
|
|
return this.prepareAndGetNestableAtRule(tree_1.default.Media, index, debugInfo, atrule_syntax_1.MediaSyntaxOptions);
|
|
}
|
|
if (parserInput.$str('@container')) {
|
|
return this.prepareAndGetNestableAtRule(tree_1.default.Container, index, debugInfo, atrule_syntax_1.ContainerSyntaxOptions);
|
|
}
|
|
}
|
|
parserInput.restore();
|
|
},
|
|
//
|
|
// A @plugin directive, used to import plugins dynamically.
|
|
//
|
|
// @plugin (args) "lib";
|
|
//
|
|
plugin: function () {
|
|
var path;
|
|
var args;
|
|
var options;
|
|
var index = parserInput.i;
|
|
var dir = parserInput.$re(/^@plugin\s+/);
|
|
if (dir) {
|
|
args = this.pluginArgs();
|
|
if (args) {
|
|
options = {
|
|
pluginArgs: args,
|
|
isPlugin: true
|
|
};
|
|
}
|
|
else {
|
|
options = { isPlugin: true };
|
|
}
|
|
if ((path = this.entities.quoted() || this.entities.url())) {
|
|
if (!parserInput.$char(';')) {
|
|
parserInput.i = index;
|
|
error('missing semi-colon on @plugin');
|
|
}
|
|
return new (tree_1.default.Import)(path, null, options, index + currentIndex, fileInfo);
|
|
}
|
|
else {
|
|
parserInput.i = index;
|
|
error('malformed @plugin statement');
|
|
}
|
|
}
|
|
},
|
|
pluginArgs: function () {
|
|
// list of options, surrounded by parens
|
|
parserInput.save();
|
|
if (!parserInput.$char('(')) {
|
|
parserInput.restore();
|
|
return null;
|
|
}
|
|
var args = parserInput.$re(/^\s*([^);]+)\)\s*/);
|
|
if (args[1]) {
|
|
parserInput.forget();
|
|
return args[1].trim();
|
|
}
|
|
else {
|
|
parserInput.restore();
|
|
return null;
|
|
}
|
|
},
|
|
atruleUnknown: function (value, name, hasBlock) {
|
|
value = this.permissiveValue(/^[{;]/);
|
|
hasBlock = (parserInput.currentChar() === '{');
|
|
if (!value) {
|
|
if (!hasBlock && parserInput.currentChar() !== ';') {
|
|
error(''.concat(name, ' rule is missing block or ending semi-colon'));
|
|
}
|
|
}
|
|
else if (!value.value) {
|
|
value = null;
|
|
}
|
|
return [value, hasBlock];
|
|
},
|
|
atruleBlock: function (rules, value, isRooted, isKeywordList) {
|
|
rules = this.blockRuleset();
|
|
parserInput.save();
|
|
if (!rules && !isRooted) {
|
|
value = this.entity();
|
|
rules = this.blockRuleset();
|
|
}
|
|
if (!rules && !isRooted) {
|
|
parserInput.restore();
|
|
var e = [];
|
|
value = this.entity();
|
|
while (parserInput.$char(',')) {
|
|
e.push(value);
|
|
value = this.entity();
|
|
}
|
|
if (value && e.length > 0) {
|
|
e.push(value);
|
|
value = e;
|
|
isKeywordList = true;
|
|
}
|
|
else {
|
|
rules = this.blockRuleset();
|
|
}
|
|
}
|
|
else {
|
|
parserInput.forget();
|
|
}
|
|
return [rules, value, isKeywordList];
|
|
},
|
|
//
|
|
// A CSS AtRule
|
|
//
|
|
// @charset "utf-8";
|
|
//
|
|
atrule: function () {
|
|
var index = parserInput.i;
|
|
var name;
|
|
var value;
|
|
var rules;
|
|
var nonVendorSpecificName;
|
|
var hasIdentifier;
|
|
var hasExpression;
|
|
var hasUnknown;
|
|
var hasBlock = true;
|
|
var isRooted = true;
|
|
var isKeywordList = false;
|
|
if (parserInput.currentChar() !== '@') {
|
|
return;
|
|
}
|
|
value = this['import']() || this.plugin() || this.nestableAtRule();
|
|
if (value) {
|
|
return value;
|
|
}
|
|
parserInput.save();
|
|
name = parserInput.$re(/^@[a-z-]+/);
|
|
if (!name) {
|
|
return;
|
|
}
|
|
nonVendorSpecificName = name;
|
|
if (name.charAt(1) == '-' && name.indexOf('-', 2) > 0) {
|
|
nonVendorSpecificName = "@".concat(name.slice(name.indexOf('-', 2) + 1));
|
|
}
|
|
switch (nonVendorSpecificName) {
|
|
case '@charset':
|
|
hasIdentifier = true;
|
|
hasBlock = false;
|
|
break;
|
|
case '@namespace':
|
|
hasExpression = true;
|
|
hasBlock = false;
|
|
break;
|
|
case '@keyframes':
|
|
case '@counter-style':
|
|
hasIdentifier = true;
|
|
break;
|
|
case '@document':
|
|
case '@supports':
|
|
hasUnknown = true;
|
|
isRooted = false;
|
|
break;
|
|
case '@starting-style':
|
|
isRooted = false;
|
|
break;
|
|
case '@layer':
|
|
isRooted = false;
|
|
break;
|
|
default:
|
|
hasUnknown = true;
|
|
break;
|
|
}
|
|
parserInput.commentStore.length = 0;
|
|
if (hasIdentifier) {
|
|
value = this.entity();
|
|
if (!value) {
|
|
error("expected ".concat(name, " identifier"));
|
|
}
|
|
}
|
|
else if (hasExpression) {
|
|
value = this.expression();
|
|
if (!value) {
|
|
error("expected ".concat(name, " expression"));
|
|
}
|
|
}
|
|
else if (hasUnknown) {
|
|
var unknownPackage = this.atruleUnknown(value, name, hasBlock);
|
|
value = unknownPackage[0];
|
|
hasBlock = unknownPackage[1];
|
|
}
|
|
if (hasBlock) {
|
|
var blockPackage = this.atruleBlock(rules, value, isRooted, isKeywordList);
|
|
rules = blockPackage[0];
|
|
value = blockPackage[1];
|
|
isKeywordList = blockPackage[2];
|
|
if (!rules && !hasUnknown) {
|
|
parserInput.restore();
|
|
name = parserInput.$re(/^@[a-z-]+/);
|
|
var unknownPackage = this.atruleUnknown(value, name, hasBlock);
|
|
value = unknownPackage[0];
|
|
hasBlock = unknownPackage[1];
|
|
if (hasBlock) {
|
|
blockPackage = this.atruleBlock(rules, value, isRooted, isKeywordList);
|
|
rules = blockPackage[0];
|
|
value = blockPackage[1];
|
|
isKeywordList = blockPackage[2];
|
|
}
|
|
}
|
|
}
|
|
if (rules || isKeywordList || (!hasBlock && value && parserInput.$char(';'))) {
|
|
parserInput.forget();
|
|
return new (tree_1.default.AtRule)(name, value, rules, index + currentIndex, fileInfo, context.dumpLineNumbers ? getDebugInfo(index) : null, isRooted);
|
|
}
|
|
parserInput.restore('at-rule options not recognised');
|
|
},
|
|
//
|
|
// A Value is a comma-delimited list of Expressions
|
|
//
|
|
// font-family: Baskerville, Georgia, serif;
|
|
//
|
|
// In a Rule, a Value represents everything after the `:`,
|
|
// and before the `;`.
|
|
//
|
|
value: function () {
|
|
var e;
|
|
var expressions = [];
|
|
var index = parserInput.i;
|
|
do {
|
|
e = this.expression();
|
|
if (e) {
|
|
expressions.push(e);
|
|
if (!parserInput.$char(',')) {
|
|
break;
|
|
}
|
|
}
|
|
} while (e);
|
|
if (expressions.length > 0) {
|
|
return new (tree_1.default.Value)(expressions, index + currentIndex);
|
|
}
|
|
},
|
|
important: function () {
|
|
if (parserInput.currentChar() === '!') {
|
|
return parserInput.$re(/^! *important/);
|
|
}
|
|
},
|
|
sub: function () {
|
|
var a;
|
|
var e;
|
|
parserInput.save();
|
|
if (parserInput.$char('(')) {
|
|
a = this.addition();
|
|
if (a && parserInput.$char(')')) {
|
|
parserInput.forget();
|
|
e = new (tree_1.default.Expression)([a]);
|
|
e.parens = true;
|
|
return e;
|
|
}
|
|
parserInput.restore('Expected \')\'');
|
|
return;
|
|
}
|
|
parserInput.restore();
|
|
},
|
|
colorOperand: function () {
|
|
parserInput.save();
|
|
// hsl or rgb or lch operand
|
|
var match = parserInput.$re(/^[lchrgbs]\s+/);
|
|
if (match) {
|
|
return new tree_1.default.Keyword(match[0]);
|
|
}
|
|
parserInput.restore();
|
|
},
|
|
multiplication: function () {
|
|
var m;
|
|
var a;
|
|
var op;
|
|
var operation;
|
|
var isSpaced;
|
|
m = this.operand();
|
|
if (m) {
|
|
isSpaced = parserInput.isWhitespace(-1);
|
|
while (true) {
|
|
if (parserInput.peek(/^\/[*/]/)) {
|
|
break;
|
|
}
|
|
parserInput.save();
|
|
op = parserInput.$char('/') || parserInput.$char('*');
|
|
if (!op) {
|
|
var index = parserInput.i;
|
|
op = parserInput.$str('./');
|
|
if (op) {
|
|
warn('./ operator is deprecated', index, 'DEPRECATED');
|
|
}
|
|
}
|
|
if (!op) {
|
|
parserInput.forget();
|
|
break;
|
|
}
|
|
a = this.operand();
|
|
if (!a) {
|
|
parserInput.restore();
|
|
break;
|
|
}
|
|
parserInput.forget();
|
|
m.parensInOp = true;
|
|
a.parensInOp = true;
|
|
operation = new (tree_1.default.Operation)(op, [operation || m, a], isSpaced);
|
|
isSpaced = parserInput.isWhitespace(-1);
|
|
}
|
|
return operation || m;
|
|
}
|
|
},
|
|
addition: function () {
|
|
var m;
|
|
var a;
|
|
var op;
|
|
var operation;
|
|
var isSpaced;
|
|
m = this.multiplication();
|
|
if (m) {
|
|
isSpaced = parserInput.isWhitespace(-1);
|
|
while (true) {
|
|
op = parserInput.$re(/^[-+]\s+/) || (!isSpaced && (parserInput.$char('+') || parserInput.$char('-')));
|
|
if (!op) {
|
|
break;
|
|
}
|
|
a = this.multiplication();
|
|
if (!a) {
|
|
break;
|
|
}
|
|
m.parensInOp = true;
|
|
a.parensInOp = true;
|
|
operation = new (tree_1.default.Operation)(op, [operation || m, a], isSpaced);
|
|
isSpaced = parserInput.isWhitespace(-1);
|
|
}
|
|
return operation || m;
|
|
}
|
|
},
|
|
conditions: function () {
|
|
var a;
|
|
var b;
|
|
var index = parserInput.i;
|
|
var condition;
|
|
a = this.condition(true);
|
|
if (a) {
|
|
while (true) {
|
|
if (!parserInput.peek(/^,\s*(not\s*)?\(/) || !parserInput.$char(',')) {
|
|
break;
|
|
}
|
|
b = this.condition(true);
|
|
if (!b) {
|
|
break;
|
|
}
|
|
condition = new (tree_1.default.Condition)('or', condition || a, b, index + currentIndex);
|
|
}
|
|
return condition || a;
|
|
}
|
|
},
|
|
condition: function (needsParens) {
|
|
var result;
|
|
var logical;
|
|
var next;
|
|
function or() {
|
|
return parserInput.$str('or');
|
|
}
|
|
result = this.conditionAnd(needsParens);
|
|
if (!result) {
|
|
return;
|
|
}
|
|
logical = or();
|
|
if (logical) {
|
|
next = this.condition(needsParens);
|
|
if (next) {
|
|
result = new (tree_1.default.Condition)(logical, result, next);
|
|
}
|
|
else {
|
|
return;
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
conditionAnd: function (needsParens) {
|
|
var result;
|
|
var logical;
|
|
var next;
|
|
var self = this;
|
|
function insideCondition() {
|
|
var cond = self.negatedCondition(needsParens) || self.parenthesisCondition(needsParens);
|
|
if (!cond && !needsParens) {
|
|
return self.atomicCondition(needsParens);
|
|
}
|
|
return cond;
|
|
}
|
|
function and() {
|
|
return parserInput.$str('and');
|
|
}
|
|
result = insideCondition();
|
|
if (!result) {
|
|
return;
|
|
}
|
|
logical = and();
|
|
if (logical) {
|
|
next = this.conditionAnd(needsParens);
|
|
if (next) {
|
|
result = new (tree_1.default.Condition)(logical, result, next);
|
|
}
|
|
else {
|
|
return;
|
|
}
|
|
}
|
|
return result;
|
|
},
|
|
negatedCondition: function (needsParens) {
|
|
if (parserInput.$str('not')) {
|
|
var result = this.parenthesisCondition(needsParens);
|
|
if (result) {
|
|
result.negate = !result.negate;
|
|
}
|
|
return result;
|
|
}
|
|
},
|
|
parenthesisCondition: function (needsParens) {
|
|
function tryConditionFollowedByParenthesis(me) {
|
|
var body;
|
|
parserInput.save();
|
|
body = me.condition(needsParens);
|
|
if (!body) {
|
|
parserInput.restore();
|
|
return;
|
|
}
|
|
if (!parserInput.$char(')')) {
|
|
parserInput.restore();
|
|
return;
|
|
}
|
|
parserInput.forget();
|
|
return body;
|
|
}
|
|
var body;
|
|
parserInput.save();
|
|
if (!parserInput.$str('(')) {
|
|
parserInput.restore();
|
|
return;
|
|
}
|
|
body = tryConditionFollowedByParenthesis(this);
|
|
if (body) {
|
|
parserInput.forget();
|
|
return body;
|
|
}
|
|
body = this.atomicCondition(needsParens);
|
|
if (!body) {
|
|
parserInput.restore();
|
|
return;
|
|
}
|
|
if (!parserInput.$char(')')) {
|
|
parserInput.restore("expected ')' got '".concat(parserInput.currentChar(), "'"));
|
|
return;
|
|
}
|
|
parserInput.forget();
|
|
return body;
|
|
},
|
|
atomicCondition: function (needsParens, preparsedCond) {
|
|
var entities = this.entities;
|
|
var index = parserInput.i;
|
|
var a;
|
|
var b;
|
|
var c;
|
|
var op;
|
|
var cond = (function () {
|
|
return this.addition() || entities.keyword() || entities.quoted() || entities.mixinLookup();
|
|
}).bind(this);
|
|
if (preparsedCond) {
|
|
a = preparsedCond;
|
|
}
|
|
else {
|
|
a = cond();
|
|
}
|
|
if (a) {
|
|
if (parserInput.$char('>')) {
|
|
if (parserInput.$char('=')) {
|
|
op = '>=';
|
|
}
|
|
else {
|
|
op = '>';
|
|
}
|
|
}
|
|
else if (parserInput.$char('<')) {
|
|
if (parserInput.$char('=')) {
|
|
op = '<=';
|
|
}
|
|
else {
|
|
op = '<';
|
|
}
|
|
}
|
|
else if (parserInput.$char('=')) {
|
|
if (parserInput.$char('>')) {
|
|
op = '=>';
|
|
}
|
|
else if (parserInput.$char('<')) {
|
|
op = '=<';
|
|
}
|
|
else {
|
|
op = '=';
|
|
}
|
|
}
|
|
if (op) {
|
|
b = cond();
|
|
if (b) {
|
|
c = new (tree_1.default.Condition)(op, a, b, index + currentIndex, false);
|
|
}
|
|
else {
|
|
error('expected expression');
|
|
}
|
|
}
|
|
else if (!preparsedCond) {
|
|
c = new (tree_1.default.Condition)('=', a, new (tree_1.default.Keyword)('true'), index + currentIndex, false);
|
|
}
|
|
return c;
|
|
}
|
|
},
|
|
//
|
|
// An operand is anything that can be part of an operation,
|
|
// such as a Color, or a Variable
|
|
//
|
|
operand: function () {
|
|
var entities = this.entities;
|
|
var negate;
|
|
if (parserInput.peek(/^-[@$(]/)) {
|
|
negate = parserInput.$char('-');
|
|
}
|
|
var o = this.sub() || entities.dimension() ||
|
|
entities.color() || entities.variable() ||
|
|
entities.property() || entities.call() ||
|
|
entities.quoted(true) || entities.colorKeyword() ||
|
|
this.colorOperand() || entities.mixinLookup();
|
|
if (negate) {
|
|
o.parensInOp = true;
|
|
o = new (tree_1.default.Negative)(o);
|
|
}
|
|
return o;
|
|
},
|
|
//
|
|
// Expressions either represent mathematical operations,
|
|
// or white-space delimited Entities.
|
|
//
|
|
// 1px solid black
|
|
// @var * 2
|
|
//
|
|
expression: function () {
|
|
var entities = [];
|
|
var e;
|
|
var delim;
|
|
var index = parserInput.i;
|
|
do {
|
|
e = this.comment();
|
|
if (e && !e.isLineComment) {
|
|
entities.push(e);
|
|
continue;
|
|
}
|
|
e = this.addition() || this.entity();
|
|
if (e instanceof tree_1.default.Comment) {
|
|
e = null;
|
|
}
|
|
if (e) {
|
|
entities.push(e);
|
|
// operations do not allow keyword "/" dimension (e.g. small/20px) so we support that here
|
|
if (!parserInput.peek(/^\/[/*]/)) {
|
|
delim = parserInput.$char('/');
|
|
if (delim) {
|
|
entities.push(new (tree_1.default.Anonymous)(delim, index + currentIndex));
|
|
}
|
|
}
|
|
}
|
|
} while (e);
|
|
if (entities.length > 0) {
|
|
return new (tree_1.default.Expression)(entities);
|
|
}
|
|
},
|
|
property: function () {
|
|
var name = parserInput.$re(/^(\*?-?[_a-zA-Z0-9-]+)\s*:/);
|
|
if (name) {
|
|
return name[1];
|
|
}
|
|
},
|
|
ruleProperty: function () {
|
|
var name = [];
|
|
var index = [];
|
|
var s;
|
|
var k;
|
|
parserInput.save();
|
|
var simpleProperty = parserInput.$re(/^([_a-zA-Z0-9-]+)\s*:/);
|
|
if (simpleProperty) {
|
|
name = [new (tree_1.default.Keyword)(simpleProperty[1])];
|
|
parserInput.forget();
|
|
return name;
|
|
}
|
|
function match(re) {
|
|
var i = parserInput.i;
|
|
var chunk = parserInput.$re(re);
|
|
if (chunk) {
|
|
index.push(i);
|
|
return name.push(chunk[1]);
|
|
}
|
|
}
|
|
match(/^(\*?)/);
|
|
while (true) {
|
|
if (!match(/^((?:[\w-]+)|(?:[@$]\{[\w-]+\}))/)) {
|
|
break;
|
|
}
|
|
}
|
|
if ((name.length > 1) && match(/^((?:\+_|\+)?)\s*:/)) {
|
|
parserInput.forget();
|
|
// at last, we have the complete match now. move forward,
|
|
// convert name particles to tree objects and return:
|
|
if (name[0] === '') {
|
|
name.shift();
|
|
index.shift();
|
|
}
|
|
for (k = 0; k < name.length; k++) {
|
|
s = name[k];
|
|
name[k] = (s.charAt(0) !== '@' && s.charAt(0) !== '$') ?
|
|
new (tree_1.default.Keyword)(s) :
|
|
(s.charAt(0) === '@' ?
|
|
new (tree_1.default.Variable)("@".concat(s.slice(2, -1)), index[k] + currentIndex, fileInfo) :
|
|
new (tree_1.default.Property)("$".concat(s.slice(2, -1)), index[k] + currentIndex, fileInfo));
|
|
}
|
|
return name;
|
|
}
|
|
parserInput.restore();
|
|
}
|
|
}
|
|
};
|
|
};
|
|
Parser.serializeVars = function (vars) {
|
|
var s = '';
|
|
for (var name_1 in vars) {
|
|
if (Object.hasOwnProperty.call(vars, name_1)) {
|
|
var value = vars[name_1];
|
|
s += "".concat(((name_1[0] === '@') ? '' : '@') + name_1, ": ").concat(value).concat((String(value).slice(-1) === ';') ? '' : ';');
|
|
}
|
|
}
|
|
return s;
|
|
};
|
|
exports.default = Parser;
|
|
//# sourceMappingURL=parser.js.map
|