687 lines
20 KiB
JavaScript
687 lines
20 KiB
JavaScript
'use strict';
|
||
|
||
Object.defineProperty(exports, '__esModule', { value: true });
|
||
|
||
var Scanner = require('@emmetio/scanner');
|
||
|
||
exports.OperatorType = void 0;
|
||
(function (OperatorType) {
|
||
OperatorType["Sibling"] = "+";
|
||
OperatorType["Important"] = "!";
|
||
OperatorType["ArgumentDelimiter"] = ",";
|
||
OperatorType["ValueDelimiter"] = "-";
|
||
OperatorType["PropertyDelimiter"] = ":";
|
||
})(exports.OperatorType || (exports.OperatorType = {}));
|
||
|
||
var Chars;
|
||
(function (Chars) {
|
||
/** `#` character */
|
||
Chars[Chars["Hash"] = 35] = "Hash";
|
||
/** `$` character */
|
||
Chars[Chars["Dollar"] = 36] = "Dollar";
|
||
/** `-` character */
|
||
Chars[Chars["Dash"] = 45] = "Dash";
|
||
/** `.` character */
|
||
Chars[Chars["Dot"] = 46] = "Dot";
|
||
/** `:` character */
|
||
Chars[Chars["Colon"] = 58] = "Colon";
|
||
/** `,` character */
|
||
Chars[Chars["Comma"] = 44] = "Comma";
|
||
/** `!` character */
|
||
Chars[Chars["Excl"] = 33] = "Excl";
|
||
/** `@` character */
|
||
Chars[Chars["At"] = 64] = "At";
|
||
/** `%` character */
|
||
Chars[Chars["Percent"] = 37] = "Percent";
|
||
/** `_` character */
|
||
Chars[Chars["Underscore"] = 95] = "Underscore";
|
||
/** `(` character */
|
||
Chars[Chars["RoundBracketOpen"] = 40] = "RoundBracketOpen";
|
||
/** `)` character */
|
||
Chars[Chars["RoundBracketClose"] = 41] = "RoundBracketClose";
|
||
/** `{` character */
|
||
Chars[Chars["CurlyBracketOpen"] = 123] = "CurlyBracketOpen";
|
||
/** `}` character */
|
||
Chars[Chars["CurlyBracketClose"] = 125] = "CurlyBracketClose";
|
||
/** `+` character */
|
||
Chars[Chars["Sibling"] = 43] = "Sibling";
|
||
/** `'` character */
|
||
Chars[Chars["SingleQuote"] = 39] = "SingleQuote";
|
||
/** `"` character */
|
||
Chars[Chars["DoubleQuote"] = 34] = "DoubleQuote";
|
||
/** `t` character */
|
||
Chars[Chars["Transparent"] = 116] = "Transparent";
|
||
/** `/` character */
|
||
Chars[Chars["Slash"] = 47] = "Slash";
|
||
})(Chars || (Chars = {}));
|
||
|
||
function tokenize(abbr, isValue) {
|
||
let brackets = 0;
|
||
let token;
|
||
const scanner = new Scanner(abbr);
|
||
const tokens = [];
|
||
while (!scanner.eof()) {
|
||
token = getToken(scanner, brackets === 0 && !isValue);
|
||
if (!token) {
|
||
throw scanner.error('Unexpected character');
|
||
}
|
||
if (token.type === 'Bracket') {
|
||
if (!brackets && token.open) {
|
||
mergeTokens(scanner, tokens);
|
||
}
|
||
brackets += token.open ? 1 : -1;
|
||
if (brackets < 0) {
|
||
throw scanner.error('Unexpected bracket', token.start);
|
||
}
|
||
}
|
||
tokens.push(token);
|
||
// Forcibly consume next operator after unit-less numeric value or color:
|
||
// next dash `-` must be used as value delimiter
|
||
if (shouldConsumeDashAfter(token) && (token = operator(scanner))) {
|
||
tokens.push(token);
|
||
}
|
||
}
|
||
return tokens;
|
||
}
|
||
/**
|
||
* Returns next token from given scanner, if possible
|
||
*/
|
||
function getToken(scanner, short) {
|
||
return field(scanner)
|
||
|| customProperty(scanner)
|
||
|| numberValue(scanner)
|
||
|| colorValue(scanner)
|
||
|| stringValue(scanner)
|
||
|| bracket(scanner)
|
||
|| operator(scanner)
|
||
|| whiteSpace(scanner)
|
||
|| literal(scanner, short);
|
||
}
|
||
function field(scanner) {
|
||
const start = scanner.pos;
|
||
if (scanner.eat(Chars.Dollar) && scanner.eat(Chars.CurlyBracketOpen)) {
|
||
scanner.start = scanner.pos;
|
||
let index;
|
||
let name = '';
|
||
if (scanner.eatWhile(Scanner.isNumber)) {
|
||
// It’s a field
|
||
index = Number(scanner.current());
|
||
name = scanner.eat(Chars.Colon) ? consumePlaceholder(scanner) : '';
|
||
}
|
||
else if (Scanner.isAlpha(scanner.peek())) {
|
||
// It’s a variable
|
||
name = consumePlaceholder(scanner);
|
||
}
|
||
if (scanner.eat(Chars.CurlyBracketClose)) {
|
||
return {
|
||
type: 'Field',
|
||
index, name,
|
||
start,
|
||
end: scanner.pos
|
||
};
|
||
}
|
||
throw scanner.error('Expecting }');
|
||
}
|
||
// If we reached here then there’s no valid field here, revert
|
||
// back to starting position
|
||
scanner.pos = start;
|
||
}
|
||
/**
|
||
* Consumes a placeholder: value right after `:` in field. Could be empty
|
||
*/
|
||
function consumePlaceholder(stream) {
|
||
const stack = [];
|
||
stream.start = stream.pos;
|
||
while (!stream.eof()) {
|
||
if (stream.eat(Chars.CurlyBracketOpen)) {
|
||
stack.push(stream.pos);
|
||
}
|
||
else if (stream.eat(Chars.CurlyBracketClose)) {
|
||
if (!stack.length) {
|
||
stream.pos--;
|
||
break;
|
||
}
|
||
stack.pop();
|
||
}
|
||
else {
|
||
stream.pos++;
|
||
}
|
||
}
|
||
if (stack.length) {
|
||
stream.pos = stack.pop();
|
||
throw stream.error(`Expecting }`);
|
||
}
|
||
return stream.current();
|
||
}
|
||
/**
|
||
* Consumes literal from given scanner
|
||
* @param short Use short notation for consuming value.
|
||
* The difference between “short” and “full” notation is that first one uses
|
||
* alpha characters only and used for extracting keywords from abbreviation,
|
||
* while “full” notation also supports numbers and dashes
|
||
*/
|
||
function literal(scanner, short) {
|
||
const start = scanner.pos;
|
||
if (scanner.eat(isIdentPrefix)) {
|
||
// SCSS or LESS variable
|
||
// NB a bit dirty hack: if abbreviation starts with identifier prefix,
|
||
// consume alpha characters only to allow embedded variables
|
||
scanner.eatWhile(start ? isKeyword : isLiteral$1);
|
||
}
|
||
else if (scanner.eat(Scanner.isAlphaWord)) {
|
||
scanner.eatWhile(short ? isLiteral$1 : isKeyword);
|
||
}
|
||
else {
|
||
// Allow dots only at the beginning of literal
|
||
scanner.eat(Chars.Dot);
|
||
scanner.eatWhile(isLiteral$1);
|
||
}
|
||
if (start !== scanner.pos) {
|
||
scanner.start = start;
|
||
return createLiteral(scanner, scanner.start = start);
|
||
}
|
||
}
|
||
function createLiteral(scanner, start = scanner.start, end = scanner.pos) {
|
||
return {
|
||
type: 'Literal',
|
||
value: scanner.substring(start, end),
|
||
start,
|
||
end
|
||
};
|
||
}
|
||
/**
|
||
* Consumes numeric CSS value (number with optional unit) from current stream,
|
||
* if possible
|
||
*/
|
||
function numberValue(scanner) {
|
||
const start = scanner.pos;
|
||
if (consumeNumber(scanner)) {
|
||
scanner.start = start;
|
||
const rawValue = scanner.current();
|
||
// eat unit, which can be a % or alpha word
|
||
scanner.start = scanner.pos;
|
||
scanner.eat(Chars.Percent) || scanner.eatWhile(Scanner.isAlphaWord);
|
||
return {
|
||
type: 'NumberValue',
|
||
value: Number(rawValue),
|
||
rawValue,
|
||
unit: scanner.current(),
|
||
start,
|
||
end: scanner.pos
|
||
};
|
||
}
|
||
}
|
||
/**
|
||
* Consumes quoted string value from given scanner
|
||
*/
|
||
function stringValue(scanner) {
|
||
const ch = scanner.peek();
|
||
const start = scanner.pos;
|
||
let finished = false;
|
||
if (Scanner.isQuote(ch)) {
|
||
scanner.pos++;
|
||
while (!scanner.eof()) {
|
||
// Do not throw error on malformed string
|
||
if (scanner.eat(ch)) {
|
||
finished = true;
|
||
break;
|
||
}
|
||
else {
|
||
scanner.pos++;
|
||
}
|
||
}
|
||
scanner.start = start;
|
||
return {
|
||
type: 'StringValue',
|
||
value: scanner.substring(start + 1, scanner.pos - (finished ? 1 : 0)),
|
||
quote: ch === Chars.SingleQuote ? 'single' : 'double',
|
||
start,
|
||
end: scanner.pos
|
||
};
|
||
}
|
||
}
|
||
/**
|
||
* Consumes a color token from given string
|
||
*/
|
||
function colorValue(scanner) {
|
||
// supported color variations:
|
||
// #abc → #aabbccc
|
||
// #0 → #000000
|
||
// #fff.5 → rgba(255, 255, 255, 0.5)
|
||
// #t → transparent
|
||
const start = scanner.pos;
|
||
if (scanner.eat(Chars.Hash)) {
|
||
const valueStart = scanner.pos;
|
||
let color = '';
|
||
let alpha = '';
|
||
if (scanner.eatWhile(isHex)) {
|
||
color = scanner.substring(valueStart, scanner.pos);
|
||
alpha = colorAlpha(scanner);
|
||
}
|
||
else if (scanner.eat(Chars.Transparent)) {
|
||
color = '0';
|
||
alpha = colorAlpha(scanner) || '0';
|
||
}
|
||
else {
|
||
alpha = colorAlpha(scanner);
|
||
}
|
||
if (color || alpha || scanner.eof()) {
|
||
const { r, g, b, a } = parseColor(color, alpha);
|
||
return {
|
||
type: 'ColorValue',
|
||
r, g, b, a,
|
||
raw: scanner.substring(start + 1, scanner.pos),
|
||
start,
|
||
end: scanner.pos
|
||
};
|
||
}
|
||
else {
|
||
// Consumed # but no actual value: invalid color value, treat it as literal
|
||
return createLiteral(scanner, start);
|
||
}
|
||
}
|
||
scanner.pos = start;
|
||
}
|
||
/**
|
||
* Consumes alpha value of color: `.1`
|
||
*/
|
||
function colorAlpha(scanner) {
|
||
const start = scanner.pos;
|
||
if (scanner.eat(Chars.Dot)) {
|
||
scanner.start = start;
|
||
if (scanner.eatWhile(Scanner.isNumber)) {
|
||
return scanner.current();
|
||
}
|
||
return '1';
|
||
}
|
||
return '';
|
||
}
|
||
/**
|
||
* Consumes white space characters as string literal from given scanner
|
||
*/
|
||
function whiteSpace(scanner) {
|
||
const start = scanner.pos;
|
||
if (scanner.eatWhile(Scanner.isSpace)) {
|
||
return {
|
||
type: 'WhiteSpace',
|
||
start,
|
||
end: scanner.pos
|
||
};
|
||
}
|
||
}
|
||
/**
|
||
* Consumes custom CSS property: --foo-bar
|
||
*/
|
||
function customProperty(scanner) {
|
||
const start = scanner.pos;
|
||
if (scanner.eat(Chars.Dash) && scanner.eat(Chars.Dash)) {
|
||
scanner.start = start;
|
||
scanner.eatWhile(isKeyword);
|
||
return {
|
||
type: 'CustomProperty',
|
||
value: scanner.current(),
|
||
start,
|
||
end: scanner.pos
|
||
};
|
||
}
|
||
scanner.pos = start;
|
||
}
|
||
/**
|
||
* Consumes bracket from given scanner
|
||
*/
|
||
function bracket(scanner) {
|
||
const ch = scanner.peek();
|
||
if (isBracket$1(ch)) {
|
||
return {
|
||
type: 'Bracket',
|
||
open: ch === Chars.RoundBracketOpen,
|
||
start: scanner.pos++,
|
||
end: scanner.pos
|
||
};
|
||
}
|
||
}
|
||
/**
|
||
* Consumes operator from given scanner
|
||
*/
|
||
function operator(scanner) {
|
||
const op = operatorType(scanner.peek());
|
||
if (op) {
|
||
return {
|
||
type: 'Operator',
|
||
operator: op,
|
||
start: scanner.pos++,
|
||
end: scanner.pos
|
||
};
|
||
}
|
||
}
|
||
/**
|
||
* Eats number value from given stream
|
||
* @return Returns `true` if number was consumed
|
||
*/
|
||
function consumeNumber(stream) {
|
||
const start = stream.pos;
|
||
stream.eat(Chars.Dash);
|
||
const afterNegative = stream.pos;
|
||
const hasDecimal = stream.eatWhile(Scanner.isNumber);
|
||
const prevPos = stream.pos;
|
||
if (stream.eat(Chars.Dot)) {
|
||
// It’s perfectly valid to have numbers like `1.`, which enforces
|
||
// value to float unit type
|
||
const hasFloat = stream.eatWhile(Scanner.isNumber);
|
||
if (!hasDecimal && !hasFloat) {
|
||
// Lone dot
|
||
stream.pos = prevPos;
|
||
}
|
||
}
|
||
// Edge case: consumed dash only: not a number, bail-out
|
||
if (stream.pos === afterNegative) {
|
||
stream.pos = start;
|
||
}
|
||
return stream.pos !== start;
|
||
}
|
||
function isIdentPrefix(code) {
|
||
return code === Chars.At || code === Chars.Dollar;
|
||
}
|
||
/**
|
||
* If given character is an operator, returns it’s type
|
||
*/
|
||
function operatorType(ch) {
|
||
return (ch === Chars.Sibling && exports.OperatorType.Sibling)
|
||
|| (ch === Chars.Excl && exports.OperatorType.Important)
|
||
|| (ch === Chars.Comma && exports.OperatorType.ArgumentDelimiter)
|
||
|| (ch === Chars.Colon && exports.OperatorType.PropertyDelimiter)
|
||
|| (ch === Chars.Dash && exports.OperatorType.ValueDelimiter)
|
||
|| void 0;
|
||
}
|
||
/**
|
||
* Check if given code is a hex value (/0-9a-f/)
|
||
*/
|
||
function isHex(code) {
|
||
return Scanner.isNumber(code) || Scanner.isAlpha(code, 65, 70); // A-F
|
||
}
|
||
function isKeyword(code) {
|
||
return Scanner.isAlphaNumericWord(code) || code === Chars.Dash;
|
||
}
|
||
function isBracket$1(code) {
|
||
return code === Chars.RoundBracketOpen || code === Chars.RoundBracketClose;
|
||
}
|
||
function isLiteral$1(code) {
|
||
return Scanner.isAlphaWord(code) || code === Chars.Percent || code === Chars.Slash;
|
||
}
|
||
/**
|
||
* Parses given color value from abbreviation into RGBA format
|
||
*/
|
||
function parseColor(value, alpha) {
|
||
let r = '0';
|
||
let g = '0';
|
||
let b = '0';
|
||
let a = Number(alpha != null && alpha !== '' ? alpha : 1);
|
||
if (value === 't') {
|
||
a = 0;
|
||
}
|
||
else {
|
||
switch (value.length) {
|
||
case 0:
|
||
break;
|
||
case 1:
|
||
r = g = b = value + value;
|
||
break;
|
||
case 2:
|
||
r = g = b = value;
|
||
break;
|
||
case 3:
|
||
r = value[0] + value[0];
|
||
g = value[1] + value[1];
|
||
b = value[2] + value[2];
|
||
break;
|
||
default:
|
||
value += value;
|
||
r = value.slice(0, 2);
|
||
g = value.slice(2, 4);
|
||
b = value.slice(4, 6);
|
||
}
|
||
}
|
||
return {
|
||
r: parseInt(r, 16),
|
||
g: parseInt(g, 16),
|
||
b: parseInt(b, 16),
|
||
a
|
||
};
|
||
}
|
||
/**
|
||
* Check if scanner reader must consume dash after given token.
|
||
* Used in cases where user must explicitly separate numeric values
|
||
*/
|
||
function shouldConsumeDashAfter(token) {
|
||
return token.type === 'ColorValue' || (token.type === 'NumberValue' && !token.unit);
|
||
}
|
||
/**
|
||
* Merges last adjacent tokens into a single literal.
|
||
* This function is used to overcome edge case when function name was parsed
|
||
* as a list of separate tokens. For example, a `scale3d()` value will be
|
||
* parsed as literal and number tokens (`scale` and `3d`) which is a perfectly
|
||
* valid abbreviation but undesired result. This function will detect last adjacent
|
||
* literal and number values and combine them into single literal
|
||
*/
|
||
function mergeTokens(scanner, tokens) {
|
||
let start = 0;
|
||
let end = 0;
|
||
while (tokens.length) {
|
||
const token = last(tokens);
|
||
if (token.type === 'Literal' || token.type === 'NumberValue') {
|
||
start = token.start;
|
||
if (!end) {
|
||
end = token.end;
|
||
}
|
||
tokens.pop();
|
||
}
|
||
else {
|
||
break;
|
||
}
|
||
}
|
||
if (start !== end) {
|
||
tokens.push(createLiteral(scanner, start, end));
|
||
}
|
||
}
|
||
function last(arr) {
|
||
return arr[arr.length - 1];
|
||
}
|
||
|
||
function tokenScanner(tokens) {
|
||
return {
|
||
tokens,
|
||
start: 0,
|
||
pos: 0,
|
||
size: tokens.length
|
||
};
|
||
}
|
||
function peek(scanner) {
|
||
return scanner.tokens[scanner.pos];
|
||
}
|
||
function readable(scanner) {
|
||
return scanner.pos < scanner.size;
|
||
}
|
||
function consume(scanner, test) {
|
||
if (test(peek(scanner))) {
|
||
scanner.pos++;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function error(scanner, message, token = peek(scanner)) {
|
||
if (token && token.start != null) {
|
||
message += ` at ${token.start}`;
|
||
}
|
||
const err = new Error(message);
|
||
err['pos'] = token && token.start;
|
||
return err;
|
||
}
|
||
|
||
function parser(tokens, options = {}) {
|
||
const scanner = tokenScanner(tokens);
|
||
const result = [];
|
||
let property;
|
||
while (readable(scanner)) {
|
||
if (property = consumeProperty(scanner, options)) {
|
||
result.push(property);
|
||
}
|
||
else if (!consume(scanner, isSiblingOperator)) {
|
||
throw error(scanner, 'Unexpected token');
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
/**
|
||
* Consumes single CSS property
|
||
*/
|
||
function consumeProperty(scanner, options) {
|
||
let name;
|
||
let important = false;
|
||
let valueFragment;
|
||
const value = [];
|
||
const token = peek(scanner);
|
||
const valueMode = !!options.value;
|
||
if (!valueMode && isLiteral(token) && !isFunctionStart(scanner)) {
|
||
scanner.pos++;
|
||
name = token.value;
|
||
// Consume any following value delimiter after property name
|
||
consume(scanner, isValueDelimiter);
|
||
}
|
||
// Skip whitespace right after property name, if any
|
||
if (valueMode) {
|
||
consume(scanner, isWhiteSpace);
|
||
}
|
||
while (readable(scanner)) {
|
||
if (consume(scanner, isImportant)) {
|
||
important = true;
|
||
}
|
||
else if (valueFragment = consumeValue(scanner, valueMode)) {
|
||
value.push(valueFragment);
|
||
}
|
||
else if (!consume(scanner, isFragmentDelimiter)) {
|
||
break;
|
||
}
|
||
}
|
||
if (name || value.length || important) {
|
||
return { name, value, important };
|
||
}
|
||
}
|
||
/**
|
||
* Consumes single value fragment, e.g. all value tokens before comma
|
||
*/
|
||
function consumeValue(scanner, inArgument) {
|
||
const result = [];
|
||
let token;
|
||
let args;
|
||
while (readable(scanner)) {
|
||
token = peek(scanner);
|
||
if (isValue(token)) {
|
||
scanner.pos++;
|
||
if (isLiteral(token) && (args = consumeArguments(scanner))) {
|
||
result.push({
|
||
type: 'FunctionCall',
|
||
name: token.value,
|
||
arguments: args
|
||
});
|
||
}
|
||
else {
|
||
result.push(token);
|
||
}
|
||
}
|
||
else if (isValueDelimiter(token) || (inArgument && isWhiteSpace(token))) {
|
||
scanner.pos++;
|
||
}
|
||
else {
|
||
break;
|
||
}
|
||
}
|
||
return result.length
|
||
? { type: 'CSSValue', value: result }
|
||
: void 0;
|
||
}
|
||
function consumeArguments(scanner) {
|
||
const start = scanner.pos;
|
||
if (consume(scanner, isOpenBracket)) {
|
||
const args = [];
|
||
let value;
|
||
while (readable(scanner) && !consume(scanner, isCloseBracket)) {
|
||
if (value = consumeValue(scanner, true)) {
|
||
args.push(value);
|
||
}
|
||
else if (!consume(scanner, isWhiteSpace) && !consume(scanner, isArgumentDelimiter)) {
|
||
throw error(scanner, 'Unexpected token');
|
||
}
|
||
}
|
||
scanner.start = start;
|
||
return args;
|
||
}
|
||
}
|
||
function isLiteral(token) {
|
||
return token && token.type === 'Literal';
|
||
}
|
||
function isBracket(token, open) {
|
||
return token && token.type === 'Bracket' && (open == null || token.open === open);
|
||
}
|
||
function isOpenBracket(token) {
|
||
return isBracket(token, true);
|
||
}
|
||
function isCloseBracket(token) {
|
||
return isBracket(token, false);
|
||
}
|
||
function isWhiteSpace(token) {
|
||
return token && token.type === 'WhiteSpace';
|
||
}
|
||
function isOperator(token, operator) {
|
||
return token && token.type === 'Operator' && (!operator || token.operator === operator);
|
||
}
|
||
function isSiblingOperator(token) {
|
||
return isOperator(token, exports.OperatorType.Sibling);
|
||
}
|
||
function isArgumentDelimiter(token) {
|
||
return isOperator(token, exports.OperatorType.ArgumentDelimiter);
|
||
}
|
||
function isFragmentDelimiter(token) {
|
||
return isArgumentDelimiter(token);
|
||
}
|
||
function isImportant(token) {
|
||
return isOperator(token, exports.OperatorType.Important);
|
||
}
|
||
function isValue(token) {
|
||
return token.type === 'StringValue'
|
||
|| token.type === 'ColorValue'
|
||
|| token.type === 'NumberValue'
|
||
|| token.type === 'Literal'
|
||
|| token.type === 'Field'
|
||
|| token.type === 'CustomProperty';
|
||
}
|
||
function isValueDelimiter(token) {
|
||
return isOperator(token, exports.OperatorType.PropertyDelimiter)
|
||
|| isOperator(token, exports.OperatorType.ValueDelimiter);
|
||
}
|
||
function isFunctionStart(scanner) {
|
||
const t1 = scanner.tokens[scanner.pos];
|
||
const t2 = scanner.tokens[scanner.pos + 1];
|
||
return t1 && t2 && isLiteral(t1) && t2.type === 'Bracket';
|
||
}
|
||
|
||
/**
|
||
* Parses given abbreviation into property set
|
||
*/
|
||
function parse(abbr, options) {
|
||
try {
|
||
const tokens = typeof abbr === 'string' ? tokenize(abbr, options && options.value) : abbr;
|
||
return parser(tokens, options);
|
||
}
|
||
catch (err) {
|
||
if (err instanceof Scanner.ScannerError && typeof abbr === 'string') {
|
||
err.message += `\n${abbr}\n${'-'.repeat(err.pos)}^`;
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
exports.default = parse;
|
||
exports.getToken = getToken;
|
||
exports.parser = parser;
|
||
exports.tokenize = tokenize;
|
||
//# sourceMappingURL=index.cjs.map
|