'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var StreamReader = _interopDefault(require('@emmetio/stream-reader')); var _emmetio_streamReaderUtils = require('@emmetio/stream-reader-utils'); /** * Abstract container that contains nested nodes or other containers */ class Node { constructor(type) { this.type = type; 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 Node { constructor() { super('stylesheet'); this.comments = []; } /** * 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; } /** * Adds comment token into a list. * This somewhat awkward feature is required to properly detect comment * ranges. Specifically, in Atom: it’s API provides scopes limited to current * line only * @param {Token} token */ addComment(token) { this.comments.push(token); } } /** * Removes tokens that matches given criteria from start and end of given list * @param {Token[]} tokens * @return {Token[]} */ function trimTokens(tokens) { tokens = tokens.slice(); let len; while (len !== tokens.length) { len = tokens.length; if (isFormattingToken(tokens[0])) { tokens.shift(); } if (isFormattingToken(last(tokens))) { tokens.pop(); } } return tokens; } /** * Trims formatting tokens (whitespace and comments) from the beginning and end * of given token list * @param {Token[]} tokens * @return {Token[]} */ function trimFormatting(tokens) { return trimTokens(tokens, isFormattingToken); } /** * Check if given token is a formatting one (whitespace or comment) * @param {Token} token * @return {Boolean} */ function isFormattingToken(token) { const type = token && token.type; return type === 'whitespace' || type === 'comment'; } /** * 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 consume(stream, match) { const start = stream.pos; if (stream.eat(match)) { stream.start = start; return true; } return false; } function consumeWhile(stream, match) { const start = stream.pos; if (stream.eatWhile(match)) { stream.start = start; return true; } return false; } function last(arr) { return arr[arr.length - 1]; } function valueOf(token) { return token && token.valueOf(); } /** * A structure describing text fragment in content stream. It may contain * other sub-fragments (also tokens) that represent current fragments’ logical * parts */ 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; } get items() { return this._items; } clone(start, end) { return new this.constructor(this.stream, this.type, start != null ? start : this.start, end != null ? end : this.end); } add(item) { if (Array.isArray(item)) { for (let i = 0, il = item.length; i < il; i++) { this.add(item[i]); } } else 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]; } limit() { return this.stream.limit(this.start, this.end); } slice(from, to) { const token = this.clone(); const items = this._items && this._items.slice(from, to); if (items && items.length) { token.start = items[0].start; token.end = items[items.length - 1].end; token.add(items); } else if (items) { // Empty token token.start = token.end; } return token; } 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}] (${this.type})`; } valueOf() { if (this._value === null) { this._value = this.stream.substring(this.start, this.end); } return this._value; } } const COMMA = 44; // , const PROP_DELIMITER$1 = 58; // : const PROP_TERMINATOR$1 = 59; // ; const RULE_START$1 = 123; // { const RULE_END$1 = 125; // } const types = new Map() .set(COMMA, 'comma') .set(PROP_DELIMITER$1, 'propertyDelimiter') .set(PROP_TERMINATOR$1, 'propertyTerminator') .set(RULE_START$1, 'ruleStart') .set(RULE_END$1, 'ruleEnd'); /** * Consumes separator token from given string */ function separator(stream) { if (isSeparator(stream.peek())) { const start = stream.pos; const type = types.get(stream.next()); const token = new Token(stream, 'separator', start); token.property('type', type); return token; } } function isSeparator(code) { return code === COMMA || code === PROP_DELIMITER$1 || code === PROP_TERMINATOR$1 || code === RULE_START$1 || code === RULE_END$1; } const ARGUMENTS_START = 40; // ( const ARGUMENTS_END = 41; // ) var args = function(stream, tokenConsumer) { if (stream.peek() === ARGUMENTS_START) { const start = stream.pos; stream.next(); const tokens = []; let t; // in LESS, it’s possible to separate arguments list either by `;` or `,`. // In first case, we should keep comma-separated item as a single argument let usePropTerminator = false; while (!stream.eof()) { if (isUnexpectedTerminator(stream.peek()) || stream.eat(ARGUMENTS_END)) { break; } t = tokenConsumer(stream); if (!t) { break; } if (isSemicolonSeparator(t)) { usePropTerminator = true; } tokens.push(t); } stream.start = start; return createArgumentList(stream, tokens, usePropTerminator); } }; function isUnexpectedTerminator(code) { return code === RULE_START$1 || code === RULE_END$1; } function createArgumentList(stream, tokens, usePropTerminator) { const argsToken = new Token(stream, 'arguments'); const isSeparator = usePropTerminator ? isSemicolonSeparator : isCommaSeparator; let arg = []; for (let i = 0, il = tokens.length, token; i < il; i++) { token = tokens[i]; if (isSeparator(token)) { argsToken.add(createArgument(stream, arg) || createEmptyArgument(stream, token.start)); arg.length = 0; } else { arg.push(token); } } if (arg.length) { argsToken.add(createArgument(stream, arg)); } return argsToken; } function createArgument(stream, tokens) { tokens = trimFormatting(tokens); if (tokens.length) { const arg = new Token(stream, 'argument', tokens[0].start, last(tokens).end); for (let i = 0; i < tokens.length; i++) { arg.add(tokens[i]); } return arg; } } function createEmptyArgument(stream, pos) { const token = new Token(stream, 'argument', pos, pos); token.property('empty', true); return token; } function isCommaSeparator(token) { return token.property('type') === 'comma'; } function isSemicolonSeparator(token) { return token.property('type') === 'propertyTerminator'; } 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 || _emmetio_streamReaderUtils.isAlpha(code) || code >= 128; } function isIdent(code) { return _emmetio_streamReaderUtils.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); } const HASH = 35; // # /** * Consumes interpolation token, e.g. `#{expression}` * @param {StreamReader} stream * @param {Function} tokenConsumer * @return {Token} */ function interpolation(stream, tokenConsumer) { const start = stream.pos; tokenConsumer = tokenConsumer || defaultTokenConsumer; if (stream.eat(HASH) && stream.eat(RULE_START$1)) { const container = new Token(stream, 'interpolation', start); let stack = 1, token; while (!stream.eof()) { if (stream.eat(RULE_START$1)) { stack++; } else if (stream.eat(RULE_END$1)) { stack--; if (!stack) { container.end = stream.pos; return container; } } else if (token = tokenConsumer(stream)) { container.add(token); } else { break; } } } stream.pos = start; } function eatInterpolation(stream) { const start = stream.pos; if (stream.eat(HASH) && _emmetio_streamReaderUtils.eatPair(stream, RULE_START$1, RULE_END$1)) { stream.start = start; return true; } stream.pos = start; return false; } function defaultTokenConsumer(stream) { const start = stream.pos; while (!stream.eof()) { if (stream.peek() === RULE_END$1) { break; } eatString$1(stream) || stream.next(); } if (start !== stream.pos) { return new Token(stream, 'expression', start); } } /** * Consumes quoted string from current string and returns token with consumed * data or `null`, if string wasn’t consumed * @param {StreamReader} stream * @return {StringToken} */ function string(stream) { return eatString$1(stream, true); } function eatString$1(stream, asToken) { let ch = stream.peek(), pos, tokens, token; if (_emmetio_streamReaderUtils.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); } else if (asToken && (token = interpolation(stream))) { if (!tokens) { tokens = [token]; } else { tokens.push(token); } } 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'); const inner = new Token(stream, 'unquoted', valueStart, pos); inner.add(tokens); token.add(inner); 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} */ var comment = function(stream) { return singleLineComment(stream) || multiLineComment(stream); }; function singleLineComment(stream) { if (eatSingleLineComment(stream)) { const token = new Token(stream, 'comment'); token.property('type', 'single-line'); return token; } } function multiLineComment(stream) { if (eatMultiLineComment(stream)) { const token = new Token(stream, 'comment'); token.property('type', 'multiline'); return token; } } 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 */; } /** * Consumes white-space tokens from given stream */ function whitespace(stream) { return eatWhitespace(stream) && new Token(stream, 'whitespace'); } function eatWhitespace(stream) { return consumeWhile(stream, _emmetio_streamReaderUtils.isSpace); } const ATTR_START = 91; // [ const ATTR_END = 93; // ] /** * Consumes attribute from given string, e.g. value between [ and ] * @param {StreamReader} stream * @return {AttributeToken} */ function eatAttribuite(stream) { const start = stream.pos; if (stream.eat(ATTR_START)) { skip(stream); const name = ident(stream); skip(stream); const op = operator(stream); skip(stream); const value = string(stream) || ident(stream); skip(stream); stream.eat(ATTR_END); return new Token(stream, 'attribute', start).add(name).add(op).add(value); } } function skip(stream) { while (!stream.eof()) { if (!eatWhitespace(stream) && !eatComment(stream)) { return true; } } } function operator(stream) { return consumeWhile(stream, isOperator) && new Token(stream, 'operator'); } function isOperator(code) { return code === 126 /* ~ */ || code === 124 /* | */ || code === 94 /* ^ */ || code === 36 /* $ */ || code === 42 /* * */ || code === 61; /* = */ } const CLASS = 46; // . /** * Consumes class fragment from given stream, e.g. `.foo` * @param {StreamReader} stream * @return {ClassToken} */ function className(stream) { return prefixed(stream, 'class', CLASS, ident); } const ADJACENT_SIBLING = 43; // + const GENERAL_SIBLING = 126; // ~ const CHILD = 62; // > const NESTING = 38; // & const types$1 = { [ADJACENT_SIBLING]: 'adjacentSibling', [GENERAL_SIBLING]: 'generalSibling', [CHILD]: 'child', [NESTING]: 'nesting' }; /** * Consumes combinator token from given string */ var combinator = function(stream) { if (isCombinator(stream.peek())) { const start = stream.pos; const type = types$1[stream.next()]; const token = new Token(stream, 'combinator', start); token.property('type', type); return token; } }; function isCombinator(code) { return code === ADJACENT_SIBLING || code === GENERAL_SIBLING || code === NESTING || code === CHILD; } const HASH$1 = 35; function hash(stream) { return prefixed(stream, 'hash', HASH$1, hashValue, true); } function hashValue(stream) { if (eatHashValue(stream)) { return new Token(stream, 'hash-value'); } } function eatHashValue(stream) { return consumeWhile(stream, isHashValue); } function isHashValue(code) { return _emmetio_streamReaderUtils.isNumber(code) || _emmetio_streamReaderUtils.isAlpha(code, 65 /* A */, 70 /* F */) || code === 95 /* _ */ || code === 45 /* - */ || code > 128; /* non-ASCII */ } const ID = 35; // # /** * Consumes id fragment from given stream, e.g. `#foo` * @param {StreamReader} stream * @return {Token} */ function id(stream) { return prefixed(stream, 'id', ID, ident); } const IMPORTANT = 33; // ! /** * Consumes !important token * @param {StreamReader} stream * @return {Token} */ function important(stream) { return prefixed(stream, 'important', IMPORTANT, ident); } const DOT = 46; // . /** * Consumes number from given string, e.g. `10px` * @param {StreamReader} stream * @return {NumberToken} */ function number(stream) { if (eatNumericPart(stream)) { const start = stream.start; const num = new Token(stream, 'value'); const unit = eatUnitPart(stream) ? new Token(stream, 'unit') : null; return new Token(stream, 'number', start).add(num).add(unit); } } function eatNumericPart(stream) { const start = stream.pos; stream.eat(isOperator$1); if (stream.eatWhile(_emmetio_streamReaderUtils.isNumber)) { stream.start = start; const decimalEnd = stream.pos; if (!(stream.eat(DOT) && stream.eatWhile(_emmetio_streamReaderUtils.isNumber))) { stream.pos = decimalEnd; } return true; } else if (stream.eat(DOT) && stream.eatWhile(_emmetio_streamReaderUtils.isNumber)) { stream.start = start; return true; } // TODO eat exponent part stream.pos = start; return false; } function eatUnitPart(stream) { return eatIdent(stream) || eatPercent(stream); } function eatPercent(stream) { return consume(stream, 37 /* % */); } function isOperator$1(code) { return code === 45 /* - */ || code === 43 /* + */; } const NOT = 33; // ! const MULTIPLY = 42; // * const PLUS = 43; // + const MINUS = 45; // - const DIVIDE = 47; // / const LESS_THAN = 60; // < const EQUALS = 61; // = const GREATER_THAN = 62; // < function operator$1(stream) { return eatOperator(stream) && new Token(stream, 'operator'); } function eatOperator(stream) { if (consume(stream, isEquality)) { stream.eatWhile(EQUALS); return true; } else if (consume(stream, isOperator$2)) { return true; } return false; } function isEquality(code) { return code === NOT || code === LESS_THAN || code === EQUALS || code === GREATER_THAN; } function isOperator$2(code) { return code === MULTIPLY || code === PLUS || code === MINUS || code === DIVIDE || isEquality(code); } const PSEUDO = 58; // : /** * Consumes pseudo-selector from given stream */ var pseudo = function(stream) { const start = stream.pos; if (stream.eatWhile(PSEUDO)) { const name = ident(stream); if (name) { return new Token(stream, 'pseudo', start).add(name); } } stream.pos = start; }; /** * Consumes unquoted value from given stream * @param {StreamReader} stream * @return {UnquotedToken} */ var unquoted = function(stream) { return eatUnquoted(stream) && new Token(stream, 'unquoted'); }; function eatUnquoted(stream) { return consumeWhile(stream, isUnquoted); } function isUnquoted(code) { return !isNaN(code) && !_emmetio_streamReaderUtils.isQuote(code) && !_emmetio_streamReaderUtils.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; } /** * Consumes URL token from given stream * @param {StreamReader} stream * @return {Token} */ function url(stream) { const start = stream.pos; if (eatString(stream, 'url(')) { eatWhitespace(stream); const value = string(stream) || unquoted(stream); eatWhitespace(stream); stream.eat(41); // ) return new Token(stream, 'url', start).add(value); } stream.pos = start; } 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 VARIABLE = 36; // $ /** * Consumes SCSS variable from given stream */ function variable(stream) { return prefixed(stream, 'variable', VARIABLE, variableName); } function variableName(stream) { if (eatVariableName(stream)) { return new Token(stream, 'name'); } } function eatVariableName(stream) { return consumeWhile(stream, isVariableName); } function isVariableName(code) { return code === VARIABLE || isIdent(code); } /** * Group tokens by commonly used context */ function consumeToken(stream) { const _token = any(stream) || args(stream, consumeToken); if (_token && _token.type === 'ident') { const _args = args(stream, consumeToken); if (_args) { // An identifier followed by arguments – function call return new Token(stream, 'function', _token.start, _args.end).add(_token).add(_args); } } return _token || unknown(stream); } function any(stream) { return formatting(stream) || url(stream) || selector(stream) || value(stream) || separator(stream); } function selector(stream) { return interpolation(stream) || ident(stream) || atKeyword(stream) || className(stream) || id(stream) || pseudo(stream) || eatAttribuite(stream) || combinator(stream); } function value(stream) { return url(stream) || string(stream) || interpolation(stream) || number(stream) || hash(stream) || keyword(stream) || important(stream) || operator$1(stream); } function keyword(stream) { return variable(stream) || atKeyword(stream) || ident(stream); } function formatting(stream) { return comment(stream) || whitespace(stream); } function unknown(stream) { stream.start = stream.pos; const ch = stream.next(); if (ch != null) { return new Token(stream, 'unknown'); } } /** * Parses CSS rule selector * @param {String|StreamReader} source * @return {Token[]} */ function parseSelector(source) { return parseList(source, 'selector'); } /** * Parses CSS property name. Mostly used for LESS where * property-like entry might be used as a mixin call * @param {String|StreamReader} source * @return {Token} */ function parsePropertyName(source) { const stream = typeof source === 'string' ? new StreamReader(source) : source; const items = []; while (!stream.eof()) { items.push(consumeToken(stream)); } let token; if (items.length === 1) { token = items[0]; } else { token = new Token(stream, 'property-name', stream.start, stream.end); for (let i = 0, il = items.length; i < il; i++) { token.add(items[i]); } } return token; } /** * Parses CSS property value * @param {String|StreamReader} source * @return {Token[]} */ function parsePropertyValue(source) { return parseList(source); } /** * Parses @media CSS rule expression * @param {String|StreamReader} source * @return {Token[]} */ function parseMediaExpression(source) { return parseList(source); } /** * Parses given source into a set of tokens, separated by comma. Each token contains * parsed sub-items as independent tokens and so on. Mostly used to parse * selectors and property values * @param {String|StreamReader} source Source to parse * @param {String} [tokenType] Type of first-level tokens. * Default is `item` * @return {Token[]} */ function parseList(source, tokenType) { tokenType = tokenType || 'item'; const stream = typeof source === 'string' ? new StreamReader(source) : source; const items = []; const fragments = []; const flush = () => { const clean = trimFormatting(fragments); if (clean.length) { const item = new Token(stream, tokenType, clean[0].start, last(clean).end); for (let i = 0; i < clean.length; i++) { item.add(clean[i]); } items.push(item); } fragments.length = 0; }; let token; while (!stream.eof()) { if (stream.eat(44 /* , */)) { flush(); } else if (token = consumeToken(stream)) { if (token.type !== 'comment') { fragments.push(token); } } else { throw stream.error('Unexpected character'); } } flush(); return items; } /** * Creates CSS rule from given tokens * @param {StreamReader} stream * @param {Token[]} tokens * @param {Token} [content] * @return {Rule} */ function createRule(stream, tokens, contentStart, contentEnd) { if (!tokens.length) { return null; } const name = tokens[0]; name.end = last(tokens).end; return new Rule(stream, name, contentStart, contentEnd); } /** * Represents CSS rule * @type {Node} */ class Rule extends Node { /** * @param {StreamReader} stream * @param {Token} name Rule’s name token * @param {Token} contentStart Rule’s content start token * @param {Token} [contentEnd] Rule’s content end token */ constructor(stream, name, contentStart, contentEnd) { super('rule'); this.stream = stream; this.selectorToken = name; this.contentStartToken = contentStart; this.contentEndToken = contentEnd || contentStart; this._parsedSelector = null; } /** * Returns rule selector * @return {String} */ get selector() { return valueOf(this.selectorToken); } get parsedSelector() { if (!this._parsedSelector) { this._parsedSelector = parseSelector(this.selectorToken.limit()); } return this._parsedSelector; } /** * Returns node’s start position in stream * @return {*} */ get start() { return this.selectorToken && this.selectorToken.start; } /** * Returns node’s end position in stream * @return {*} */ get end() { const token = this.contentEndToken || this.contentStartToken || this.nameToken; return token && token.end; } } /** * Creates CSS rule from given tokens * @param {StreamReader} stream * @param {Token[]} tokens * @param {Token} [content] * @return {Rule} */ function createAtRule(stream, tokens, contentStart, contentEnd) { if (!tokens.length) { return null; } let ix = 0, expression; const name = tokens[ix++]; 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, contentStart, contentEnd); } class AtRule extends Node { constructor(stream, name, expression, contentStart, contentEnd) { super('at-rule'); this.stream = stream; this.nameToken = name; this.expressionToken = expression; this.contentStartToken = contentStart; this.contentEndToken = contentEnd || contentStart; this._parsedExpression = null; } /** * Returns at-rule name * @return {String} */ get name() { return valueOf(this.nameToken && this.nameToken.item(0)); } get expression() { return valueOf(this.expressionToken); } get parsedExpression() { if (!this._parsedExpression) { this._parsedExpression = parseMediaExpression(this.expressionToken.limit()); } return this._parsedExpression; } /** * 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.contentEndToken || this.contentStartToken || this.nameToken; return token && token.end; } } /** * Factory method that creates property node from given tokens * @param {StreamReader} stream * @param {Token[]} tokens * @param {Token} terminator * @return {Property} */ 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 extends Node { constructor(stream, name, value, separator, terminator) { super('property'); this.stream = stream; this.nameToken = name; this.valueToken = value; this._parsedName = null; this._parsedValue = null; this.separatorToken = separator; this.terminatorToken = terminator; } /** * Property name * @return {String} */ get name() { return valueOf(this.nameToken); } /** * Returns parsed sub-tokens of current property name * @return {Token[]} */ get parsedName() { if (!this._parsedName) { this._parsedName = parsePropertyName(this.nameToken.limit()); } return this._parsedName; } /** * Property value * @return {String} */ get value() { return valueOf(this.valueToken); } /** * Parsed value parts: a list of tokens, separated by comma. Each token may * contains parsed sub-tokens and so on * @return {Token[]} */ get parsedValue() { if (!this._parsedValue) { this._parsedValue = parsePropertyValue(this.valueToken.limit()); } return this._parsedValue; } get separator() { return valueOf(this.separatorToken); } get terminator() { return valueOf(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; } } 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)) { continue; } if (token = comment(stream)) { root.addComment(token); 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(PROP_TERMINATOR)) { flush(); ctx.add(createProperty(stream, tokens, new Token(stream, 'termintator'))); tokens.length = 0; } else if (stream.eat(RULE_START)) { flush(); child = tokens[0].type === 'at-keyword' ? createAtRule(stream, tokens, new Token(stream, 'body-start')) : createRule(stream, tokens, new Token(stream, 'body-start')); ctx.add(child); ctx = child; tokens.length = 0; } 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.contentEndToken = new Token(stream, 'body-end'); ctx = ctx.parent; } 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) || eatInterpolation(stream) || eatBraces(stream, root) || 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)); // Finalize unterminated rules stream.start = stream.pos; while (ctx && ctx !== root) { ctx.contentEndToken = new Token(stream, 'body-end'); ctx = ctx.parent; } return root; } /** * Parses given source into tokens * @param {String|StreamReader} source * @param {Function} [consumer] Token consumer function, for example, `selector`, * `value` etc. from `lib/tokens` module. Default is generic `consumeToken` * @return {Token[]} */ function lexer(source, consumer) { consumer = consumer || consumeToken; const stream = typeof source === 'string' ? new StreamReader(source) : source; const result = []; let token; while (!stream.eof() && (token = consumer(stream))) { result.push(token); } return result; } /** * 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 * @param {Stylesheet} root A stylesheet root. Used to accumulate comments * @return {Boolean} */ function eatBraces(stream, root) { if (stream.eat(LBRACE)) { let stack = 1, token; while (!stream.eof()) { if (stream.eat(RBRACE)) { stack--; if (!stack) { break; } } else if (stream.eat(LBRACE)) { stack++; } else if (eatUrl(stream) || eatString$1(stream)) { continue; } else if (token = comment(stream)) { root.addComment(token); continue; } else { stream.next(); } } return true; } return false; } exports['default'] = parseStylesheet; exports.lexer = lexer; exports.Token = Token; exports.any = any; exports.selector = selector; exports.value = value; exports.keyword = keyword; exports.variable = variable; exports.formatting = formatting; exports.comment = comment; exports.whitespace = whitespace; exports.ident = ident; exports.string = string; exports.url = url; exports.interpolation = interpolation; exports.parseMediaExpression = parseMediaExpression; exports.parsePropertyName = parsePropertyName; exports.parsePropertyValue = parsePropertyValue; exports.parseSelector = parseSelector; exports.createProperty = createProperty; exports.createRule = createRule; exports.createAtRule = createAtRule;