(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 || {})));