(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 undefined 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 eat with the given argument, until it
* fails. Returns true 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 || {})));