Files
ry.kazcloud.dev/node_modules/@emmetio/css-parser/dist/css-parser.es.js
Ryan Kazokas d181f77fb2
All checks were successful
Build and Push / build (push) Successful in 55s
Updates dockerfile
2026-02-16 15:09:37 -05:00

1594 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import StreamReader from '@emmetio/stream-reader';
import { eatPair, isAlpha, isNumber, isQuote, isSpace } from '@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 elements 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 nodes start position in stream
* @return {*}
*/
get start() {
const node = this.firstChild;
return node && node.start;
}
/**
* Returns nodes 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: its 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, its 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 || 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);
}
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) && 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 wasnt 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 (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 (dont 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, 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 isNumber(code) || 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(isNumber)) {
stream.start = start;
const decimalEnd = stream.pos;
if (!(stream.eat(DOT) && stream.eatWhile(isNumber))) {
stream.pos = decimalEnd;
}
return true;
} else if (stream.eat(DOT) && stream.eatWhile(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) && !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;
}
/**
* 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 Rules name token
* @param {Token} contentStart Rules content start token
* @param {Token} [contentEnd] Rules 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 nodes start position in stream
* @return {*}
*/
get start() {
return this.selectorToken && this.selectorToken.start;
}
/**
* Returns nodes 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 nodes start position in stream
* @return {*}
*/
get start() {
return this.nameToken && this.nameToken.start;
}
/**
* Returns nodes 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 its
// 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
// its 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 `}`,
// dont 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;
}
export { lexer, Token, any, selector, value, keyword, variable, formatting, comment, whitespace, ident, string, url, interpolation, parseMediaExpression, parsePropertyName, parsePropertyValue, parseSelector, createProperty, createRule, createAtRule };export default parseStylesheet;