888 lines
18 KiB
JavaScript
888 lines
18 KiB
JavaScript
(function (exports) {
|
||
'use strict';
|
||
|
||
/**
|
||
* A streaming, character code-based string reader
|
||
*/
|
||
class StreamReader {
|
||
constructor(string, start, end) {
|
||
if (end == null && typeof string === 'string') {
|
||
end = string.length;
|
||
}
|
||
|
||
this.string = string;
|
||
this.pos = this.start = start || 0;
|
||
this.end = end;
|
||
}
|
||
|
||
/**
|
||
* Returns true only if the stream is at the end of the file.
|
||
* @returns {Boolean}
|
||
*/
|
||
eof() {
|
||
return this.pos >= this.end;
|
||
}
|
||
|
||
/**
|
||
* Creates a new stream instance which is limited to given `start` and `end`
|
||
* range. E.g. its `eof()` method will look at `end` property, not actual
|
||
* stream end
|
||
* @param {Point} start
|
||
* @param {Point} end
|
||
* @return {StreamReader}
|
||
*/
|
||
limit(start, end) {
|
||
return new this.constructor(this.string, start, end);
|
||
}
|
||
|
||
/**
|
||
* Returns the next character code in the stream without advancing it.
|
||
* Will return NaN at the end of the file.
|
||
* @returns {Number}
|
||
*/
|
||
peek() {
|
||
return this.string.charCodeAt(this.pos);
|
||
}
|
||
|
||
/**
|
||
* Returns the next character in the stream and advances it.
|
||
* Also returns <code>undefined</code> when no more characters are available.
|
||
* @returns {Number}
|
||
*/
|
||
next() {
|
||
if (this.pos < this.string.length) {
|
||
return this.string.charCodeAt(this.pos++);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* `match` can be a character code or a function that takes a character code
|
||
* and returns a boolean. If the next character in the stream 'matches'
|
||
* the given argument, it is consumed and returned.
|
||
* Otherwise, `false` is returned.
|
||
* @param {Number|Function} match
|
||
* @returns {Boolean}
|
||
*/
|
||
eat(match) {
|
||
const ch = this.peek();
|
||
const ok = typeof match === 'function' ? match(ch) : ch === match;
|
||
|
||
if (ok) {
|
||
this.next();
|
||
}
|
||
|
||
return ok;
|
||
}
|
||
|
||
/**
|
||
* Repeatedly calls <code>eat</code> with the given argument, until it
|
||
* fails. Returns <code>true</code> if any characters were eaten.
|
||
* @param {Object} match
|
||
* @returns {Boolean}
|
||
*/
|
||
eatWhile(match) {
|
||
const start = this.pos;
|
||
while (!this.eof() && this.eat(match)) {}
|
||
return this.pos !== start;
|
||
}
|
||
|
||
/**
|
||
* Backs up the stream n characters. Backing it up further than the
|
||
* start of the current token will cause things to break, so be careful.
|
||
* @param {Number} n
|
||
*/
|
||
backUp(n) {
|
||
this.pos -= (n || 1);
|
||
}
|
||
|
||
/**
|
||
* Get the string between the start of the current token and the
|
||
* current stream position.
|
||
* @returns {String}
|
||
*/
|
||
current() {
|
||
return this.substring(this.start, this.pos);
|
||
}
|
||
|
||
/**
|
||
* Returns substring for given range
|
||
* @param {Number} start
|
||
* @param {Number} [end]
|
||
* @return {String}
|
||
*/
|
||
substring(start, end) {
|
||
return this.string.slice(start, end);
|
||
}
|
||
|
||
/**
|
||
* Creates error object with current stream state
|
||
* @param {String} message
|
||
* @return {Error}
|
||
*/
|
||
error(message) {
|
||
const err = new Error(`${message} at char ${this.pos + 1}`);
|
||
err.originalMessage = message;
|
||
err.pos = this.pos;
|
||
err.string = this.string;
|
||
return err;
|
||
}
|
||
}
|
||
|
||
class Container {
|
||
constructor() {
|
||
this.children = [];
|
||
this.parent = null;
|
||
}
|
||
|
||
get firstChild() {
|
||
return this.children[0];
|
||
}
|
||
|
||
get nextSibling() {
|
||
const ix = this.index();
|
||
return ix !== -1 ? this.parent.children[ix + 1] : null;
|
||
}
|
||
|
||
get previousSibling() {
|
||
const ix = this.index();
|
||
return ix !== -1 ? this.parent.children[ix - 1] : null;
|
||
}
|
||
|
||
/**
|
||
* Returns current element’s index in parent list of child nodes
|
||
* @return {Number}
|
||
*/
|
||
index() {
|
||
return this.parent ? this.parent.children.indexOf(this) : -1;
|
||
}
|
||
|
||
/**
|
||
* Adds given node as a child
|
||
* @param {Node} node
|
||
* @return {Node} Current node
|
||
*/
|
||
add(node) {
|
||
if (node) {
|
||
node.remove();
|
||
this.children.push(node);
|
||
node.parent = this;
|
||
}
|
||
return this;
|
||
}
|
||
|
||
/**
|
||
* Removes current node from its parent
|
||
* @return {Node} Current node
|
||
*/
|
||
remove() {
|
||
if (this.parent) {
|
||
const ix = this.index();
|
||
if (ix !== -1) {
|
||
this.parent.children.splice(ix, 1);
|
||
this.parent = null;
|
||
}
|
||
}
|
||
|
||
return this;
|
||
}
|
||
}
|
||
|
||
class Stylesheet extends Container {
|
||
constructor() {
|
||
super();
|
||
this.type = 'stylesheet';
|
||
}
|
||
|
||
/**
|
||
* Returns node’s start position in stream
|
||
* @return {*}
|
||
*/
|
||
get start() {
|
||
const node = this.firstChild;
|
||
return node && node.start;
|
||
}
|
||
|
||
/**
|
||
* Returns node’s end position in stream
|
||
* @return {*}
|
||
*/
|
||
get end() {
|
||
const node = this.children[this.children.length - 1];
|
||
return node && node.end;
|
||
}
|
||
}
|
||
|
||
class Token {
|
||
/**
|
||
* @param {StreamReader} stream
|
||
* @param {String} type Token type
|
||
* @param {Object} [start] Tokens’ start position in `stream`
|
||
* @param {Object} [end] Tokens’ end position in `stream`
|
||
*/
|
||
constructor(stream, type, start, end) {
|
||
this.stream = stream;
|
||
this.start = start != null ? start : stream.start;
|
||
this.end = end != null ? end : stream.pos;
|
||
this.type = type;
|
||
|
||
this._props = null;
|
||
this._value = null;
|
||
this._items = null;
|
||
}
|
||
|
||
get size() {
|
||
return this._items ? this._items.length : 0;
|
||
}
|
||
|
||
add(item) {
|
||
if (item) {
|
||
if (!this._items) {
|
||
this._items = [item];
|
||
} else {
|
||
this._items.push(item);
|
||
}
|
||
}
|
||
|
||
return this;
|
||
}
|
||
|
||
remove(item) {
|
||
if (this._items) {
|
||
const ix = this._items.indexOf(item);
|
||
if (ix !== -1 ) {
|
||
this._items.splice(ix, 1);
|
||
}
|
||
}
|
||
|
||
return this;
|
||
}
|
||
|
||
item(i) {
|
||
return this._items && this._items[i];
|
||
}
|
||
|
||
property(name, value) {
|
||
if (typeof value !== 'undefined') {
|
||
// set property value
|
||
if (!this._props) {
|
||
this._props = {};
|
||
}
|
||
|
||
this._props[name] = value;
|
||
}
|
||
|
||
return this._props && this._props[name];
|
||
}
|
||
|
||
/**
|
||
* Returns token textual representation
|
||
* @return {String}
|
||
*/
|
||
toString() {
|
||
return `${this.valueOf()} [${this.start}, ${this.end}]`;
|
||
}
|
||
|
||
valueOf() {
|
||
if (this._value === null) {
|
||
this._value = this.stream.substring(this.start, this.end);
|
||
}
|
||
|
||
return this._value;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Removes tokens that matches given criteria from start and end of given list
|
||
* @param {Token[]} tokens
|
||
* @param {Function} test
|
||
* @return {Token[]}
|
||
*/
|
||
|
||
|
||
/**
|
||
* Trims formatting tokens (whitespace and comments) from the beginning and end
|
||
* of given token list
|
||
* @param {Token[]} tokens
|
||
* @return {Token[]}
|
||
*/
|
||
|
||
|
||
/**
|
||
* Check if given token is a formatting one (whitespace or comment)
|
||
* @param {Token} token
|
||
* @return {Boolean}
|
||
*/
|
||
|
||
|
||
/**
|
||
* Consumes string char-by-char from given stream
|
||
* @param {StreamReader} stream
|
||
* @param {String} string
|
||
* @return {Boolean} Returns `true` if string was completely consumed
|
||
*/
|
||
function eatString(stream, string) {
|
||
const start = stream.pos;
|
||
|
||
for (let i = 0, il = string.length; i < il; i++) {
|
||
if (!stream.eat(string.charCodeAt(i))) {
|
||
stream.pos = start;
|
||
return false;
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
|
||
|
||
function consumeWhile(stream, match) {
|
||
const start = stream.pos;
|
||
if (stream.eatWhile(match)) {
|
||
stream.start = start;
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
|
||
|
||
/**
|
||
* Returns type of given token
|
||
* @param {Token} token
|
||
* @return {String}
|
||
*/
|
||
|
||
|
||
function last(arr) {
|
||
return arr[arr.length - 1];
|
||
}
|
||
|
||
function createRule(stream, tokens, content) {
|
||
if (!tokens.length) {
|
||
return null;
|
||
}
|
||
|
||
let ix = 0;
|
||
const name = tokens[ix++];
|
||
|
||
if (name.type === 'at-keyword') {
|
||
let expression;
|
||
if (ix < tokens.length) {
|
||
expression = tokens[ix++];
|
||
expression.type = 'expression';
|
||
expression.end = last(tokens).end;
|
||
} else {
|
||
expression = new Token(stream, 'expression', name.end, name.end);
|
||
}
|
||
|
||
return new AtRule(stream, name, expression, content);
|
||
} else {
|
||
name.end = last(tokens).end;
|
||
}
|
||
|
||
return new Rule(stream, name, content);
|
||
}
|
||
|
||
/**
|
||
* Represents CSS rule
|
||
* @type {Node}
|
||
*/
|
||
class Rule extends Container {
|
||
/**
|
||
* @param {StreamReader} stream
|
||
* @param {Token} name Rule’s name token
|
||
* @param {Token} content Rule’s content token
|
||
*/
|
||
constructor(stream, name, content) {
|
||
super();
|
||
this.type = 'rule';
|
||
this.stream = stream;
|
||
this.nameToken = name;
|
||
this.contentToken = content;
|
||
}
|
||
|
||
/**
|
||
* Returns node name
|
||
* @return {String}
|
||
*/
|
||
get name() {
|
||
return valueOf(this.nameToken);
|
||
}
|
||
|
||
/**
|
||
* Returns node’s start position in stream
|
||
* @return {*}
|
||
*/
|
||
get start() {
|
||
return this.nameToken && this.nameToken.start;
|
||
}
|
||
|
||
/**
|
||
* Returns node’s end position in stream
|
||
* @return {*}
|
||
*/
|
||
get end() {
|
||
const token = this.contentToken || this.nameToken;
|
||
return token && token.end;
|
||
}
|
||
}
|
||
|
||
class AtRule extends Rule {
|
||
constructor(stream, name, expression, content) {
|
||
super(stream, name, content);
|
||
this.type = 'at-rule';
|
||
this.expressionToken = expression;
|
||
}
|
||
|
||
get expressions() {
|
||
return valueOf(this.expressionToken);
|
||
}
|
||
}
|
||
|
||
function valueOf(token) {
|
||
return token && token.valueOf();
|
||
}
|
||
|
||
function createProperty(stream, tokens, terminator) {
|
||
// NB in LESS, fragmented properties without value like `.foo.bar;` must be
|
||
// treated like mixin call
|
||
if (!tokens.length) {
|
||
return null;
|
||
}
|
||
|
||
let separator, value, ix = 0;
|
||
const name = tokens[ix++];
|
||
|
||
if (ix < tokens.length) {
|
||
value = tokens[ix++];
|
||
value.type = 'value';
|
||
value.end = last(tokens).end;
|
||
}
|
||
|
||
if (name && value) {
|
||
separator = new Token(stream, 'separator', name.end, value.start);
|
||
}
|
||
|
||
return new Property(
|
||
stream,
|
||
name,
|
||
value,
|
||
separator,
|
||
terminator
|
||
);
|
||
}
|
||
|
||
class Property {
|
||
constructor(stream, name, value, separator, terminator) {
|
||
this.type = 'property';
|
||
this.stream = stream;
|
||
this.nameToken = name;
|
||
this.valueToken = value;
|
||
|
||
this.separatorToken = separator;
|
||
this.terminatorToken = terminator;
|
||
}
|
||
|
||
get name() {
|
||
return valueOf$1(this.nameToken);
|
||
}
|
||
|
||
get value() {
|
||
return valueOf$1(this.valueToken);
|
||
}
|
||
|
||
get separator() {
|
||
return valueOf$1(this.separatorToken);
|
||
}
|
||
|
||
get terminator() {
|
||
return valueOf$1(this.terminatorToken);
|
||
}
|
||
|
||
get start() {
|
||
const token = this.nameToken || this.separatorToken || this.valueToken
|
||
|| this.terminatorToken;
|
||
return token && token.start;
|
||
}
|
||
|
||
get end() {
|
||
const token = this.terminatorToken || this.valueToken
|
||
|| this.separatorToken || this.nameToken;
|
||
return token && token.end;
|
||
}
|
||
}
|
||
|
||
function valueOf$1(token) {
|
||
return token && token.valueOf();
|
||
}
|
||
|
||
/**
|
||
* Methods for consuming quoted values
|
||
*/
|
||
|
||
const SINGLE_QUOTE = 39; // '
|
||
const DOUBLE_QUOTE = 34; // "
|
||
|
||
function isQuote(code) {
|
||
return code === SINGLE_QUOTE || code === DOUBLE_QUOTE;
|
||
}
|
||
|
||
/**
|
||
* Check if given code is a number
|
||
* @param {Number} code
|
||
* @return {Boolean}
|
||
*/
|
||
function isNumber(code) {
|
||
return code > 47 && code < 58;
|
||
}
|
||
|
||
/**
|
||
* Check if given character code is alpha code (letter through A to Z)
|
||
* @param {Number} code
|
||
* @param {Number} [from]
|
||
* @param {Number} [to]
|
||
* @return {Boolean}
|
||
*/
|
||
function isAlpha(code, from, to) {
|
||
from = from || 65; // A
|
||
to = to || 90; // Z
|
||
code &= ~32; // quick hack to convert any char code to uppercase char code
|
||
|
||
return code >= from && code <= to;
|
||
}
|
||
|
||
/**
|
||
* Check if given character code is alpha-numeric (letter through A to Z or number)
|
||
* @param {Number} code
|
||
* @return {Boolean}
|
||
*/
|
||
function isWhiteSpace(code) {
|
||
return code === 32 /* space */
|
||
|| code === 9 /* tab */
|
||
|| code === 160; /* non-breaking space */
|
||
}
|
||
|
||
/**
|
||
* Check if given character code is a space
|
||
* @param {Number} code
|
||
* @return {Boolean}
|
||
*/
|
||
function isSpace(code) {
|
||
return isWhiteSpace(code)
|
||
|| code === 10 /* LF */
|
||
|| code === 13; /* CR */
|
||
}
|
||
|
||
const HYPHEN = 45;
|
||
const UNDERSCORE = 95;
|
||
|
||
function ident(stream) {
|
||
return eatIdent(stream) && new Token(stream, 'ident');
|
||
}
|
||
|
||
function eatIdent(stream) {
|
||
const start = stream.pos;
|
||
|
||
stream.eat(HYPHEN);
|
||
if (stream.eat(isIdentStart)) {
|
||
stream.eatWhile(isIdent);
|
||
stream.start = start;
|
||
return true;
|
||
}
|
||
|
||
stream.pos = start;
|
||
return false;
|
||
}
|
||
|
||
function isIdentStart(code) {
|
||
return code === UNDERSCORE || code === HYPHEN || isAlpha(code) || code >= 128;
|
||
}
|
||
|
||
function isIdent(code) {
|
||
return isNumber(code) || isIdentStart(code);
|
||
}
|
||
|
||
function prefixed(stream, tokenType, prefix, body, allowEmptyBody) {
|
||
const start = stream.pos;
|
||
|
||
if (stream.eat(prefix)) {
|
||
const bodyToken = body(stream, start);
|
||
if (bodyToken || allowEmptyBody) {
|
||
stream.start = start;
|
||
return new Token(stream, tokenType, start).add(bodyToken);
|
||
}
|
||
}
|
||
|
||
stream.pos = start;
|
||
}
|
||
|
||
const AT = 64; // @
|
||
|
||
/**
|
||
* Consumes at-keyword from given stream
|
||
*/
|
||
function atKeyword(stream) {
|
||
return prefixed(stream, 'at-keyword', AT, ident);
|
||
}
|
||
|
||
function eatString$1(stream, asToken) {
|
||
let ch = stream.peek(), pos;
|
||
|
||
if (isQuote(ch)) {
|
||
stream.start = stream.pos;
|
||
stream.next();
|
||
const quote = ch;
|
||
const valueStart = stream.pos;
|
||
|
||
while (!stream.eof()) {
|
||
pos = stream.pos;
|
||
if (stream.eat(quote) || stream.eat(isNewline)) {
|
||
// found end of string or newline without preceding '\',
|
||
// which is not allowed (don’t throw error, for now)
|
||
break;
|
||
} else if (stream.eat(92 /* \ */)) {
|
||
// backslash allows newline in string
|
||
stream.eat(isNewline);
|
||
}
|
||
|
||
stream.next();
|
||
}
|
||
|
||
// Either reached EOF or explicitly stopped at string end
|
||
// NB use extra `asToken` param to return boolean instead of token to reduce
|
||
// memory allocations and improve performance
|
||
if (asToken) {
|
||
const token = new Token(stream, 'string');
|
||
token.add(new Token(stream, 'unquoted', valueStart, pos));
|
||
token.property('quote', quote);
|
||
return token;
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
function isNewline(code) {
|
||
return code === 10 /* LF */ || code === 13 /* CR */;
|
||
}
|
||
|
||
const ASTERISK = 42;
|
||
const SLASH = 47;
|
||
|
||
/**
|
||
* Consumes comment from given stream: either multi-line or single-line
|
||
* @param {StreamReader} stream
|
||
* @return {CommentToken}
|
||
*/
|
||
|
||
|
||
|
||
|
||
function eatComment(stream) {
|
||
return eatSingleLineComment(stream) || eatMultiLineComment(stream);
|
||
}
|
||
|
||
function eatSingleLineComment(stream) {
|
||
const start = stream.pos;
|
||
|
||
if (stream.eat(SLASH) && stream.eat(SLASH)) {
|
||
// single-line comment, consume till the end of line
|
||
stream.start = start;
|
||
while (!stream.eof()) {
|
||
if (isLineBreak(stream.next())) {
|
||
break;
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
stream.pos = start;
|
||
return false;
|
||
}
|
||
|
||
function eatMultiLineComment(stream) {
|
||
const start = stream.pos;
|
||
|
||
if (stream.eat(SLASH) && stream.eat(ASTERISK)) {
|
||
while (!stream.eof()) {
|
||
if (stream.next() === ASTERISK && stream.eat(SLASH)) {
|
||
break;
|
||
}
|
||
}
|
||
|
||
stream.start = start;
|
||
return true;
|
||
}
|
||
|
||
stream.pos = start;
|
||
return false;
|
||
}
|
||
|
||
function isLineBreak(code) {
|
||
return code === 10 /* LF */ || code === 13 /* CR */;
|
||
}
|
||
|
||
function eatWhitespace(stream) {
|
||
return consumeWhile(stream, isSpace);
|
||
}
|
||
|
||
function eatUnquoted(stream) {
|
||
return consumeWhile(stream, isUnquoted);
|
||
}
|
||
|
||
function isUnquoted(code) {
|
||
return !isNaN(code) && !isQuote(code) && !isSpace(code)
|
||
&& code !== 40 /* ( */ && code !== 41 /* ) */ && code !== 92 /* \ */
|
||
&& !isNonPrintable(code);
|
||
}
|
||
|
||
function isNonPrintable(code) {
|
||
return (code >= 0 && code <= 8) || code === 11
|
||
|| (code >= 14 && code <= 31) || code === 127;
|
||
}
|
||
|
||
function eatUrl(stream) {
|
||
const start = stream.pos;
|
||
|
||
if (eatString(stream, 'url(')) {
|
||
eatWhitespace(stream);
|
||
eatString$1(stream) || eatUnquoted(stream);
|
||
eatWhitespace(stream);
|
||
stream.eat(41); // )
|
||
stream.start = start;
|
||
|
||
return true;
|
||
}
|
||
|
||
stream.pos = start;
|
||
return false;
|
||
}
|
||
|
||
const LBRACE = 40; // (
|
||
const RBRACE = 41; // )
|
||
const PROP_DELIMITER = 58; // :
|
||
const PROP_TERMINATOR = 59; // ;
|
||
const RULE_START = 123; // {
|
||
const RULE_END = 125; // }
|
||
|
||
function parseStylesheet(source) {
|
||
const stream = typeof source === 'string' ? new StreamReader(source) : source;
|
||
const root = new Stylesheet();
|
||
let ctx = root, child, accum, token;
|
||
let tokens = [];
|
||
const flush = () => {
|
||
if (accum) {
|
||
tokens.push(accum);
|
||
accum = null;
|
||
}
|
||
};
|
||
|
||
while (!stream.eof()) {
|
||
if (eatWhitespace(stream) || eatComment(stream)) {
|
||
continue;
|
||
}
|
||
|
||
stream.start = stream.pos;
|
||
|
||
if (stream.eatWhile(PROP_DELIMITER)) {
|
||
// Property delimiter can be either a real property delimiter or a
|
||
// part of pseudo-selector.
|
||
if (!tokens.length) {
|
||
if (accum) {
|
||
// No consumed tokens yet but pending token: most likely it’s
|
||
// a CSS property
|
||
flush();
|
||
} else {
|
||
// No consumend or accumulated token, seems like a start of
|
||
// pseudo-selector, e.g. `::slotted`
|
||
accum = new Token(stream, 'preparse');
|
||
}
|
||
}
|
||
// Skip delimiter if there are already consumend tokens: most likely
|
||
// it’s a part of pseudo-selector
|
||
} else if (stream.eat(RULE_END)) {
|
||
flush();
|
||
|
||
// Finalize context section
|
||
ctx.add(createProperty(stream, tokens));
|
||
|
||
if (ctx.type !== 'stylesheet') {
|
||
// In case of invalid stylesheet with redundant `}`,
|
||
// don’t modify root section.
|
||
ctx.contentToken.end = stream.pos;
|
||
ctx = ctx.parent;
|
||
}
|
||
|
||
tokens.length = 0;
|
||
} else if (stream.eat(PROP_TERMINATOR)) {
|
||
flush();
|
||
ctx.add(createProperty(stream, tokens, new Token(stream, 'termintator')));
|
||
tokens.length = 0;
|
||
} else if (stream.eat(RULE_START)) {
|
||
flush();
|
||
child = createRule(stream, tokens, new Token(stream, 'body'));
|
||
ctx.add(child);
|
||
ctx = child;
|
||
tokens.length = 0;
|
||
} else if (token = atKeyword(stream)) {
|
||
// Explictly consume @-tokens since it defines how rule or property
|
||
// should be pre-parsed
|
||
flush();
|
||
tokens.push(token);
|
||
} else if (eatUrl(stream) || eatBraces(stream) || eatString$1(stream) || stream.next()) {
|
||
// NB explicitly consume `url()` token since it may contain
|
||
// an unquoted url like `http://example.com` which interferes
|
||
// with single-line comment
|
||
accum = accum || new Token(stream, 'preparse');
|
||
accum.end = stream.pos;
|
||
} else {
|
||
throw new Error(`Unexpected end-of-stream at ${stream.pos}`);
|
||
}
|
||
}
|
||
|
||
if (accum) {
|
||
tokens.push(accum);
|
||
}
|
||
|
||
// Finalize all the rest properties
|
||
ctx.add(createProperty(stream, tokens));
|
||
|
||
return root;
|
||
}
|
||
|
||
/**
|
||
* Consumes content inside round braces. Mostly used to skip `;` token inside
|
||
* expressions since in LESS it is also used to separate function arguments
|
||
* @param {StringReader} stream
|
||
* @return {Boolean}
|
||
*/
|
||
function eatBraces(stream) {
|
||
if (stream.eat(LBRACE)) {
|
||
let stack = 1;
|
||
|
||
while (!stream.eof()) {
|
||
if (stream.eat(RBRACE)) {
|
||
stack--;
|
||
if (!stack) {
|
||
break;
|
||
}
|
||
} else if (stream.eat(LBRACE)) {
|
||
stack++;
|
||
} else {
|
||
eatUrl(stream) || eatString$1(stream) || eatComment(stream) || stream.next();
|
||
}
|
||
}
|
||
|
||
return true;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
|
||
exports['default'] = parseStylesheet;
|
||
|
||
}((this.cssParser = this.cssParser || {})));
|