5373 lines
171 KiB
Plaintext
5373 lines
171 KiB
Plaintext
'use strict';
|
||
|
||
Object.defineProperty(exports, '__esModule', { value: true });
|
||
|
||
/**
|
||
* Check if given code is a number
|
||
*/
|
||
function isNumber$1(code) {
|
||
return code > 47 && code < 58;
|
||
}
|
||
/**
|
||
* Check if given character code is alpha code (letter through A to Z)
|
||
*/
|
||
function isAlpha$1(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;
|
||
}
|
||
function isAlphaNumericWord(code) {
|
||
return isNumber$1(code) || isAlphaWord(code);
|
||
}
|
||
function isAlphaWord(code) {
|
||
return code === 95 /* _ */ || isAlpha$1(code);
|
||
}
|
||
/**
|
||
* Check for Umlauts i.e. ä, Ä, ö, Ö, ü and Ü
|
||
*/
|
||
function isUmlaut(code) {
|
||
return code === 196
|
||
|| code == 214
|
||
|| code === 220
|
||
|| code === 228
|
||
|| code === 246
|
||
|| code === 252;
|
||
}
|
||
/**
|
||
* Check if given character code is a white-space character: a space character
|
||
* or line breaks
|
||
*/
|
||
function isWhiteSpace$3(code) {
|
||
return code === 32 /* space */
|
||
|| code === 9 /* tab */
|
||
|| code === 160; /* non-breaking space */
|
||
}
|
||
/**
|
||
* Check if given character code is a space character
|
||
*/
|
||
function isSpace(code) {
|
||
return isWhiteSpace$3(code)
|
||
|| code === 10 /* LF */
|
||
|| code === 13; /* CR */
|
||
}
|
||
/**
|
||
* Check if given character code is a quote character
|
||
*/
|
||
function isQuote$2(code) {
|
||
return code === 39 /* ' */ || code === 34 /* " */;
|
||
}
|
||
|
||
/**
|
||
* A streaming, character code-based string reader
|
||
*/
|
||
class Scanner {
|
||
constructor(str, start, end) {
|
||
if (end == null && typeof str === 'string') {
|
||
end = str.length;
|
||
}
|
||
this.string = str;
|
||
this.pos = this.start = start || 0;
|
||
this.end = end || 0;
|
||
}
|
||
/**
|
||
* Returns true only if the stream is at the end of the file.
|
||
*/
|
||
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
|
||
*/
|
||
limit(start, end) {
|
||
return new Scanner(this.string, start, end);
|
||
}
|
||
/**
|
||
* Returns the next character code in the stream without advancing it.
|
||
* Will return NaN at the end of the file.
|
||
*/
|
||
peek() {
|
||
return this.string.charCodeAt(this.pos);
|
||
}
|
||
/**
|
||
* Returns the next character in the stream and advances it.
|
||
* Also returns <code>undefined</code> when no more characters are available.
|
||
*/
|
||
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.
|
||
*/
|
||
eat(match) {
|
||
const ch = this.peek();
|
||
const ok = typeof match === 'function' ? match(ch) : ch === match;
|
||
if (ok) {
|
||
this.next();
|
||
}
|
||
return ok;
|
||
}
|
||
/**
|
||
* Repeatedly calls <code>eat</code> with the given argument, until it
|
||
* fails. Returns <code>true</code> if any characters were eaten.
|
||
*/
|
||
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.
|
||
*/
|
||
backUp(n) {
|
||
this.pos -= (n || 1);
|
||
}
|
||
/**
|
||
* Get the string between the start of the current token and the
|
||
* current stream position.
|
||
*/
|
||
current() {
|
||
return this.substring(this.start, this.pos);
|
||
}
|
||
/**
|
||
* Returns substring for given range
|
||
*/
|
||
substring(start, end) {
|
||
return this.string.slice(start, end);
|
||
}
|
||
/**
|
||
* Creates error object with current stream state
|
||
*/
|
||
error(message, pos = this.pos) {
|
||
return new ScannerError(`${message} at ${pos + 1}`, pos, this.string);
|
||
}
|
||
}
|
||
class ScannerError extends Error {
|
||
constructor(message, pos, str) {
|
||
super(message);
|
||
this.pos = pos;
|
||
this.string = str;
|
||
}
|
||
}
|
||
|
||
function tokenScanner$1(tokens) {
|
||
return {
|
||
tokens,
|
||
start: 0,
|
||
pos: 0,
|
||
size: tokens.length
|
||
};
|
||
}
|
||
function peek$3(scanner) {
|
||
return scanner.tokens[scanner.pos];
|
||
}
|
||
function next(scanner) {
|
||
return scanner.tokens[scanner.pos++];
|
||
}
|
||
function slice(scanner, from = scanner.start, to = scanner.pos) {
|
||
return scanner.tokens.slice(from, to);
|
||
}
|
||
function readable$1(scanner) {
|
||
return scanner.pos < scanner.size;
|
||
}
|
||
function consume$2(scanner, test) {
|
||
const token = peek$3(scanner);
|
||
if (token && test(token)) {
|
||
scanner.pos++;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function error$1(scanner, message, token = peek$3(scanner)) {
|
||
if (token && token.start != null) {
|
||
message += ` at ${token.start}`;
|
||
}
|
||
const err = new Error(message);
|
||
err['pos'] = token && token.start;
|
||
return err;
|
||
}
|
||
|
||
function abbreviation(abbr, options = {}) {
|
||
const scanner = tokenScanner$1(abbr);
|
||
const result = statements(scanner, options);
|
||
if (readable$1(scanner)) {
|
||
throw error$1(scanner, 'Unexpected character');
|
||
}
|
||
return result;
|
||
}
|
||
function statements(scanner, options) {
|
||
const result = {
|
||
type: 'TokenGroup',
|
||
elements: []
|
||
};
|
||
let ctx = result;
|
||
let node;
|
||
const stack = [];
|
||
while (readable$1(scanner)) {
|
||
if (node = element$2(scanner, options) || group(scanner, options)) {
|
||
ctx.elements.push(node);
|
||
if (consume$2(scanner, isChildOperator)) {
|
||
stack.push(ctx);
|
||
ctx = node;
|
||
}
|
||
else if (consume$2(scanner, isSiblingOperator$1)) {
|
||
continue;
|
||
}
|
||
else if (consume$2(scanner, isClimbOperator)) {
|
||
do {
|
||
if (stack.length) {
|
||
ctx = stack.pop();
|
||
}
|
||
} while (consume$2(scanner, isClimbOperator));
|
||
}
|
||
}
|
||
else {
|
||
break;
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
/**
|
||
* Consumes group from given scanner
|
||
*/
|
||
function group(scanner, options) {
|
||
if (consume$2(scanner, isGroupStart)) {
|
||
const result = statements(scanner, options);
|
||
const token = next(scanner);
|
||
if (isBracket$2(token, 'group', false)) {
|
||
result.repeat = repeater$1(scanner);
|
||
}
|
||
return result;
|
||
}
|
||
}
|
||
/**
|
||
* Consumes single element from given scanner
|
||
*/
|
||
function element$2(scanner, options) {
|
||
let attr;
|
||
const elem = {
|
||
type: 'TokenElement',
|
||
name: void 0,
|
||
attributes: void 0,
|
||
value: void 0,
|
||
repeat: void 0,
|
||
selfClose: false,
|
||
elements: []
|
||
};
|
||
if (elementName(scanner, options)) {
|
||
elem.name = slice(scanner);
|
||
}
|
||
while (readable$1(scanner)) {
|
||
scanner.start = scanner.pos;
|
||
if (!elem.repeat && !isEmpty(elem) && consume$2(scanner, isRepeater)) {
|
||
elem.repeat = scanner.tokens[scanner.pos - 1];
|
||
}
|
||
else if (!elem.value && text(scanner)) {
|
||
elem.value = getText(scanner);
|
||
}
|
||
else if (attr = shortAttribute(scanner, 'id', options) || shortAttribute(scanner, 'class', options) || attributeSet(scanner)) {
|
||
if (!elem.attributes) {
|
||
elem.attributes = Array.isArray(attr) ? attr.slice() : [attr];
|
||
}
|
||
else {
|
||
elem.attributes = elem.attributes.concat(attr);
|
||
}
|
||
}
|
||
else {
|
||
if (!isEmpty(elem) && consume$2(scanner, isCloseOperator)) {
|
||
elem.selfClose = true;
|
||
if (!elem.repeat && consume$2(scanner, isRepeater)) {
|
||
elem.repeat = scanner.tokens[scanner.pos - 1];
|
||
}
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
return !isEmpty(elem) ? elem : void 0;
|
||
}
|
||
/**
|
||
* Consumes attribute set from given scanner
|
||
*/
|
||
function attributeSet(scanner) {
|
||
if (consume$2(scanner, isAttributeSetStart)) {
|
||
const attributes = [];
|
||
let attr;
|
||
while (readable$1(scanner)) {
|
||
if (attr = attribute(scanner)) {
|
||
attributes.push(attr);
|
||
}
|
||
else if (consume$2(scanner, isAttributeSetEnd)) {
|
||
break;
|
||
}
|
||
else if (!consume$2(scanner, isWhiteSpace$2)) {
|
||
throw error$1(scanner, `Unexpected "${peek$3(scanner).type}" token`);
|
||
}
|
||
}
|
||
return attributes;
|
||
}
|
||
}
|
||
/**
|
||
* Consumes attribute shorthand (class or id) from given scanner
|
||
*/
|
||
function shortAttribute(scanner, type, options) {
|
||
if (isOperator$1(peek$3(scanner), type)) {
|
||
scanner.pos++;
|
||
// Consume multiple operators
|
||
let count = 1;
|
||
while (isOperator$1(peek$3(scanner), type)) {
|
||
scanner.pos++;
|
||
count++;
|
||
}
|
||
const attr = {
|
||
name: [createLiteral$1(type)]
|
||
};
|
||
if (count > 1) {
|
||
attr.multiple = true;
|
||
}
|
||
// Consume expression after shorthand start for React-like components
|
||
if (options.jsx && text(scanner)) {
|
||
attr.value = getText(scanner);
|
||
attr.expression = true;
|
||
}
|
||
else {
|
||
attr.value = literal$1$1(scanner) ? slice(scanner) : void 0;
|
||
}
|
||
return attr;
|
||
}
|
||
}
|
||
/**
|
||
* Consumes single attribute from given scanner
|
||
*/
|
||
function attribute(scanner) {
|
||
if (quoted(scanner)) {
|
||
// Consumed quoted value: it’s a value for default attribute
|
||
return {
|
||
value: slice(scanner)
|
||
};
|
||
}
|
||
if (literal$1$1(scanner, true)) {
|
||
const name = slice(scanner);
|
||
let value;
|
||
if (consume$2(scanner, isEquals)) {
|
||
if (quoted(scanner) || literal$1$1(scanner, true)) {
|
||
value = slice(scanner);
|
||
}
|
||
}
|
||
return { name, value };
|
||
}
|
||
}
|
||
function repeater$1(scanner) {
|
||
return isRepeater(peek$3(scanner))
|
||
? scanner.tokens[scanner.pos++]
|
||
: void 0;
|
||
}
|
||
/**
|
||
* Consumes quoted value from given scanner, if possible
|
||
*/
|
||
function quoted(scanner) {
|
||
const start = scanner.pos;
|
||
const quote = peek$3(scanner);
|
||
if (isQuote$1(quote)) {
|
||
scanner.pos++;
|
||
while (readable$1(scanner)) {
|
||
if (isQuote$1(next(scanner), quote.single)) {
|
||
scanner.start = start;
|
||
return true;
|
||
}
|
||
}
|
||
throw error$1(scanner, 'Unclosed quote', quote);
|
||
}
|
||
return false;
|
||
}
|
||
/**
|
||
* Consumes literal (unquoted value) from given scanner
|
||
*/
|
||
function literal$1$1(scanner, allowBrackets) {
|
||
const start = scanner.pos;
|
||
const brackets = {
|
||
attribute: 0,
|
||
expression: 0,
|
||
group: 0
|
||
};
|
||
while (readable$1(scanner)) {
|
||
const token = peek$3(scanner);
|
||
if (brackets.expression) {
|
||
// If we’re inside expression, we should consume all content in it
|
||
if (isBracket$2(token, 'expression')) {
|
||
brackets[token.context] += token.open ? 1 : -1;
|
||
}
|
||
}
|
||
else if (isQuote$1(token) || isOperator$1(token) || isWhiteSpace$2(token) || isRepeater(token)) {
|
||
break;
|
||
}
|
||
else if (isBracket$2(token)) {
|
||
if (!allowBrackets) {
|
||
break;
|
||
}
|
||
if (token.open) {
|
||
brackets[token.context]++;
|
||
}
|
||
else if (!brackets[token.context]) {
|
||
// Stop if found unmatched closing brace: it must be handled
|
||
// by parent consumer
|
||
break;
|
||
}
|
||
else {
|
||
brackets[token.context]--;
|
||
}
|
||
}
|
||
scanner.pos++;
|
||
}
|
||
if (start !== scanner.pos) {
|
||
scanner.start = start;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
/**
|
||
* Consumes element name from given scanner
|
||
*/
|
||
function elementName(scanner, options) {
|
||
const start = scanner.pos;
|
||
if (options.jsx && consume$2(scanner, isCapitalizedLiteral)) {
|
||
// Check for edge case: consume immediate capitalized class names
|
||
// for React-like components, e.g. `Foo.Bar.Baz`
|
||
while (readable$1(scanner)) {
|
||
const { pos } = scanner;
|
||
if (!consume$2(scanner, isClassNameOperator) || !consume$2(scanner, isCapitalizedLiteral)) {
|
||
scanner.pos = pos;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
while (readable$1(scanner) && consume$2(scanner, isElementName$1)) {
|
||
// empty
|
||
}
|
||
if (scanner.pos !== start) {
|
||
scanner.start = start;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
/**
|
||
* Consumes text value from given scanner
|
||
*/
|
||
function text(scanner) {
|
||
const start = scanner.pos;
|
||
if (consume$2(scanner, isTextStart)) {
|
||
let brackets = 0;
|
||
while (readable$1(scanner)) {
|
||
const token = next(scanner);
|
||
if (isBracket$2(token, 'expression')) {
|
||
if (token.open) {
|
||
brackets++;
|
||
}
|
||
else if (!brackets) {
|
||
break;
|
||
}
|
||
else {
|
||
brackets--;
|
||
}
|
||
}
|
||
}
|
||
scanner.start = start;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function getText(scanner) {
|
||
let from = scanner.start;
|
||
let to = scanner.pos;
|
||
if (isBracket$2(scanner.tokens[from], 'expression', true)) {
|
||
from++;
|
||
}
|
||
if (isBracket$2(scanner.tokens[to - 1], 'expression', false)) {
|
||
to--;
|
||
}
|
||
return slice(scanner, from, to);
|
||
}
|
||
function isBracket$2(token, context, isOpen) {
|
||
return Boolean(token && token.type === 'Bracket'
|
||
&& (!context || token.context === context)
|
||
&& (isOpen == null || token.open === isOpen));
|
||
}
|
||
function isOperator$1(token, type) {
|
||
return Boolean(token && token.type === 'Operator' && (!type || token.operator === type));
|
||
}
|
||
function isQuote$1(token, isSingle) {
|
||
return Boolean(token && token.type === 'Quote' && (isSingle == null || token.single === isSingle));
|
||
}
|
||
function isWhiteSpace$2(token) {
|
||
return Boolean(token && token.type === 'WhiteSpace');
|
||
}
|
||
function isEquals(token) {
|
||
return isOperator$1(token, 'equal');
|
||
}
|
||
function isRepeater(token) {
|
||
return Boolean(token && token.type === 'Repeater');
|
||
}
|
||
function isLiteral$2(token) {
|
||
return token.type === 'Literal';
|
||
}
|
||
function isCapitalizedLiteral(token) {
|
||
if (isLiteral$2(token)) {
|
||
const ch = token.value.charCodeAt(0);
|
||
return ch >= 65 && ch <= 90;
|
||
}
|
||
return false;
|
||
}
|
||
function isElementName$1(token) {
|
||
return token.type === 'Literal' || token.type === 'RepeaterNumber' || token.type === 'RepeaterPlaceholder';
|
||
}
|
||
function isClassNameOperator(token) {
|
||
return isOperator$1(token, 'class');
|
||
}
|
||
function isAttributeSetStart(token) {
|
||
return isBracket$2(token, 'attribute', true);
|
||
}
|
||
function isAttributeSetEnd(token) {
|
||
return isBracket$2(token, 'attribute', false);
|
||
}
|
||
function isTextStart(token) {
|
||
return isBracket$2(token, 'expression', true);
|
||
}
|
||
function isGroupStart(token) {
|
||
return isBracket$2(token, 'group', true);
|
||
}
|
||
function createLiteral$1(value) {
|
||
return { type: 'Literal', value };
|
||
}
|
||
function isEmpty(elem) {
|
||
return !elem.name && !elem.value && !elem.attributes;
|
||
}
|
||
function isChildOperator(token) {
|
||
return isOperator$1(token, 'child');
|
||
}
|
||
function isSiblingOperator$1(token) {
|
||
return isOperator$1(token, 'sibling');
|
||
}
|
||
function isClimbOperator(token) {
|
||
return isOperator$1(token, 'climb');
|
||
}
|
||
function isCloseOperator(token) {
|
||
return isOperator$1(token, 'close');
|
||
}
|
||
|
||
var Chars$3;
|
||
(function (Chars) {
|
||
/** `{` character */
|
||
Chars[Chars["CurlyBracketOpen"] = 123] = "CurlyBracketOpen";
|
||
/** `}` character */
|
||
Chars[Chars["CurlyBracketClose"] = 125] = "CurlyBracketClose";
|
||
/** `\\` character */
|
||
Chars[Chars["Escape"] = 92] = "Escape";
|
||
/** `=` character */
|
||
Chars[Chars["Equals"] = 61] = "Equals";
|
||
/** `[` character */
|
||
Chars[Chars["SquareBracketOpen"] = 91] = "SquareBracketOpen";
|
||
/** `]` character */
|
||
Chars[Chars["SquareBracketClose"] = 93] = "SquareBracketClose";
|
||
/** `*` character */
|
||
Chars[Chars["Asterisk"] = 42] = "Asterisk";
|
||
/** `#` 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["Slash"] = 47] = "Slash";
|
||
/** `:` character */
|
||
Chars[Chars["Colon"] = 58] = "Colon";
|
||
/** `!` character */
|
||
Chars[Chars["Excl"] = 33] = "Excl";
|
||
/** `@` character */
|
||
Chars[Chars["At"] = 64] = "At";
|
||
/** `_` character */
|
||
Chars[Chars["Underscore"] = 95] = "Underscore";
|
||
/** `(` character */
|
||
Chars[Chars["RoundBracketOpen"] = 40] = "RoundBracketOpen";
|
||
/** `)` character */
|
||
Chars[Chars["RoundBracketClose"] = 41] = "RoundBracketClose";
|
||
/** `+` character */
|
||
Chars[Chars["Sibling"] = 43] = "Sibling";
|
||
/** `>` character */
|
||
Chars[Chars["Child"] = 62] = "Child";
|
||
/** `^` character */
|
||
Chars[Chars["Climb"] = 94] = "Climb";
|
||
/** `'` character */
|
||
Chars[Chars["SingleQuote"] = 39] = "SingleQuote";
|
||
/** `""` character */
|
||
Chars[Chars["DoubleQuote"] = 34] = "DoubleQuote";
|
||
})(Chars$3 || (Chars$3 = {}));
|
||
/**
|
||
* If consumes escape character, sets current stream range to escaped value
|
||
*/
|
||
function escaped(scanner) {
|
||
if (scanner.eat(Chars$3.Escape)) {
|
||
scanner.start = scanner.pos;
|
||
if (!scanner.eof()) {
|
||
scanner.pos++;
|
||
}
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function tokenize$1(source) {
|
||
const scanner = new Scanner(source);
|
||
const result = [];
|
||
const ctx = {
|
||
group: 0,
|
||
attribute: 0,
|
||
expression: 0,
|
||
quote: 0
|
||
};
|
||
let ch = 0;
|
||
let token;
|
||
while (!scanner.eof()) {
|
||
ch = scanner.peek();
|
||
token = getToken$1(scanner, ctx);
|
||
if (token) {
|
||
result.push(token);
|
||
if (token.type === 'Quote') {
|
||
ctx.quote = ch === ctx.quote ? 0 : ch;
|
||
}
|
||
else if (token.type === 'Bracket') {
|
||
ctx[token.context] += token.open ? 1 : -1;
|
||
}
|
||
}
|
||
else {
|
||
throw scanner.error('Unexpected character');
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
/**
|
||
* Returns next token from given scanner, if possible
|
||
*/
|
||
function getToken$1(scanner, ctx) {
|
||
return field$2(scanner, ctx)
|
||
|| repeaterPlaceholder(scanner)
|
||
|| repeaterNumber(scanner)
|
||
|| repeater(scanner)
|
||
|| whiteSpace$1(scanner)
|
||
|| literal$2(scanner, ctx)
|
||
|| operator$1(scanner)
|
||
|| quote(scanner)
|
||
|| bracket$1(scanner);
|
||
}
|
||
/**
|
||
* Consumes literal from given scanner
|
||
*/
|
||
function literal$2(scanner, ctx) {
|
||
const start = scanner.pos;
|
||
const expressionStart = ctx.expression;
|
||
let value = '';
|
||
while (!scanner.eof()) {
|
||
// Consume escaped sequence no matter of context
|
||
if (escaped(scanner)) {
|
||
value += scanner.current();
|
||
continue;
|
||
}
|
||
const ch = scanner.peek();
|
||
if (ch === Chars$3.Slash && !ctx.quote && !ctx.expression && !ctx.attribute) {
|
||
// Special case for `/` character between numbers in class names
|
||
const prev = scanner.string.charCodeAt(scanner.pos - 1);
|
||
const next = scanner.string.charCodeAt(scanner.pos + 1);
|
||
if (isNumber$1(prev) && isNumber$1(next)) {
|
||
value += scanner.string[scanner.pos++];
|
||
continue;
|
||
}
|
||
}
|
||
if (ch === ctx.quote || ch === Chars$3.Dollar || isAllowedOperator(ch, ctx)) {
|
||
// 1. Found matching quote
|
||
// 2. The `$` character has special meaning in every context
|
||
// 3. Depending on context, some characters should be treated as operators
|
||
break;
|
||
}
|
||
if (expressionStart) {
|
||
// Consume nested expressions, e.g. span{{foo}}
|
||
if (ch === Chars$3.CurlyBracketOpen) {
|
||
ctx.expression++;
|
||
}
|
||
else if (ch === Chars$3.CurlyBracketClose) {
|
||
if (ctx.expression > expressionStart) {
|
||
ctx.expression--;
|
||
}
|
||
else {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
else if (!ctx.quote) {
|
||
// Consuming element name
|
||
if (!ctx.attribute && !isElementName(ch)) {
|
||
break;
|
||
}
|
||
if (isAllowedSpace(ch, ctx) || isAllowedRepeater(ch, ctx) || isQuote$2(ch) || bracketType(ch)) {
|
||
// Stop for characters not allowed in unquoted literal
|
||
break;
|
||
}
|
||
}
|
||
value += scanner.string[scanner.pos++];
|
||
}
|
||
if (start !== scanner.pos) {
|
||
scanner.start = start;
|
||
return {
|
||
type: 'Literal',
|
||
value,
|
||
start,
|
||
end: scanner.pos
|
||
};
|
||
}
|
||
}
|
||
/**
|
||
* Consumes white space characters as string literal from given scanner
|
||
*/
|
||
function whiteSpace$1(scanner) {
|
||
const start = scanner.pos;
|
||
if (scanner.eatWhile(isSpace)) {
|
||
return {
|
||
type: 'WhiteSpace',
|
||
start,
|
||
end: scanner.pos,
|
||
value: scanner.substring(start, scanner.pos)
|
||
};
|
||
}
|
||
}
|
||
/**
|
||
* Consumes quote from given scanner
|
||
*/
|
||
function quote(scanner) {
|
||
const ch = scanner.peek();
|
||
if (isQuote$2(ch)) {
|
||
return {
|
||
type: 'Quote',
|
||
single: ch === Chars$3.SingleQuote,
|
||
start: scanner.pos++,
|
||
end: scanner.pos
|
||
};
|
||
}
|
||
}
|
||
/**
|
||
* Consumes bracket from given scanner
|
||
*/
|
||
function bracket$1(scanner) {
|
||
const ch = scanner.peek();
|
||
const context = bracketType(ch);
|
||
if (context) {
|
||
return {
|
||
type: 'Bracket',
|
||
open: isOpenBracket$2(ch),
|
||
context,
|
||
start: scanner.pos++,
|
||
end: scanner.pos
|
||
};
|
||
}
|
||
}
|
||
/**
|
||
* Consumes operator from given scanner
|
||
*/
|
||
function operator$1(scanner) {
|
||
const op = operatorType$1(scanner.peek());
|
||
if (op) {
|
||
return {
|
||
type: 'Operator',
|
||
operator: op,
|
||
start: scanner.pos++,
|
||
end: scanner.pos
|
||
};
|
||
}
|
||
}
|
||
/**
|
||
* Consumes node repeat token from current stream position and returns its
|
||
* parsed value
|
||
*/
|
||
function repeater(scanner) {
|
||
const start = scanner.pos;
|
||
if (scanner.eat(Chars$3.Asterisk)) {
|
||
scanner.start = scanner.pos;
|
||
let count = 1;
|
||
let implicit = false;
|
||
if (scanner.eatWhile(isNumber$1)) {
|
||
count = Number(scanner.current());
|
||
}
|
||
else {
|
||
implicit = true;
|
||
}
|
||
return {
|
||
type: 'Repeater',
|
||
count,
|
||
value: 0,
|
||
implicit,
|
||
start,
|
||
end: scanner.pos
|
||
};
|
||
}
|
||
}
|
||
/**
|
||
* Consumes repeater placeholder `$#` from given scanner
|
||
*/
|
||
function repeaterPlaceholder(scanner) {
|
||
const start = scanner.pos;
|
||
if (scanner.eat(Chars$3.Dollar) && scanner.eat(Chars$3.Hash)) {
|
||
return {
|
||
type: 'RepeaterPlaceholder',
|
||
value: void 0,
|
||
start,
|
||
end: scanner.pos
|
||
};
|
||
}
|
||
scanner.pos = start;
|
||
}
|
||
/**
|
||
* Consumes numbering token like `$` from given scanner state
|
||
*/
|
||
function repeaterNumber(scanner) {
|
||
const start = scanner.pos;
|
||
if (scanner.eatWhile(Chars$3.Dollar)) {
|
||
const size = scanner.pos - start;
|
||
let reverse = false;
|
||
let base = 1;
|
||
let parent = 0;
|
||
if (scanner.eat(Chars$3.At)) {
|
||
// Consume numbering modifiers
|
||
while (scanner.eat(Chars$3.Climb)) {
|
||
parent++;
|
||
}
|
||
reverse = scanner.eat(Chars$3.Dash);
|
||
scanner.start = scanner.pos;
|
||
if (scanner.eatWhile(isNumber$1)) {
|
||
base = Number(scanner.current());
|
||
}
|
||
}
|
||
scanner.start = start;
|
||
return {
|
||
type: 'RepeaterNumber',
|
||
size,
|
||
reverse,
|
||
base,
|
||
parent,
|
||
start,
|
||
end: scanner.pos
|
||
};
|
||
}
|
||
}
|
||
function field$2(scanner, ctx) {
|
||
const start = scanner.pos;
|
||
// Fields are allowed inside expressions and attributes
|
||
if ((ctx.expression || ctx.attribute) && scanner.eat(Chars$3.Dollar) && scanner.eat(Chars$3.CurlyBracketOpen)) {
|
||
scanner.start = scanner.pos;
|
||
let index;
|
||
let name = '';
|
||
if (scanner.eatWhile(isNumber$1)) {
|
||
// It’s a field
|
||
index = Number(scanner.current());
|
||
name = scanner.eat(Chars$3.Colon) ? consumePlaceholder$2(scanner) : '';
|
||
}
|
||
else if (isAlpha$1(scanner.peek())) {
|
||
// It’s a variable
|
||
name = consumePlaceholder$2(scanner);
|
||
}
|
||
if (scanner.eat(Chars$3.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$2(stream) {
|
||
const stack = [];
|
||
stream.start = stream.pos;
|
||
while (!stream.eof()) {
|
||
if (stream.eat(Chars$3.CurlyBracketOpen)) {
|
||
stack.push(stream.pos);
|
||
}
|
||
else if (stream.eat(Chars$3.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();
|
||
}
|
||
/**
|
||
* Check if given character code is an operator and it’s allowed in current context
|
||
*/
|
||
function isAllowedOperator(ch, ctx) {
|
||
const op = operatorType$1(ch);
|
||
if (!op || ctx.quote || ctx.expression) {
|
||
// No operators inside quoted values or expressions
|
||
return false;
|
||
}
|
||
// Inside attributes, only `equals` is allowed
|
||
return !ctx.attribute || op === 'equal';
|
||
}
|
||
/**
|
||
* Check if given character is a space character and is allowed to be consumed
|
||
* as a space token in current context
|
||
*/
|
||
function isAllowedSpace(ch, ctx) {
|
||
return isSpace(ch) && !ctx.expression;
|
||
}
|
||
/**
|
||
* Check if given character can be consumed as repeater in current context
|
||
*/
|
||
function isAllowedRepeater(ch, ctx) {
|
||
return ch === Chars$3.Asterisk && !ctx.attribute && !ctx.expression;
|
||
}
|
||
/**
|
||
* If given character is a bracket, returns it’s type
|
||
*/
|
||
function bracketType(ch) {
|
||
if (ch === Chars$3.RoundBracketOpen || ch === Chars$3.RoundBracketClose) {
|
||
return 'group';
|
||
}
|
||
if (ch === Chars$3.SquareBracketOpen || ch === Chars$3.SquareBracketClose) {
|
||
return 'attribute';
|
||
}
|
||
if (ch === Chars$3.CurlyBracketOpen || ch === Chars$3.CurlyBracketClose) {
|
||
return 'expression';
|
||
}
|
||
}
|
||
/**
|
||
* If given character is an operator, returns it’s type
|
||
*/
|
||
function operatorType$1(ch) {
|
||
return (ch === Chars$3.Child && 'child')
|
||
|| (ch === Chars$3.Sibling && 'sibling')
|
||
|| (ch === Chars$3.Climb && 'climb')
|
||
|| (ch === Chars$3.Dot && 'class')
|
||
|| (ch === Chars$3.Hash && 'id')
|
||
|| (ch === Chars$3.Slash && 'close')
|
||
|| (ch === Chars$3.Equals && 'equal')
|
||
|| void 0;
|
||
}
|
||
/**
|
||
* Check if given character is an open bracket
|
||
*/
|
||
function isOpenBracket$2(ch) {
|
||
return ch === Chars$3.CurlyBracketOpen
|
||
|| ch === Chars$3.SquareBracketOpen
|
||
|| ch === Chars$3.RoundBracketOpen;
|
||
}
|
||
/**
|
||
* Check if given character is allowed in element name
|
||
*/
|
||
function isElementName(ch) {
|
||
return isAlphaNumericWord(ch)
|
||
|| isUmlaut(ch)
|
||
|| ch === Chars$3.Dash
|
||
|| ch === Chars$3.Colon
|
||
|| ch === Chars$3.Excl;
|
||
}
|
||
|
||
const operators = {
|
||
child: '>',
|
||
class: '.',
|
||
climb: '^',
|
||
id: '#',
|
||
equal: '=',
|
||
close: '/',
|
||
sibling: '+'
|
||
};
|
||
const tokenVisitor = {
|
||
Literal(token) {
|
||
return token.value;
|
||
},
|
||
Quote(token) {
|
||
return token.single ? '\'' : '"';
|
||
},
|
||
Bracket(token) {
|
||
if (token.context === 'attribute') {
|
||
return token.open ? '[' : ']';
|
||
}
|
||
else if (token.context === 'expression') {
|
||
return token.open ? '{' : '}';
|
||
}
|
||
else {
|
||
return token.open ? '(' : '}';
|
||
}
|
||
},
|
||
Operator(token) {
|
||
return operators[token.operator];
|
||
},
|
||
Field(token, state) {
|
||
if (token.index != null) {
|
||
// It’s a field: by default, return TextMate-compatible field
|
||
return token.name
|
||
? `\${${token.index}:${token.name}}`
|
||
: `\${${token.index}`;
|
||
}
|
||
else if (token.name) {
|
||
// It’s a variable
|
||
return state.getVariable(token.name);
|
||
}
|
||
return '';
|
||
},
|
||
RepeaterPlaceholder(token, state) {
|
||
// Find closest implicit repeater
|
||
let repeater;
|
||
for (let i = state.repeaters.length - 1; i >= 0; i--) {
|
||
if (state.repeaters[i].implicit) {
|
||
repeater = state.repeaters[i];
|
||
break;
|
||
}
|
||
}
|
||
state.inserted = true;
|
||
return state.getText(repeater && repeater.value);
|
||
},
|
||
RepeaterNumber(token, state) {
|
||
let value = 1;
|
||
const lastIx = state.repeaters.length - 1;
|
||
// const repeaterIx = Math.max(0, state.repeaters.length - 1 - token.parent);
|
||
const repeater = state.repeaters[lastIx];
|
||
if (repeater) {
|
||
value = token.reverse
|
||
? token.base + repeater.count - repeater.value - 1
|
||
: token.base + repeater.value;
|
||
if (token.parent) {
|
||
const parentIx = Math.max(0, lastIx - token.parent);
|
||
if (parentIx !== lastIx) {
|
||
const parentRepeater = state.repeaters[parentIx];
|
||
value += repeater.count * parentRepeater.value;
|
||
}
|
||
}
|
||
}
|
||
let result = String(value);
|
||
while (result.length < token.size) {
|
||
result = '0' + result;
|
||
}
|
||
return result;
|
||
},
|
||
WhiteSpace(token) {
|
||
return token.value;
|
||
}
|
||
};
|
||
/**
|
||
* Converts given value token to string
|
||
*/
|
||
function stringify$1(token, state) {
|
||
if (!tokenVisitor[token.type]) {
|
||
throw new Error(`Unknown token ${token.type}`);
|
||
}
|
||
return tokenVisitor[token.type](token, state);
|
||
}
|
||
|
||
const urlRegex = /^((https?:|ftp:|file:)?\/\/|(www|ftp)\.)[^ ]*$/;
|
||
const emailRegex = /^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,5}$/;
|
||
/**
|
||
* Converts given token-based abbreviation into simplified and unrolled node-based
|
||
* abbreviation
|
||
*/
|
||
function convert(abbr, options = {}) {
|
||
let textInserted = false;
|
||
let cleanText;
|
||
if (options.text) {
|
||
if (Array.isArray(options.text)) {
|
||
cleanText = options.text.filter(s => s.trim());
|
||
}
|
||
else {
|
||
cleanText = options.text;
|
||
}
|
||
}
|
||
const result = {
|
||
type: 'Abbreviation',
|
||
children: convertGroup(abbr, {
|
||
inserted: false,
|
||
repeaters: [],
|
||
text: options.text,
|
||
cleanText,
|
||
repeatGuard: options.maxRepeat || Number.POSITIVE_INFINITY,
|
||
getText(pos) {
|
||
var _a;
|
||
textInserted = true;
|
||
let value;
|
||
if (Array.isArray(options.text)) {
|
||
if (pos !== undefined && pos >= 0 && pos < cleanText.length) {
|
||
return cleanText[pos];
|
||
}
|
||
value = pos !== undefined ? options.text[pos] : options.text.join('\n');
|
||
}
|
||
else {
|
||
value = (_a = options.text) !== null && _a !== void 0 ? _a : '';
|
||
}
|
||
return value;
|
||
},
|
||
getVariable(name) {
|
||
const varValue = options.variables && options.variables[name];
|
||
return varValue != null ? varValue : name;
|
||
}
|
||
})
|
||
};
|
||
if (options.text != null && !textInserted) {
|
||
// Text given but no implicitly repeated elements: insert it into
|
||
// deepest child
|
||
const deepest = deepestNode(last$1(result.children));
|
||
if (deepest) {
|
||
const text = Array.isArray(options.text) ? options.text.join('\n') : options.text;
|
||
insertText(deepest, text);
|
||
if (deepest.name === 'a' && options.href) {
|
||
// Automatically update value of `<a>` element if inserting URL or email
|
||
insertHref(deepest, text);
|
||
}
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
/**
|
||
* Converts given statement to abbreviation nodes
|
||
*/
|
||
function convertStatement(node, state) {
|
||
let result = [];
|
||
if (node.repeat) {
|
||
// Node is repeated: we should create copies of given node
|
||
// and supply context token with actual repeater state
|
||
const original = node.repeat;
|
||
const repeat = Object.assign({}, original);
|
||
repeat.count = repeat.implicit && Array.isArray(state.text)
|
||
? state.cleanText.length
|
||
: (repeat.count || 1);
|
||
let items;
|
||
state.repeaters.push(repeat);
|
||
for (let i = 0; i < repeat.count; i++) {
|
||
repeat.value = i;
|
||
node.repeat = repeat;
|
||
items = isGroup(node)
|
||
? convertGroup(node, state)
|
||
: convertElement(node, state);
|
||
if (repeat.implicit && !state.inserted) {
|
||
// It’s an implicit repeater but no repeater placeholders found inside,
|
||
// we should insert text into deepest node
|
||
const target = last$1(items);
|
||
const deepest = target && deepestNode(target);
|
||
if (deepest) {
|
||
insertText(deepest, state.getText(repeat.value));
|
||
}
|
||
}
|
||
result = result.concat(items);
|
||
// We should output at least one repeated item even if it’s reached
|
||
// repeat limit
|
||
if (--state.repeatGuard <= 0) {
|
||
break;
|
||
}
|
||
}
|
||
state.repeaters.pop();
|
||
node.repeat = original;
|
||
if (repeat.implicit) {
|
||
state.inserted = true;
|
||
}
|
||
}
|
||
else {
|
||
result = result.concat(isGroup(node) ? convertGroup(node, state) : convertElement(node, state));
|
||
}
|
||
return result;
|
||
}
|
||
function convertElement(node, state) {
|
||
let children = [];
|
||
const elem = {
|
||
type: 'AbbreviationNode',
|
||
name: node.name && stringifyName(node.name, state),
|
||
value: node.value && stringifyValue$1(node.value, state),
|
||
attributes: void 0,
|
||
children,
|
||
repeat: node.repeat && Object.assign({}, node.repeat),
|
||
selfClosing: node.selfClose,
|
||
};
|
||
let result = [elem];
|
||
for (const child of node.elements) {
|
||
children = children.concat(convertStatement(child, state));
|
||
}
|
||
if (node.attributes) {
|
||
elem.attributes = [];
|
||
for (const attr of node.attributes) {
|
||
elem.attributes.push(convertAttribute(attr, state));
|
||
}
|
||
}
|
||
// In case if current node is a text-only snippet without fields, we should
|
||
// put all children as siblings
|
||
if (!elem.name && !elem.attributes && elem.value && !elem.value.some(isField$1)) {
|
||
// XXX it’s unclear that `children` is not bound to `elem`
|
||
// due to concat operation
|
||
result = result.concat(children);
|
||
}
|
||
else {
|
||
elem.children = children;
|
||
}
|
||
return result;
|
||
}
|
||
function convertGroup(node, state) {
|
||
let result = [];
|
||
for (const child of node.elements) {
|
||
result = result.concat(convertStatement(child, state));
|
||
}
|
||
if (node.repeat) {
|
||
result = attachRepeater(result, node.repeat);
|
||
}
|
||
return result;
|
||
}
|
||
function convertAttribute(node, state) {
|
||
let implied = false;
|
||
let isBoolean = false;
|
||
let valueType = node.expression ? 'expression' : 'raw';
|
||
let value;
|
||
const name = node.name && stringifyName(node.name, state);
|
||
if (name && name[0] === '!') {
|
||
implied = true;
|
||
}
|
||
if (name && name[name.length - 1] === '.') {
|
||
isBoolean = true;
|
||
}
|
||
if (node.value) {
|
||
const tokens = node.value.slice();
|
||
if (isQuote$1(tokens[0])) {
|
||
// It’s a quoted value: remove quotes from output but mark attribute
|
||
// value as quoted
|
||
const quote = tokens.shift();
|
||
if (tokens.length && last$1(tokens).type === quote.type) {
|
||
tokens.pop();
|
||
}
|
||
valueType = quote.single ? 'singleQuote' : 'doubleQuote';
|
||
}
|
||
else if (isBracket$2(tokens[0], 'expression', true)) {
|
||
// Value is expression: remove brackets but mark value type
|
||
valueType = 'expression';
|
||
tokens.shift();
|
||
if (isBracket$2(last$1(tokens), 'expression', false)) {
|
||
tokens.pop();
|
||
}
|
||
}
|
||
value = stringifyValue$1(tokens, state);
|
||
}
|
||
return {
|
||
name: isBoolean || implied
|
||
? name.slice(implied ? 1 : 0, isBoolean ? -1 : void 0)
|
||
: name,
|
||
value,
|
||
boolean: isBoolean,
|
||
implied,
|
||
valueType,
|
||
multiple: node.multiple
|
||
};
|
||
}
|
||
/**
|
||
* Converts given token list to string
|
||
*/
|
||
function stringifyName(tokens, state) {
|
||
let str = '';
|
||
for (let i = 0; i < tokens.length; i++) {
|
||
str += stringify$1(tokens[i], state);
|
||
}
|
||
return str;
|
||
}
|
||
/**
|
||
* Converts given token list to value list
|
||
*/
|
||
function stringifyValue$1(tokens, state) {
|
||
const result = [];
|
||
let str = '';
|
||
for (let i = 0, token; i < tokens.length; i++) {
|
||
token = tokens[i];
|
||
if (isField$1(token)) {
|
||
// We should keep original fields in output since some editors has their
|
||
// own syntax for field or doesn’t support fields at all so we should
|
||
// capture actual field location in output stream
|
||
if (str) {
|
||
result.push(str);
|
||
str = '';
|
||
}
|
||
result.push(token);
|
||
}
|
||
else {
|
||
str += stringify$1(token, state);
|
||
}
|
||
}
|
||
if (str) {
|
||
result.push(str);
|
||
}
|
||
return result;
|
||
}
|
||
function isGroup(node) {
|
||
return node.type === 'TokenGroup';
|
||
}
|
||
function isField$1(token) {
|
||
return typeof token === 'object' && token.type === 'Field' && token.index != null;
|
||
}
|
||
function last$1(arr) {
|
||
return arr[arr.length - 1];
|
||
}
|
||
function deepestNode(node) {
|
||
return node.children.length ? deepestNode(last$1(node.children)) : node;
|
||
}
|
||
function insertText(node, text) {
|
||
if (node.value) {
|
||
const lastToken = last$1(node.value);
|
||
if (typeof lastToken === 'string') {
|
||
node.value[node.value.length - 1] += text;
|
||
}
|
||
else {
|
||
node.value.push(text);
|
||
}
|
||
}
|
||
else {
|
||
node.value = [text];
|
||
}
|
||
}
|
||
function insertHref(node, text) {
|
||
var _a;
|
||
let href = '';
|
||
if (urlRegex.test(text)) {
|
||
href = text;
|
||
if (!/\w+:/.test(href) && !href.startsWith('//')) {
|
||
href = `http://${href}`;
|
||
}
|
||
}
|
||
else if (emailRegex.test(text)) {
|
||
href = `mailto:${text}`;
|
||
}
|
||
const hrefAttribute = (_a = node.attributes) === null || _a === void 0 ? void 0 : _a.find(attr => attr.name === 'href');
|
||
if (!hrefAttribute) {
|
||
if (!node.attributes) {
|
||
node.attributes = [];
|
||
}
|
||
node.attributes.push({ name: 'href', value: [href], valueType: 'doubleQuote' });
|
||
}
|
||
else if (!hrefAttribute.value) {
|
||
hrefAttribute.value = [href];
|
||
}
|
||
}
|
||
function attachRepeater(items, repeater) {
|
||
for (const item of items) {
|
||
if (!item.repeat) {
|
||
item.repeat = Object.assign({}, repeater);
|
||
}
|
||
}
|
||
return items;
|
||
}
|
||
|
||
/**
|
||
* Parses given abbreviation into node tree
|
||
*/
|
||
function parseAbbreviation(abbr, options) {
|
||
try {
|
||
const tokens = typeof abbr === 'string' ? tokenize$1(abbr) : abbr;
|
||
return convert(abbreviation(tokens, options), options);
|
||
}
|
||
catch (err) {
|
||
if (err instanceof ScannerError && typeof abbr === 'string') {
|
||
err.message += `\n${abbr}\n${'-'.repeat(err.pos)}^`;
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
var OperatorType;
|
||
(function (OperatorType) {
|
||
OperatorType["Sibling"] = "+";
|
||
OperatorType["Important"] = "!";
|
||
OperatorType["ArgumentDelimiter"] = ",";
|
||
OperatorType["ValueDelimiter"] = "-";
|
||
OperatorType["PropertyDelimiter"] = ":";
|
||
})(OperatorType || (OperatorType = {}));
|
||
|
||
var Chars$2;
|
||
(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$2 || (Chars$2 = {}));
|
||
|
||
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$1(scanner)
|
||
|| customProperty(scanner)
|
||
|| numberValue(scanner)
|
||
|| colorValue(scanner)
|
||
|| stringValue(scanner)
|
||
|| bracket(scanner)
|
||
|| operator(scanner)
|
||
|| whiteSpace(scanner)
|
||
|| literal$1(scanner, short);
|
||
}
|
||
function field$1(scanner) {
|
||
const start = scanner.pos;
|
||
if (scanner.eat(Chars$2.Dollar) && scanner.eat(Chars$2.CurlyBracketOpen)) {
|
||
scanner.start = scanner.pos;
|
||
let index;
|
||
let name = '';
|
||
if (scanner.eatWhile(isNumber$1)) {
|
||
// It’s a field
|
||
index = Number(scanner.current());
|
||
name = scanner.eat(Chars$2.Colon) ? consumePlaceholder$1(scanner) : '';
|
||
}
|
||
else if (isAlpha$1(scanner.peek())) {
|
||
// It’s a variable
|
||
name = consumePlaceholder$1(scanner);
|
||
}
|
||
if (scanner.eat(Chars$2.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$1(stream) {
|
||
const stack = [];
|
||
stream.start = stream.pos;
|
||
while (!stream.eof()) {
|
||
if (stream.eat(Chars$2.CurlyBracketOpen)) {
|
||
stack.push(stream.pos);
|
||
}
|
||
else if (stream.eat(Chars$2.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$1(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(isAlphaWord)) {
|
||
scanner.eatWhile(short ? isLiteral$1 : isKeyword);
|
||
}
|
||
else {
|
||
// Allow dots only at the beginning of literal
|
||
scanner.eat(Chars$2.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$2.Percent) || scanner.eatWhile(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 (isQuote$2(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$2.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$2.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$2.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$2.Dot)) {
|
||
scanner.start = start;
|
||
if (scanner.eatWhile(isNumber$1)) {
|
||
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(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$2.Dash) && scanner.eat(Chars$2.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$2.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$2.Dash);
|
||
const afterNegative = stream.pos;
|
||
const hasDecimal = stream.eatWhile(isNumber$1);
|
||
const prevPos = stream.pos;
|
||
if (stream.eat(Chars$2.Dot)) {
|
||
// It’s perfectly valid to have numbers like `1.`, which enforces
|
||
// value to float unit type
|
||
const hasFloat = stream.eatWhile(isNumber$1);
|
||
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$2.At || code === Chars$2.Dollar;
|
||
}
|
||
/**
|
||
* If given character is an operator, returns it’s type
|
||
*/
|
||
function operatorType(ch) {
|
||
return (ch === Chars$2.Sibling && OperatorType.Sibling)
|
||
|| (ch === Chars$2.Excl && OperatorType.Important)
|
||
|| (ch === Chars$2.Comma && OperatorType.ArgumentDelimiter)
|
||
|| (ch === Chars$2.Colon && OperatorType.PropertyDelimiter)
|
||
|| (ch === Chars$2.Dash && OperatorType.ValueDelimiter)
|
||
|| void 0;
|
||
}
|
||
/**
|
||
* Check if given code is a hex value (/0-9a-f/)
|
||
*/
|
||
function isHex(code) {
|
||
return isNumber$1(code) || isAlpha$1(code, 65, 70); // A-F
|
||
}
|
||
function isKeyword(code) {
|
||
return isAlphaNumericWord(code) || code === Chars$2.Dash;
|
||
}
|
||
function isBracket$1(code) {
|
||
return code === Chars$2.RoundBracketOpen || code === Chars$2.RoundBracketClose;
|
||
}
|
||
function isLiteral$1(code) {
|
||
return isAlphaWord(code) || code === Chars$2.Percent || code === Chars$2.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$2(scanner) {
|
||
return scanner.tokens[scanner.pos];
|
||
}
|
||
function readable(scanner) {
|
||
return scanner.pos < scanner.size;
|
||
}
|
||
function consume$1(scanner, test) {
|
||
if (test(peek$2(scanner))) {
|
||
scanner.pos++;
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
function error(scanner, message, token = peek$2(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$1(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$2(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$1(scanner, isValueDelimiter);
|
||
}
|
||
// Skip whitespace right after property name, if any
|
||
if (valueMode) {
|
||
consume$1(scanner, isWhiteSpace$1);
|
||
}
|
||
while (readable(scanner)) {
|
||
if (consume$1(scanner, isImportant)) {
|
||
important = true;
|
||
}
|
||
else if (valueFragment = consumeValue(scanner, valueMode)) {
|
||
value.push(valueFragment);
|
||
}
|
||
else if (!consume$1(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$2(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$1(token))) {
|
||
scanner.pos++;
|
||
}
|
||
else {
|
||
break;
|
||
}
|
||
}
|
||
return result.length
|
||
? { type: 'CSSValue', value: result }
|
||
: void 0;
|
||
}
|
||
function consumeArguments(scanner) {
|
||
const start = scanner.pos;
|
||
if (consume$1(scanner, isOpenBracket$1)) {
|
||
const args = [];
|
||
let value;
|
||
while (readable(scanner) && !consume$1(scanner, isCloseBracket$1)) {
|
||
if (value = consumeValue(scanner, true)) {
|
||
args.push(value);
|
||
}
|
||
else if (!consume$1(scanner, isWhiteSpace$1) && !consume$1(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$1(token) {
|
||
return isBracket(token, true);
|
||
}
|
||
function isCloseBracket$1(token) {
|
||
return isBracket(token, false);
|
||
}
|
||
function isWhiteSpace$1(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, OperatorType.Sibling);
|
||
}
|
||
function isArgumentDelimiter(token) {
|
||
return isOperator(token, OperatorType.ArgumentDelimiter);
|
||
}
|
||
function isFragmentDelimiter(token) {
|
||
return isArgumentDelimiter(token);
|
||
}
|
||
function isImportant(token) {
|
||
return isOperator(token, 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, OperatorType.PropertyDelimiter)
|
||
|| isOperator(token, 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$2(abbr, options) {
|
||
try {
|
||
const tokens = typeof abbr === 'string' ? tokenize(abbr, options && options.value) : abbr;
|
||
return parser(tokens, options);
|
||
}
|
||
catch (err) {
|
||
if (err instanceof ScannerError && typeof abbr === 'string') {
|
||
err.message += `\n${abbr}\n${'-'.repeat(err.pos)}^`;
|
||
}
|
||
throw err;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Merges attributes in current node: de-duplicates attributes with the same name
|
||
* and merges class names
|
||
*/
|
||
function mergeAttributes(node, config) {
|
||
if (!node.attributes) {
|
||
return;
|
||
}
|
||
const attributes = [];
|
||
const lookup = {};
|
||
for (const attr of node.attributes) {
|
||
if (attr.name) {
|
||
const attrName = attr.name;
|
||
if (attrName in lookup) {
|
||
const prev = lookup[attrName];
|
||
if (attrName === 'class') {
|
||
prev.value = mergeValue(prev.value, attr.value, ' ');
|
||
}
|
||
else {
|
||
mergeDeclarations(prev, attr, config);
|
||
}
|
||
}
|
||
else {
|
||
// Create new attribute instance so we can safely modify it later
|
||
attributes.push(lookup[attrName] = Object.assign({}, attr));
|
||
}
|
||
}
|
||
else {
|
||
attributes.push(attr);
|
||
}
|
||
}
|
||
node.attributes = attributes;
|
||
}
|
||
/**
|
||
* Merges two token lists into single list. Adjacent strings are merged together
|
||
*/
|
||
function mergeValue(prev, next, glue) {
|
||
if (prev && next) {
|
||
if (prev.length && glue) {
|
||
append(prev, glue);
|
||
}
|
||
for (const t of next) {
|
||
append(prev, t);
|
||
}
|
||
return prev;
|
||
}
|
||
const result = prev || next;
|
||
return result && result.slice();
|
||
}
|
||
/**
|
||
* Merges data from `src` attribute into `dest` and returns it
|
||
*/
|
||
function mergeDeclarations(dest, src, config) {
|
||
dest.name = src.name;
|
||
if (!config.options['output.reverseAttributes']) {
|
||
dest.value = src.value;
|
||
}
|
||
// Keep high-priority properties
|
||
if (!dest.implied) {
|
||
dest.implied = src.implied;
|
||
}
|
||
if (!dest.boolean) {
|
||
dest.boolean = src.boolean;
|
||
}
|
||
if (dest.valueType !== 'expression') {
|
||
dest.valueType = src.valueType;
|
||
}
|
||
return dest;
|
||
}
|
||
function append(tokens, value) {
|
||
const lastIx = tokens.length - 1;
|
||
if (typeof tokens[lastIx] === 'string' && typeof value === 'string') {
|
||
tokens[lastIx] += value;
|
||
}
|
||
else {
|
||
tokens.push(value);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Walks over each child node of given markup abbreviation AST node (not including
|
||
* given one) and invokes `fn` on each node.
|
||
* The `fn` callback accepts context node, list of ancestor nodes and optional
|
||
* state object
|
||
*/
|
||
function walk$1(node, fn, state) {
|
||
const ancestors = [node];
|
||
const callback = (ctx) => {
|
||
fn(ctx, ancestors, state);
|
||
ancestors.push(ctx);
|
||
ctx.children.forEach(callback);
|
||
ancestors.pop();
|
||
};
|
||
node.children.forEach(callback);
|
||
}
|
||
/**
|
||
* Finds node which is the deepest for in current node or node itself.
|
||
*/
|
||
function findDeepest(node) {
|
||
let parent;
|
||
while (node.children.length) {
|
||
parent = node;
|
||
node = node.children[node.children.length - 1];
|
||
}
|
||
return { parent, node };
|
||
}
|
||
function isNode(node) {
|
||
return node.type === 'AbbreviationNode';
|
||
}
|
||
|
||
/**
|
||
* Finds matching snippet from `registry` and resolves it into a parsed abbreviation.
|
||
* Resolved node is then updated or replaced with matched abbreviation tree.
|
||
*
|
||
* A HTML registry basically contains aliases to another Emmet abbreviations,
|
||
* e.g. a predefined set of name, attributes and so on, possibly a complex
|
||
* abbreviation with multiple elements. So we have to get snippet, parse it
|
||
* and recursively resolve it.
|
||
*/
|
||
function resolveSnippets(abbr, config) {
|
||
const stack = [];
|
||
const reversed = config.options['output.reverseAttributes'];
|
||
const resolve = (child) => {
|
||
const snippet = child.name && config.snippets[child.name];
|
||
// A snippet in stack means circular reference.
|
||
// It can be either a user error or a perfectly valid snippet like
|
||
// "img": "img[src alt]/", e.g. an element with predefined shape.
|
||
// In any case, simply stop parsing and keep element as is
|
||
if (!snippet || stack.includes(snippet)) {
|
||
return null;
|
||
}
|
||
const snippetAbbr = parseAbbreviation(snippet, config);
|
||
stack.push(snippet);
|
||
walkResolve(snippetAbbr, resolve);
|
||
stack.pop();
|
||
// Add attributes from current node into every top-level node of parsed abbreviation
|
||
for (const topNode of snippetAbbr.children) {
|
||
if (child.attributes) {
|
||
const from = topNode.attributes || [];
|
||
const to = child.attributes || [];
|
||
topNode.attributes = reversed ? to.concat(from) : from.concat(to);
|
||
}
|
||
mergeNodes(child, topNode);
|
||
}
|
||
return snippetAbbr;
|
||
};
|
||
walkResolve(abbr, resolve);
|
||
return abbr;
|
||
}
|
||
function walkResolve(node, resolve, config) {
|
||
let children = [];
|
||
for (const child of node.children) {
|
||
const resolved = resolve(child);
|
||
if (resolved) {
|
||
children = children.concat(resolved.children);
|
||
const deepest = findDeepest(resolved);
|
||
if (isNode(deepest.node)) {
|
||
deepest.node.children = deepest.node.children.concat(walkResolve(child, resolve));
|
||
}
|
||
}
|
||
else {
|
||
children.push(child);
|
||
child.children = walkResolve(child, resolve);
|
||
}
|
||
}
|
||
return node.children = children;
|
||
}
|
||
/**
|
||
* Adds data from first node into second node
|
||
*/
|
||
function mergeNodes(from, to) {
|
||
if (from.selfClosing) {
|
||
to.selfClosing = true;
|
||
}
|
||
if (from.value != null) {
|
||
to.value = from.value;
|
||
}
|
||
if (from.repeat) {
|
||
to.repeat = from.repeat;
|
||
}
|
||
}
|
||
|
||
const expressionStart = '{';
|
||
const expressionEnd = '}';
|
||
function createOutputStream(options, level = 0) {
|
||
return {
|
||
options,
|
||
value: '',
|
||
level,
|
||
offset: 0,
|
||
line: 0,
|
||
column: 0
|
||
};
|
||
}
|
||
/**
|
||
* Pushes plain string into output stream without newline processing
|
||
*/
|
||
function push(stream, text) {
|
||
const processText = stream.options['output.text'];
|
||
_push(stream, processText(text, stream.offset, stream.line, stream.column));
|
||
}
|
||
/**
|
||
* Pushes given string with possible newline formatting into output
|
||
*/
|
||
function pushString(stream, value) {
|
||
// If given value contains newlines, we should push content line-by-line and
|
||
// use `pushNewline()` to maintain proper line/column state
|
||
const lines = splitByLines$1(value);
|
||
for (let i = 0, il = lines.length - 1; i <= il; i++) {
|
||
push(stream, lines[i]);
|
||
if (i !== il) {
|
||
pushNewline(stream, true);
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Pushes new line into given output stream
|
||
*/
|
||
function pushNewline(stream, indent) {
|
||
const baseIndent = stream.options['output.baseIndent'];
|
||
const newline = stream.options['output.newline'];
|
||
push(stream, newline + baseIndent);
|
||
stream.line++;
|
||
stream.column = baseIndent.length;
|
||
if (indent) {
|
||
pushIndent(stream, indent === true ? stream.level : indent);
|
||
}
|
||
}
|
||
/**
|
||
* Adds indentation of `size` to current output stream
|
||
*/
|
||
function pushIndent(stream, size = stream.level) {
|
||
const indent = stream.options['output.indent'];
|
||
push(stream, indent.repeat(Math.max(size, 0)));
|
||
}
|
||
/**
|
||
* Pushes field/tabstop into output stream
|
||
*/
|
||
function pushField(stream, index, placeholder) {
|
||
const field = stream.options['output.field'];
|
||
// NB: use `_push` instead of `push` to skip text processing
|
||
_push(stream, field(index, placeholder, stream.offset, stream.line, stream.column));
|
||
}
|
||
/**
|
||
* Returns given tag name formatted according to given config
|
||
*/
|
||
function tagName(name, config) {
|
||
return strCase(name, config.options['output.tagCase']);
|
||
}
|
||
/**
|
||
* Returns given attribute name formatted according to given config
|
||
*/
|
||
function attrName(name, config) {
|
||
return strCase(name, config.options['output.attributeCase']);
|
||
}
|
||
/**
|
||
* Returns character for quoting value of given attribute
|
||
*/
|
||
function attrQuote(attr, config, isOpen) {
|
||
if (attr.valueType === 'expression') {
|
||
return isOpen ? expressionStart : expressionEnd;
|
||
}
|
||
return config.options['output.attributeQuotes'] === 'single' ? '\'' : '"';
|
||
}
|
||
/**
|
||
* Check if given attribute is boolean
|
||
*/
|
||
function isBooleanAttribute(attr, config) {
|
||
return attr.boolean
|
||
|| config.options['output.booleanAttributes'].includes((attr.name || '').toLowerCase());
|
||
}
|
||
/**
|
||
* Returns a token for self-closing tag, depending on current options
|
||
*/
|
||
function selfClose(config) {
|
||
switch (config.options['output.selfClosingStyle']) {
|
||
case 'xhtml': return ' /';
|
||
case 'xml': return '/';
|
||
default: return '';
|
||
}
|
||
}
|
||
/**
|
||
* Check if given tag name belongs to inline-level element
|
||
* @param node Parsed node or tag name
|
||
*/
|
||
function isInline(node, config) {
|
||
if (typeof node === 'string') {
|
||
return config.options.inlineElements.includes(node.toLowerCase());
|
||
}
|
||
// inline node is a node either with inline-level name or text-only node
|
||
return node.name ? isInline(node.name, config) : Boolean(node.value && !node.attributes);
|
||
}
|
||
/**
|
||
* Splits given text by lines
|
||
*/
|
||
function splitByLines$1(text) {
|
||
return text.split(/\r\n|\r|\n/g);
|
||
}
|
||
/**
|
||
* Pushes raw string into output stream without any processing
|
||
*/
|
||
function _push(stream, text) {
|
||
stream.value += text;
|
||
stream.offset += text.length;
|
||
stream.column += text.length;
|
||
}
|
||
function strCase(str, type) {
|
||
if (type) {
|
||
return type === 'upper' ? str.toUpperCase() : str.toLowerCase();
|
||
}
|
||
return str;
|
||
}
|
||
|
||
const elementMap = {
|
||
p: 'span',
|
||
ul: 'li',
|
||
ol: 'li',
|
||
table: 'tr',
|
||
tr: 'td',
|
||
tbody: 'tr',
|
||
thead: 'tr',
|
||
tfoot: 'tr',
|
||
colgroup: 'col',
|
||
select: 'option',
|
||
optgroup: 'option',
|
||
audio: 'source',
|
||
video: 'source',
|
||
object: 'param',
|
||
map: 'area'
|
||
};
|
||
function implicitTag(node, ancestors, config) {
|
||
if (!node.name && node.attributes) {
|
||
resolveImplicitTag(node, ancestors, config);
|
||
}
|
||
}
|
||
function resolveImplicitTag(node, ancestors, config) {
|
||
const parent = getParentElement(ancestors);
|
||
const contextName = config.context ? config.context.name : '';
|
||
const parentName = lowercase(parent ? parent.name : contextName);
|
||
node.name = elementMap[parentName]
|
||
|| (isInline(parentName, config) ? 'span' : 'div');
|
||
}
|
||
function lowercase(str) {
|
||
return (str || '').toLowerCase();
|
||
}
|
||
/**
|
||
* Returns closest element node from given ancestors list
|
||
*/
|
||
function getParentElement(ancestors) {
|
||
for (let i = ancestors.length - 1; i >= 0; i--) {
|
||
const elem = ancestors[i];
|
||
if (isNode(elem)) {
|
||
return elem;
|
||
}
|
||
}
|
||
}
|
||
|
||
var latin = {
|
||
"common": ["lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipisicing", "elit"],
|
||
"words": ["exercitationem", "perferendis", "perspiciatis", "laborum", "eveniet",
|
||
"sunt", "iure", "nam", "nobis", "eum", "cum", "officiis", "excepturi",
|
||
"odio", "consectetur", "quasi", "aut", "quisquam", "vel", "eligendi",
|
||
"itaque", "non", "odit", "tempore", "quaerat", "dignissimos",
|
||
"facilis", "neque", "nihil", "expedita", "vitae", "vero", "ipsum",
|
||
"nisi", "animi", "cumque", "pariatur", "velit", "modi", "natus",
|
||
"iusto", "eaque", "sequi", "illo", "sed", "ex", "et", "voluptatibus",
|
||
"tempora", "veritatis", "ratione", "assumenda", "incidunt", "nostrum",
|
||
"placeat", "aliquid", "fuga", "provident", "praesentium", "rem",
|
||
"necessitatibus", "suscipit", "adipisci", "quidem", "possimus",
|
||
"voluptas", "debitis", "sint", "accusantium", "unde", "sapiente",
|
||
"voluptate", "qui", "aspernatur", "laudantium", "soluta", "amet",
|
||
"quo", "aliquam", "saepe", "culpa", "libero", "ipsa", "dicta",
|
||
"reiciendis", "nesciunt", "doloribus", "autem", "impedit", "minima",
|
||
"maiores", "repudiandae", "ipsam", "obcaecati", "ullam", "enim",
|
||
"totam", "delectus", "ducimus", "quis", "voluptates", "dolores",
|
||
"molestiae", "harum", "dolorem", "quia", "voluptatem", "molestias",
|
||
"magni", "distinctio", "omnis", "illum", "dolorum", "voluptatum", "ea",
|
||
"quas", "quam", "corporis", "quae", "blanditiis", "atque", "deserunt",
|
||
"laboriosam", "earum", "consequuntur", "hic", "cupiditate",
|
||
"quibusdam", "accusamus", "ut", "rerum", "error", "minus", "eius",
|
||
"ab", "ad", "nemo", "fugit", "officia", "at", "in", "id", "quos",
|
||
"reprehenderit", "numquam", "iste", "fugiat", "sit", "inventore",
|
||
"beatae", "repellendus", "magnam", "recusandae", "quod", "explicabo",
|
||
"doloremque", "aperiam", "consequatur", "asperiores", "commodi",
|
||
"optio", "dolor", "labore", "temporibus", "repellat", "veniam",
|
||
"architecto", "est", "esse", "mollitia", "nulla", "a", "similique",
|
||
"eos", "alias", "dolore", "tenetur", "deleniti", "porro", "facere",
|
||
"maxime", "corrupti"]
|
||
};
|
||
|
||
var ru = {
|
||
"common": ["далеко-далеко", "за", "словесными", "горами", "в стране", "гласных", "и согласных", "живут", "рыбные", "тексты"],
|
||
"words": ["вдали", "от всех", "они", "буквенных", "домах", "на берегу", "семантика",
|
||
"большого", "языкового", "океана", "маленький", "ручеек", "даль",
|
||
"журчит", "по всей", "обеспечивает", "ее","всеми", "необходимыми",
|
||
"правилами", "эта", "парадигматическая", "страна", "которой", "жаренные",
|
||
"предложения", "залетают", "прямо", "рот", "даже", "всемогущая",
|
||
"пунктуация", "не", "имеет", "власти", "над", "рыбными", "текстами",
|
||
"ведущими", "безорфографичный", "образ", "жизни", "однажды", "одна",
|
||
"маленькая", "строчка","рыбного", "текста", "имени", "lorem", "ipsum",
|
||
"решила", "выйти", "большой", "мир", "грамматики", "великий", "оксмокс",
|
||
"предупреждал", "о", "злых", "запятых", "диких", "знаках", "вопроса",
|
||
"коварных", "точках", "запятой", "но", "текст", "дал", "сбить",
|
||
"себя", "толку", "он", "собрал", "семь", "своих", "заглавных", "букв",
|
||
"подпоясал", "инициал", "за", "пояс", "пустился", "дорогу",
|
||
"взобравшись", "первую", "вершину", "курсивных", "гор", "бросил",
|
||
"последний", "взгляд", "назад", "силуэт", "своего", "родного", "города",
|
||
"буквоград", "заголовок", "деревни", "алфавит", "подзаголовок", "своего",
|
||
"переулка", "грустный", "реторический", "вопрос", "скатился", "его",
|
||
"щеке", "продолжил", "свой", "путь", "дороге", "встретил", "рукопись",
|
||
"она", "предупредила", "моей", "все", "переписывается", "несколько",
|
||
"раз", "единственное", "что", "меня", "осталось", "это", "приставка",
|
||
"возвращайся", "ты", "лучше", "свою", "безопасную", "страну", "послушавшись",
|
||
"рукописи", "наш", "продолжил", "свой", "путь", "вскоре", "ему",
|
||
"повстречался", "коварный", "составитель", "рекламных", "текстов",
|
||
"напоивший", "языком", "речью", "заманивший", "свое", "агентство",
|
||
"которое", "использовало", "снова", "снова", "своих", "проектах",
|
||
"если", "переписали", "то", "живет", "там", "до", "сих", "пор"]
|
||
};
|
||
|
||
var sp = {
|
||
"common": ["mujer", "uno", "dolor", "más", "de", "poder", "mismo", "si"],
|
||
"words": ["ejercicio", "preferencia", "perspicacia", "laboral", "paño",
|
||
"suntuoso", "molde", "namibia", "planeador", "mirar", "demás", "oficinista", "excepción",
|
||
"odio", "consecuencia", "casi", "auto", "chicharra", "velo", "elixir",
|
||
"ataque", "no", "odio", "temporal", "cuórum", "dignísimo",
|
||
"facilismo", "letra", "nihilista", "expedición", "alma", "alveolar", "aparte",
|
||
"león", "animal", "como", "paria", "belleza", "modo", "natividad",
|
||
"justo", "ataque", "séquito", "pillo", "sed", "ex", "y", "voluminoso",
|
||
"temporalidad", "verdades", "racional", "asunción", "incidente", "marejada",
|
||
"placenta", "amanecer", "fuga", "previsor", "presentación", "lejos",
|
||
"necesariamente", "sospechoso", "adiposidad", "quindío", "pócima",
|
||
"voluble", "débito", "sintió", "accesorio", "falda", "sapiencia",
|
||
"volutas", "queso", "permacultura", "laudo", "soluciones", "entero",
|
||
"pan", "litro", "tonelada", "culpa", "libertario", "mosca", "dictado",
|
||
"reincidente", "nascimiento", "dolor", "escolar", "impedimento", "mínima",
|
||
"mayores", "repugnante", "dulce", "obcecado", "montaña", "enigma",
|
||
"total", "deletéreo", "décima", "cábala", "fotografía", "dolores",
|
||
"molesto", "olvido", "paciencia", "resiliencia", "voluntad", "molestias",
|
||
"magnífico", "distinción", "ovni", "marejada", "cerro", "torre", "y",
|
||
"abogada", "manantial", "corporal", "agua", "crepúsculo", "ataque", "desierto",
|
||
"laboriosamente", "angustia", "afortunado", "alma", "encefalograma",
|
||
"materialidad", "cosas", "o", "renuncia", "error", "menos", "conejo",
|
||
"abadía", "analfabeto", "remo", "fugacidad", "oficio", "en", "almácigo", "vos", "pan",
|
||
"represión", "números", "triste", "refugiado", "trote", "inventor",
|
||
"corchea", "repelente", "magma", "recusado", "patrón", "explícito",
|
||
"paloma", "síndrome", "inmune", "autoinmune", "comodidad",
|
||
"ley", "vietnamita", "demonio", "tasmania", "repeler", "apéndice",
|
||
"arquitecto", "columna", "yugo", "computador", "mula", "a", "propósito",
|
||
"fantasía", "alias", "rayo", "tenedor", "deleznable", "ventana", "cara",
|
||
"anemia", "corrupto"]
|
||
};
|
||
|
||
const vocabularies = { ru, sp, latin };
|
||
const reLorem = /^lorem([a-z]*)(\d*)(-\d*)?$/i;
|
||
function lorem(node, ancestors, config) {
|
||
let m;
|
||
if (node.name && (m = node.name.match(reLorem))) {
|
||
const db = vocabularies[m[1]] || vocabularies.latin;
|
||
const minWordCount = m[2] ? Math.max(1, Number(m[2])) : 30;
|
||
const maxWordCount = m[3] ? Math.max(minWordCount, Number(m[3].slice(1))) : minWordCount;
|
||
const wordCount = rand(minWordCount, maxWordCount);
|
||
const repeat = node.repeat || findRepeater(ancestors);
|
||
node.name = node.attributes = void 0;
|
||
node.value = [paragraph(db, wordCount, !repeat || repeat.value === 0)];
|
||
if (node.repeat && ancestors.length > 1) {
|
||
resolveImplicitTag(node, ancestors, config);
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Returns random integer between <code>from</code> and <code>to</code> values
|
||
*/
|
||
function rand(from, to) {
|
||
return Math.floor(Math.random() * (to - from) + from);
|
||
}
|
||
function sample(arr, count) {
|
||
const len = arr.length;
|
||
const iterations = Math.min(len, count);
|
||
const result = [];
|
||
while (result.length < iterations) {
|
||
const str = arr[rand(0, len)];
|
||
if (!result.includes(str)) {
|
||
result.push(str);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
function choice(val) {
|
||
return val[rand(0, val.length - 1)];
|
||
}
|
||
function sentence(words, end) {
|
||
if (words.length) {
|
||
words = [capitalize(words[0])].concat(words.slice(1));
|
||
}
|
||
return words.join(' ') + (end || choice('?!...')); // more dots than question marks
|
||
}
|
||
function capitalize(word) {
|
||
return word[0].toUpperCase() + word.slice(1);
|
||
}
|
||
/**
|
||
* Insert commas at randomly selected words. This function modifies values
|
||
* inside `words` array
|
||
*/
|
||
function insertCommas(words) {
|
||
if (words.length < 2) {
|
||
return words;
|
||
}
|
||
words = words.slice();
|
||
const len = words.length;
|
||
const hasComma = /,$/;
|
||
let totalCommas = 0;
|
||
if (len > 3 && len <= 6) {
|
||
totalCommas = rand(0, 1);
|
||
}
|
||
else if (len > 6 && len <= 12) {
|
||
totalCommas = rand(0, 2);
|
||
}
|
||
else {
|
||
totalCommas = rand(1, 4);
|
||
}
|
||
for (let i = 0, pos; i < totalCommas; i++) {
|
||
pos = rand(0, len - 2);
|
||
if (!hasComma.test(words[pos])) {
|
||
words[pos] += ',';
|
||
}
|
||
}
|
||
return words;
|
||
}
|
||
/**
|
||
* Generate a paragraph of "Lorem ipsum" text
|
||
* @param dict Words dictionary
|
||
* @param wordCount Words count in paragraph
|
||
* @param startWithCommon Should paragraph start with common "lorem ipsum" sentence.
|
||
*/
|
||
function paragraph(dict, wordCount, startWithCommon) {
|
||
const result = [];
|
||
let totalWords = 0;
|
||
let words;
|
||
if (startWithCommon && dict.common) {
|
||
words = dict.common.slice(0, wordCount);
|
||
totalWords += words.length;
|
||
result.push(sentence(insertCommas(words), '.'));
|
||
}
|
||
while (totalWords < wordCount) {
|
||
words = sample(dict.words, Math.min(rand(2, 30), wordCount - totalWords));
|
||
totalWords += words.length;
|
||
result.push(sentence(insertCommas(words)));
|
||
}
|
||
return result.join(' ');
|
||
}
|
||
function findRepeater(ancestors) {
|
||
for (let i = ancestors.length - 1; i >= 0; i--) {
|
||
const element = ancestors[i];
|
||
if (element.type === 'AbbreviationNode' && element.repeat) {
|
||
return element.repeat;
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* XSL transformer: removes `select` attributes from certain nodes that contain
|
||
* children
|
||
*/
|
||
function xsl(node) {
|
||
if (matchesName(node.name) && node.attributes && (node.children.length || node.value)) {
|
||
node.attributes = node.attributes.filter(isAllowed);
|
||
}
|
||
}
|
||
function isAllowed(attr) {
|
||
return attr.name !== 'select';
|
||
}
|
||
function matchesName(name) {
|
||
return name === 'xsl:variable' || name === 'xsl:with-param';
|
||
}
|
||
|
||
const reElement = /^(-+)([a-z0-9]+[a-z0-9-]*)/i;
|
||
const reModifier = /^(_+)([a-z0-9]+[a-z0-9-_]*)/i;
|
||
const blockCandidates1 = (className) => /^[a-z]\-/i.test(className);
|
||
const blockCandidates2 = (className) => /^[a-z]/i.test(className);
|
||
function bem(node, ancestors, config) {
|
||
expandClassNames(node);
|
||
expandShortNotation(node, ancestors, config);
|
||
}
|
||
/**
|
||
* Expands existing class names in BEM notation in given `node`.
|
||
* For example, if node contains `b__el_mod` class name, this method ensures
|
||
* that element contains `b__el` class as well
|
||
*/
|
||
function expandClassNames(node) {
|
||
const data = getBEMData(node);
|
||
const classNames = [];
|
||
for (const cl of data.classNames) {
|
||
// remove all modifiers and element prefixes from class name to get a base element name
|
||
const ix = cl.indexOf('_');
|
||
if (ix > 0 && !cl.startsWith('-')) {
|
||
classNames.push(cl.slice(0, ix));
|
||
classNames.push(cl.slice(ix));
|
||
}
|
||
else {
|
||
classNames.push(cl);
|
||
}
|
||
}
|
||
if (classNames.length) {
|
||
data.classNames = classNames.filter(uniqueClass);
|
||
data.block = findBlockName(data.classNames);
|
||
updateClass(node, data.classNames.join(' '));
|
||
}
|
||
}
|
||
/**
|
||
* Expands short BEM notation, e.g. `-element` and `_modifier`
|
||
*/
|
||
function expandShortNotation(node, ancestors, config) {
|
||
const data = getBEMData(node);
|
||
const classNames = [];
|
||
const { options } = config;
|
||
const path = ancestors.slice(1).concat(node);
|
||
for (let cl of data.classNames) {
|
||
let prefix = '';
|
||
let m;
|
||
const originalClass = cl;
|
||
// parse element definition (could be only one)
|
||
if (m = cl.match(reElement)) {
|
||
prefix = getBlockName(path, m[1].length, config.context) + options['bem.element'] + m[2];
|
||
classNames.push(prefix);
|
||
cl = cl.slice(m[0].length);
|
||
}
|
||
// parse modifiers definitions
|
||
if (m = cl.match(reModifier)) {
|
||
if (!prefix) {
|
||
prefix = getBlockName(path, m[1].length);
|
||
classNames.push(prefix);
|
||
}
|
||
classNames.push(`${prefix}${options['bem.modifier']}${m[2]}`);
|
||
cl = cl.slice(m[0].length);
|
||
}
|
||
if (cl === originalClass) {
|
||
// class name wasn’t modified: it’s not a BEM-specific class,
|
||
// add it as-is into output
|
||
classNames.push(originalClass);
|
||
}
|
||
}
|
||
const arrClassNames = classNames.filter(uniqueClass);
|
||
if (arrClassNames.length) {
|
||
updateClass(node, arrClassNames.join(' '));
|
||
}
|
||
}
|
||
/**
|
||
* Returns BEM data from given abbreviation node
|
||
*/
|
||
function getBEMData(node) {
|
||
if (!node._bem) {
|
||
let classValue = '';
|
||
if (node.attributes) {
|
||
for (const attr of node.attributes) {
|
||
if (attr.name === 'class' && attr.value) {
|
||
classValue = stringifyValue(attr.value);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
node._bem = parseBEM(classValue);
|
||
}
|
||
return node._bem;
|
||
}
|
||
function getBEMDataFromContext(context) {
|
||
if (!context._bem) {
|
||
context._bem = parseBEM(context.attributes && context.attributes.class || '');
|
||
}
|
||
return context._bem;
|
||
}
|
||
/**
|
||
* Parses BEM data from given class name
|
||
*/
|
||
function parseBEM(classValue) {
|
||
const classNames = classValue ? classValue.split(/\s+/) : [];
|
||
return {
|
||
classNames,
|
||
block: findBlockName(classNames)
|
||
};
|
||
}
|
||
/**
|
||
* Returns block name for given `node` by `prefix`, which tells the depth of
|
||
* of parent node lookup
|
||
*/
|
||
function getBlockName(ancestors, depth = 0, context) {
|
||
const maxParentIx = 0;
|
||
let parentIx = Math.max(ancestors.length - depth, maxParentIx);
|
||
do {
|
||
const parent = ancestors[parentIx];
|
||
if (parent) {
|
||
const data = getBEMData(parent);
|
||
if (data.block) {
|
||
return data.block;
|
||
}
|
||
}
|
||
} while (maxParentIx < parentIx--);
|
||
if (context) {
|
||
const data = getBEMDataFromContext(context);
|
||
if (data.block) {
|
||
return data.block;
|
||
}
|
||
}
|
||
return '';
|
||
}
|
||
function findBlockName(classNames) {
|
||
return find(classNames, blockCandidates1)
|
||
|| find(classNames, blockCandidates2)
|
||
|| void 0;
|
||
}
|
||
/**
|
||
* Finds class name from given list which may be used as block name
|
||
*/
|
||
function find(classNames, filter) {
|
||
for (const cl of classNames) {
|
||
if (reElement.test(cl) || reModifier.test(cl)) {
|
||
break;
|
||
}
|
||
if (filter(cl)) {
|
||
return cl;
|
||
}
|
||
}
|
||
}
|
||
function updateClass(node, value) {
|
||
for (const attr of node.attributes) {
|
||
if (attr.name === 'class') {
|
||
attr.value = [value];
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
function stringifyValue(value) {
|
||
let result = '';
|
||
for (const t of value) {
|
||
result += typeof t === 'string' ? t : t.name;
|
||
}
|
||
return result;
|
||
}
|
||
function uniqueClass(item, ix, arr) {
|
||
return !!item && arr.indexOf(item) === ix;
|
||
}
|
||
|
||
function walk(abbr, visitor, state) {
|
||
const callback = (ctx, index, items) => {
|
||
const { parent, current } = state;
|
||
state.parent = current;
|
||
state.current = ctx;
|
||
visitor(ctx, index, items, state, next);
|
||
state.current = current;
|
||
state.parent = parent;
|
||
};
|
||
const next = (node, index, items) => {
|
||
state.ancestors.push(state.current);
|
||
callback(node, index, items);
|
||
state.ancestors.pop();
|
||
};
|
||
abbr.children.forEach(callback);
|
||
}
|
||
function createWalkState(config) {
|
||
return {
|
||
// @ts-ignore: Will set value in iterator
|
||
current: null,
|
||
parent: void 0,
|
||
ancestors: [],
|
||
config,
|
||
field: 1,
|
||
out: createOutputStream(config.options)
|
||
};
|
||
}
|
||
|
||
const caret = [{ type: 'Field', index: 0, name: '' }];
|
||
/**
|
||
* Check if given node is a snippet: a node without name and attributes
|
||
*/
|
||
function isSnippet(node) {
|
||
return node ? !node.name && !node.attributes : false;
|
||
}
|
||
/**
|
||
* Check if given node is inline-level element, e.g. element with explicitly
|
||
* defined node name
|
||
*/
|
||
function isInlineElement(node, config) {
|
||
return node ? isInline(node, config) : false;
|
||
}
|
||
/**
|
||
* Check if given value token is a field
|
||
*/
|
||
function isField(token) {
|
||
return typeof token === 'object' && token.type === 'Field';
|
||
}
|
||
function pushTokens(tokens, state) {
|
||
const { out } = state;
|
||
let largestIndex = -1;
|
||
for (const t of tokens) {
|
||
if (typeof t === 'string') {
|
||
pushString(out, t);
|
||
}
|
||
else {
|
||
pushField(out, state.field + t.index, t.name);
|
||
if (t.index > largestIndex) {
|
||
largestIndex = t.index;
|
||
}
|
||
}
|
||
}
|
||
if (largestIndex !== -1) {
|
||
state.field += largestIndex + 1;
|
||
}
|
||
}
|
||
/**
|
||
* Splits given value token by lines: returns array where each entry is a token list
|
||
* for a single line
|
||
*/
|
||
function splitByLines(tokens) {
|
||
const result = [];
|
||
let line = [];
|
||
for (const t of tokens) {
|
||
if (typeof t === 'string') {
|
||
const lines = t.split(/\r\n?|\n/g);
|
||
line.push(lines.shift() || '');
|
||
while (lines.length) {
|
||
result.push(line);
|
||
line = [lines.shift() || ''];
|
||
}
|
||
}
|
||
else {
|
||
line.push(t);
|
||
}
|
||
}
|
||
line.length && result.push(line);
|
||
return result;
|
||
}
|
||
/**
|
||
* Check if given attribute should be outputted
|
||
*/
|
||
function shouldOutputAttribute(attr) {
|
||
// In case if attribute is implied, check if it has a defined value:
|
||
// either non-empty value or quoted empty value
|
||
return !attr.implied || attr.valueType !== 'raw' || (!!attr.value && attr.value.length > 0);
|
||
}
|
||
|
||
var TemplateChars;
|
||
(function (TemplateChars) {
|
||
/** `[` character */
|
||
TemplateChars[TemplateChars["Start"] = 91] = "Start";
|
||
/** `]` character */
|
||
TemplateChars[TemplateChars["End"] = 93] = "End";
|
||
/* `_` character */
|
||
TemplateChars[TemplateChars["Underscore"] = 95] = "Underscore";
|
||
/* `-` character */
|
||
TemplateChars[TemplateChars["Dash"] = 45] = "Dash";
|
||
})(TemplateChars || (TemplateChars = {}));
|
||
/**
|
||
* Splits given string into template tokens.
|
||
* Template is a string which contains placeholders which are uppercase names
|
||
* between `[` and `]`, for example: `[PLACEHOLDER]`.
|
||
* Unlike other templates, a placeholder may contain extra characters before and
|
||
* after name: `[%PLACEHOLDER.]`. If data for `PLACEHOLDER` is defined, it will
|
||
* be outputted with with these extra character, otherwise will be completely omitted.
|
||
*/
|
||
function template(text) {
|
||
const tokens = [];
|
||
const scanner = { pos: 0, text };
|
||
let placeholder;
|
||
let offset = scanner.pos;
|
||
let pos = scanner.pos;
|
||
while (scanner.pos < scanner.text.length) {
|
||
pos = scanner.pos;
|
||
if (placeholder = consumePlaceholder(scanner)) {
|
||
if (offset !== scanner.pos) {
|
||
tokens.push(text.slice(offset, pos));
|
||
}
|
||
tokens.push(placeholder);
|
||
offset = scanner.pos;
|
||
}
|
||
else {
|
||
scanner.pos++;
|
||
}
|
||
}
|
||
if (offset !== scanner.pos) {
|
||
tokens.push(text.slice(offset));
|
||
}
|
||
return tokens;
|
||
}
|
||
/**
|
||
* Consumes placeholder like `[#ID]` from given scanner
|
||
*/
|
||
function consumePlaceholder(scanner) {
|
||
if (peek$1(scanner) === TemplateChars.Start) {
|
||
const start = ++scanner.pos;
|
||
let namePos = start;
|
||
let afterPos = start;
|
||
let stack = 1;
|
||
while (scanner.pos < scanner.text.length) {
|
||
const code = peek$1(scanner);
|
||
if (isTokenStart(code)) {
|
||
namePos = scanner.pos;
|
||
while (isToken(peek$1(scanner))) {
|
||
scanner.pos++;
|
||
}
|
||
afterPos = scanner.pos;
|
||
}
|
||
else {
|
||
if (code === TemplateChars.Start) {
|
||
stack++;
|
||
}
|
||
else if (code === TemplateChars.End) {
|
||
if (--stack === 0) {
|
||
return {
|
||
before: scanner.text.slice(start, namePos),
|
||
after: scanner.text.slice(afterPos, scanner.pos++),
|
||
name: scanner.text.slice(namePos, afterPos)
|
||
};
|
||
}
|
||
}
|
||
scanner.pos++;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
function peek$1(scanner, pos = scanner.pos) {
|
||
return scanner.text.charCodeAt(pos);
|
||
}
|
||
function isTokenStart(code) {
|
||
return code >= 65 && code <= 90; // A-Z
|
||
}
|
||
function isToken(code) {
|
||
return isTokenStart(code)
|
||
|| (code > 47 && code < 58) /* 0-9 */
|
||
|| code === TemplateChars.Underscore
|
||
|| code === TemplateChars.Dash;
|
||
}
|
||
|
||
function createCommentState(config) {
|
||
const { options } = config;
|
||
return {
|
||
enabled: options['comment.enabled'],
|
||
trigger: options['comment.trigger'],
|
||
before: options['comment.before'] ? template(options['comment.before']) : void 0,
|
||
after: options['comment.after'] ? template(options['comment.after']) : void 0
|
||
};
|
||
}
|
||
/**
|
||
* Adds comment prefix for given node, if required
|
||
*/
|
||
function commentNodeBefore(node, state) {
|
||
if (shouldComment(node, state) && state.comment.before) {
|
||
output(node, state.comment.before, state);
|
||
}
|
||
}
|
||
/**
|
||
* Adds comment suffix for given node, if required
|
||
*/
|
||
function commentNodeAfter(node, state) {
|
||
if (shouldComment(node, state) && state.comment.after) {
|
||
output(node, state.comment.after, state);
|
||
}
|
||
}
|
||
/**
|
||
* Check if given node should be commented
|
||
*/
|
||
function shouldComment(node, state) {
|
||
const { comment } = state;
|
||
if (!comment.enabled || !comment.trigger || !node.name || !node.attributes) {
|
||
return false;
|
||
}
|
||
for (const attr of node.attributes) {
|
||
if (attr.name && comment.trigger.includes(attr.name)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
/**
|
||
* Pushes given template tokens into output stream
|
||
*/
|
||
function output(node, tokens, state) {
|
||
const attrs = {};
|
||
const { out } = state;
|
||
// Collect attributes payload
|
||
for (const attr of node.attributes) {
|
||
if (attr.name && attr.value) {
|
||
attrs[attr.name.toUpperCase()] = attr.value;
|
||
}
|
||
}
|
||
// Output parsed tokens
|
||
for (const token of tokens) {
|
||
if (typeof token === 'string') {
|
||
pushString(out, token);
|
||
}
|
||
else if (attrs[token.name]) {
|
||
pushString(out, token.before);
|
||
pushTokens(attrs[token.name], state);
|
||
pushString(out, token.after);
|
||
}
|
||
}
|
||
}
|
||
|
||
const htmlTagRegex = /^<([\w\-:]+)[\s>]/;
|
||
const reservedKeywords = new Set([
|
||
'for', 'while', 'of', 'async', 'await', 'const', 'let', 'var', 'continue',
|
||
'break', 'debugger', 'do', 'export', 'import', 'in', 'instanceof', 'new', 'return',
|
||
'switch', 'this', 'throw', 'try', 'catch', 'typeof', 'void', 'with', 'yield'
|
||
]);
|
||
function html(abbr, config) {
|
||
const state = createWalkState(config);
|
||
state.comment = createCommentState(config);
|
||
walk(abbr, element$1, state);
|
||
return state.out.value;
|
||
}
|
||
/**
|
||
* Outputs `node` content to output stream of `state`
|
||
* @param node Context node
|
||
* @param index Index of `node` in `items`
|
||
* @param items List of `node`’s siblings
|
||
* @param state Current walk state
|
||
*/
|
||
function element$1(node, index, items, state, next) {
|
||
const { out, config } = state;
|
||
const format = shouldFormat$1(node, index, items, state);
|
||
// Pick offset level for current node
|
||
const level = getIndent(state);
|
||
out.level += level;
|
||
format && pushNewline(out, true);
|
||
if (node.name) {
|
||
const name = tagName(node.name, config);
|
||
commentNodeBefore(node, state);
|
||
pushString(out, `<${name}`);
|
||
if (node.attributes) {
|
||
for (const attr of node.attributes) {
|
||
if (shouldOutputAttribute(attr)) {
|
||
pushAttribute(attr, state);
|
||
}
|
||
}
|
||
}
|
||
if (node.selfClosing && !node.children.length && !node.value) {
|
||
pushString(out, `${selfClose(config)}>`);
|
||
}
|
||
else {
|
||
pushString(out, '>');
|
||
if (!pushSnippet(node, state, next)) {
|
||
if (node.value) {
|
||
const innerFormat = node.value.some(hasNewline) || startsWithBlockTag(node.value, config);
|
||
innerFormat && pushNewline(state.out, ++out.level);
|
||
pushTokens(node.value, state);
|
||
innerFormat && pushNewline(state.out, --out.level);
|
||
}
|
||
node.children.forEach(next);
|
||
if (!node.value && !node.children.length) {
|
||
const innerFormat = config.options['output.formatLeafNode']
|
||
|| config.options['output.formatForce'].includes(node.name);
|
||
innerFormat && pushNewline(state.out, ++out.level);
|
||
pushTokens(caret, state);
|
||
innerFormat && pushNewline(state.out, --out.level);
|
||
}
|
||
}
|
||
pushString(out, `</${name}>`);
|
||
commentNodeAfter(node, state);
|
||
}
|
||
}
|
||
else if (!pushSnippet(node, state, next) && node.value) {
|
||
// A text-only node (snippet)
|
||
pushTokens(node.value, state);
|
||
node.children.forEach(next);
|
||
}
|
||
if (format && index === items.length - 1 && state.parent) {
|
||
const offset = isSnippet(state.parent) ? 0 : 1;
|
||
pushNewline(out, out.level - offset);
|
||
}
|
||
out.level -= level;
|
||
}
|
||
/**
|
||
* Outputs given attribute’s content into output stream
|
||
*/
|
||
function pushAttribute(attr, state) {
|
||
const { out, config } = state;
|
||
if (attr.name) {
|
||
const attributes = config.options['markup.attributes'];
|
||
const valuePrefix = config.options['markup.valuePrefix'];
|
||
let { name, value } = attr;
|
||
let lQuote = attrQuote(attr, config, true);
|
||
let rQuote = attrQuote(attr, config);
|
||
if (attributes) {
|
||
name = getMultiValue(name, attributes, attr.multiple) || name;
|
||
}
|
||
name = attrName(name, config);
|
||
if (config.options['jsx.enabled'] && attr.multiple) {
|
||
lQuote = expressionStart;
|
||
rQuote = expressionEnd;
|
||
}
|
||
const prefix = valuePrefix
|
||
? getMultiValue(attr.name, valuePrefix, attr.multiple)
|
||
: null;
|
||
if (prefix && (value === null || value === void 0 ? void 0 : value.length) === 1 && typeof value[0] === 'string') {
|
||
// Add given prefix in object notation
|
||
const val = value[0];
|
||
value = [isPropKey(val) ? `${prefix}.${val}` : `${prefix}['${val}']`];
|
||
if (config.options['jsx.enabled']) {
|
||
lQuote = expressionStart;
|
||
rQuote = expressionEnd;
|
||
}
|
||
}
|
||
if (isBooleanAttribute(attr, config) && !value) {
|
||
// If attribute value is omitted and it’s a boolean value, check for
|
||
// `compactBoolean` option: if it’s disabled, set value to attribute name
|
||
// (XML style)
|
||
if (!config.options['output.compactBoolean']) {
|
||
value = [name];
|
||
}
|
||
}
|
||
else if (!value) {
|
||
value = caret;
|
||
}
|
||
pushString(out, ' ' + name);
|
||
if (value) {
|
||
pushString(out, '=' + lQuote);
|
||
pushTokens(value, state);
|
||
pushString(out, rQuote);
|
||
}
|
||
else if (config.options['output.selfClosingStyle'] !== 'html') {
|
||
pushString(out, '=' + lQuote + rQuote);
|
||
}
|
||
}
|
||
}
|
||
function pushSnippet(node, state, next) {
|
||
if (node.value && node.children.length) {
|
||
// We have a value and child nodes. In case if value contains fields,
|
||
// we should output children as a content of first field
|
||
const fieldIx = node.value.findIndex(isField);
|
||
if (fieldIx !== -1) {
|
||
pushTokens(node.value.slice(0, fieldIx), state);
|
||
const line = state.out.line;
|
||
let pos = fieldIx + 1;
|
||
node.children.forEach(next);
|
||
// If there was a line change, trim leading whitespace for better result
|
||
if (state.out.line !== line && typeof node.value[pos] === 'string') {
|
||
pushString(state.out, node.value[pos++].trimLeft());
|
||
}
|
||
pushTokens(node.value.slice(pos), state);
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
/**
|
||
* Check if given node should be formatted in its parent context
|
||
*/
|
||
function shouldFormat$1(node, index, items, state) {
|
||
const { config, parent } = state;
|
||
if (!config.options['output.format']) {
|
||
return false;
|
||
}
|
||
if (index === 0 && !parent) {
|
||
// Do not format very first node
|
||
return false;
|
||
}
|
||
// Do not format single child of snippet
|
||
if (parent && isSnippet(parent) && items.length === 1) {
|
||
return false;
|
||
}
|
||
/**
|
||
* Adjacent text-only/snippet nodes
|
||
*/
|
||
if (isSnippet(node)) {
|
||
// Adjacent text-only/snippet nodes
|
||
const format = isSnippet(items[index - 1]) || isSnippet(items[index + 1])
|
||
// Has newlines: looks like wrapping code fragment
|
||
|| node.value.some(hasNewline)
|
||
// Format as wrapper: contains children which will be outputted as field content
|
||
|| (node.value.some(isField) && node.children.length);
|
||
if (format) {
|
||
return true;
|
||
}
|
||
}
|
||
if (isInline(node, config)) {
|
||
// Check if inline node is the next sibling of block-level node
|
||
if (index === 0) {
|
||
// First node in parent: format if it’s followed by a block-level element
|
||
for (let i = 0; i < items.length; i++) {
|
||
if (!isInline(items[i], config)) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
else if (!isInline(items[index - 1], config)) {
|
||
// Node is right after block-level element
|
||
return true;
|
||
}
|
||
if (config.options['output.inlineBreak']) {
|
||
// check for adjacent inline elements before and after current element
|
||
let adjacentInline = 1;
|
||
let before = index;
|
||
let after = index;
|
||
while (isInlineElement(items[--before], config)) {
|
||
adjacentInline++;
|
||
}
|
||
while (isInlineElement(items[++after], config)) {
|
||
adjacentInline++;
|
||
}
|
||
if (adjacentInline >= config.options['output.inlineBreak']) {
|
||
return true;
|
||
}
|
||
}
|
||
// Edge case: inline node contains node that should receive formatting
|
||
for (let i = 0, il = node.children.length; i < il; i++) {
|
||
if (shouldFormat$1(node.children[i], i, node.children, state)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
/**
|
||
* Returns indentation offset for given node
|
||
*/
|
||
function getIndent(state) {
|
||
const { config, parent } = state;
|
||
if (!parent || isSnippet(parent) || (parent.name && config.options['output.formatSkip'].includes(parent.name))) {
|
||
return 0;
|
||
}
|
||
return 1;
|
||
}
|
||
/**
|
||
* Check if given node value contains newlines
|
||
*/
|
||
function hasNewline(value) {
|
||
return typeof value === 'string' && /\r|\n/.test(value);
|
||
}
|
||
/**
|
||
* Check if given node value starts with block-level tag
|
||
*/
|
||
function startsWithBlockTag(value, config) {
|
||
if (value.length && typeof value[0] === 'string') {
|
||
const matches = htmlTagRegex.exec(value[0]);
|
||
if ((matches === null || matches === void 0 ? void 0 : matches.length) && !config.options['inlineElements'].includes(matches[1].toLowerCase())) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
function getMultiValue(key, data, multiple) {
|
||
return (multiple && data[`${key}*`]) || data[key];
|
||
}
|
||
function isPropKey(name) {
|
||
return !reservedKeywords.has(name) && /^[a-zA-Z_$][\w_$]*$/.test(name);
|
||
}
|
||
|
||
function indentFormat(abbr, config, options) {
|
||
const state = createWalkState(config);
|
||
state.options = options || {};
|
||
walk(abbr, element, state);
|
||
return state.out.value;
|
||
}
|
||
/**
|
||
* Outputs `node` content to output stream of `state`
|
||
* @param node Context node
|
||
* @param index Index of `node` in `items`
|
||
* @param items List of `node`’s siblings
|
||
* @param state Current walk state
|
||
*/
|
||
function element(node, index, items, state, next) {
|
||
const { out, options } = state;
|
||
const { primary, secondary } = collectAttributes(node);
|
||
// Pick offset level for current node
|
||
const level = state.parent ? 1 : 0;
|
||
out.level += level;
|
||
// Do not indent top-level elements
|
||
if (shouldFormat(node, index, items, state)) {
|
||
pushNewline(out, true);
|
||
}
|
||
if (node.name && (node.name !== 'div' || !primary.length)) {
|
||
pushString(out, (options.beforeName || '') + node.name + (options.afterName || ''));
|
||
}
|
||
pushPrimaryAttributes(primary, state);
|
||
pushSecondaryAttributes(secondary.filter(shouldOutputAttribute), state);
|
||
if (node.selfClosing && !node.value && !node.children.length) {
|
||
if (state.options.selfClose) {
|
||
pushString(out, state.options.selfClose);
|
||
}
|
||
}
|
||
else {
|
||
pushValue(node, state);
|
||
node.children.forEach(next);
|
||
}
|
||
out.level -= level;
|
||
}
|
||
/**
|
||
* From given node, collects all attributes as `primary` (id, class) and
|
||
* `secondary` (all the rest) lists. In most indent-based syntaxes, primary attribute
|
||
* has special syntax
|
||
*/
|
||
function collectAttributes(node) {
|
||
const primary = [];
|
||
const secondary = [];
|
||
if (node.attributes) {
|
||
for (const attr of node.attributes) {
|
||
if (isPrimaryAttribute(attr)) {
|
||
primary.push(attr);
|
||
}
|
||
else {
|
||
secondary.push(attr);
|
||
}
|
||
}
|
||
}
|
||
return { primary, secondary };
|
||
}
|
||
/**
|
||
* Outputs given attributes as primary into output stream
|
||
*/
|
||
function pushPrimaryAttributes(attrs, state) {
|
||
for (const attr of attrs) {
|
||
if (attr.value) {
|
||
if (attr.name === 'class') {
|
||
pushString(state.out, '.');
|
||
// All whitespace characters must be replaced with dots in class names
|
||
const tokens = attr.value.map(t => typeof t === 'string' ? t.replace(/\s+/g, '.') : t);
|
||
pushTokens(tokens, state);
|
||
}
|
||
else {
|
||
// ID attribute
|
||
pushString(state.out, '#');
|
||
pushTokens(attr.value, state);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Outputs given attributes as secondary into output stream
|
||
*/
|
||
function pushSecondaryAttributes(attrs, state) {
|
||
if (attrs.length) {
|
||
const { out, config, options } = state;
|
||
options.beforeAttribute && pushString(out, options.beforeAttribute);
|
||
for (let i = 0; i < attrs.length; i++) {
|
||
const attr = attrs[i];
|
||
pushString(out, attrName(attr.name || '', config));
|
||
if (isBooleanAttribute(attr, config) && !attr.value) {
|
||
if (!config.options['output.compactBoolean'] && options.booleanValue) {
|
||
pushString(out, '=' + options.booleanValue);
|
||
}
|
||
}
|
||
else {
|
||
pushString(out, '=' + attrQuote(attr, config, true));
|
||
pushTokens(attr.value || caret, state);
|
||
pushString(out, attrQuote(attr, config));
|
||
}
|
||
if (i !== attrs.length - 1 && options.glueAttribute) {
|
||
pushString(out, options.glueAttribute);
|
||
}
|
||
}
|
||
options.afterAttribute && pushString(out, options.afterAttribute);
|
||
}
|
||
}
|
||
/**
|
||
* Outputs given node value into state output stream
|
||
*/
|
||
function pushValue(node, state) {
|
||
// We should either output value or add caret but for leaf nodes only (no children)
|
||
if (!node.value && node.children.length) {
|
||
return;
|
||
}
|
||
const value = node.value || caret;
|
||
const lines = splitByLines(value);
|
||
const { out, options } = state;
|
||
if (lines.length === 1) {
|
||
if (node.name || node.attributes) {
|
||
push(out, ' ');
|
||
}
|
||
pushTokens(value, state);
|
||
}
|
||
else {
|
||
// We should format multi-line value with terminating `|` character
|
||
// and same line length
|
||
const lineLengths = [];
|
||
let maxLength = 0;
|
||
// Calculate lengths of all lines and max line length
|
||
for (const line of lines) {
|
||
const len = valueLength(line);
|
||
lineLengths.push(len);
|
||
if (len > maxLength) {
|
||
maxLength = len;
|
||
}
|
||
}
|
||
// Output each line, padded to max length
|
||
out.level++;
|
||
for (let i = 0; i < lines.length; i++) {
|
||
pushNewline(out, true);
|
||
options.beforeTextLine && push(out, options.beforeTextLine);
|
||
pushTokens(lines[i], state);
|
||
if (options.afterTextLine) {
|
||
push(out, ' '.repeat(maxLength - lineLengths[i]));
|
||
push(out, options.afterTextLine);
|
||
}
|
||
}
|
||
out.level--;
|
||
}
|
||
}
|
||
function isPrimaryAttribute(attr) {
|
||
return attr.name === 'class' || attr.name === 'id';
|
||
}
|
||
/**
|
||
* Calculates string length from given tokens
|
||
*/
|
||
function valueLength(tokens) {
|
||
let len = 0;
|
||
for (const token of tokens) {
|
||
len += typeof token === 'string' ? token.length : token.name.length;
|
||
}
|
||
return len;
|
||
}
|
||
function shouldFormat(node, index, items, state) {
|
||
// Do not format first top-level element or snippets
|
||
if (!state.parent && index === 0) {
|
||
return false;
|
||
}
|
||
return !isSnippet(node);
|
||
}
|
||
|
||
function haml(abbr, config) {
|
||
return indentFormat(abbr, config, {
|
||
beforeName: '%',
|
||
beforeAttribute: '(',
|
||
afterAttribute: ')',
|
||
glueAttribute: ' ',
|
||
afterTextLine: ' |',
|
||
booleanValue: 'true',
|
||
selfClose: '/'
|
||
});
|
||
}
|
||
|
||
function slim(abbr, config) {
|
||
return indentFormat(abbr, config, {
|
||
beforeAttribute: ' ',
|
||
glueAttribute: ' ',
|
||
beforeTextLine: '| ',
|
||
selfClose: '/'
|
||
});
|
||
}
|
||
|
||
function pug(abbr, config) {
|
||
return indentFormat(abbr, config, {
|
||
beforeAttribute: '(',
|
||
afterAttribute: ')',
|
||
glueAttribute: ', ',
|
||
beforeTextLine: '| ',
|
||
selfClose: config.options['output.selfClosingStyle'] === 'xml' ? '/' : ''
|
||
});
|
||
}
|
||
|
||
const formatters = { html, haml, slim, pug };
|
||
/**
|
||
* Parses given Emmet abbreviation into a final abbreviation tree with all
|
||
* required transformations applied
|
||
*/
|
||
function parse$1(abbr, config) {
|
||
let oldTextValue;
|
||
if (typeof abbr === 'string') {
|
||
const parseOpt = Object.assign({}, config);
|
||
if (config.options['jsx.enabled']) {
|
||
parseOpt.jsx = true;
|
||
}
|
||
if (config.options['markup.href']) {
|
||
parseOpt.href = true;
|
||
}
|
||
abbr = parseAbbreviation(abbr, parseOpt);
|
||
// remove text field before snippets(abbr, config) call
|
||
// as abbreviation(abbr, parseOpt) already handled it
|
||
oldTextValue = config.text;
|
||
config.text = undefined;
|
||
}
|
||
// Run abbreviation resolve in two passes:
|
||
// 1. Map each node to snippets, which are abbreviations as well. A single snippet
|
||
// may produce multiple nodes
|
||
// 2. Transform every resolved node
|
||
abbr = resolveSnippets(abbr, config);
|
||
walk$1(abbr, transform, config);
|
||
config.text = oldTextValue !== null && oldTextValue !== void 0 ? oldTextValue : config.text;
|
||
return abbr;
|
||
}
|
||
/**
|
||
* Converts given abbreviation to string according to provided `config`
|
||
*/
|
||
function stringify(abbr, config) {
|
||
const formatter = formatters[config.syntax] || html;
|
||
return formatter(abbr, config);
|
||
}
|
||
/**
|
||
* Modifies given node and prepares it for output
|
||
*/
|
||
function transform(node, ancestors, config) {
|
||
implicitTag(node, ancestors, config);
|
||
mergeAttributes(node, config);
|
||
lorem(node, ancestors, config);
|
||
if (config.syntax === 'xsl') {
|
||
xsl(node);
|
||
}
|
||
if (config.options['bem.enabled']) {
|
||
bem(node, ancestors, config);
|
||
}
|
||
}
|
||
|
||
var CSSSnippetType;
|
||
(function (CSSSnippetType) {
|
||
CSSSnippetType["Raw"] = "Raw";
|
||
CSSSnippetType["Property"] = "Property";
|
||
})(CSSSnippetType || (CSSSnippetType = {}));
|
||
const reProperty = /^([a-z-]+)(?:\s*:\s*([^\n\r;]+?);*)?$/;
|
||
const opt = { value: true };
|
||
/**
|
||
* Creates structure for holding resolved CSS snippet
|
||
*/
|
||
function createSnippet(key, value) {
|
||
// A snippet could be a raw text snippet (e.g. arbitrary text string) or a
|
||
// CSS property with possible values separated by `|`.
|
||
// In latter case, we have to parse snippet as CSS abbreviation
|
||
const m = value.match(reProperty);
|
||
if (m) {
|
||
const keywords = {};
|
||
const parsed = m[2] ? m[2].split('|').map(parseValue) : [];
|
||
for (const item of parsed) {
|
||
for (const cssVal of item) {
|
||
collectKeywords(cssVal, keywords);
|
||
}
|
||
}
|
||
return {
|
||
type: CSSSnippetType.Property,
|
||
key,
|
||
property: m[1],
|
||
value: parsed,
|
||
keywords,
|
||
dependencies: []
|
||
};
|
||
}
|
||
return { type: CSSSnippetType.Raw, key, value };
|
||
}
|
||
/**
|
||
* Nests more specific CSS properties into shorthand ones, e.g.
|
||
* `background-position-x` -> `background-position` -> `background`
|
||
*/
|
||
function nest(snippets) {
|
||
snippets = snippets.slice().sort(snippetsSort);
|
||
const stack = [];
|
||
let prev;
|
||
// For sorted list of CSS properties, create dependency graph where each
|
||
// shorthand property contains its more specific one, e.g.
|
||
// background -> background-position -> background-position-x
|
||
for (const cur of snippets.filter(isProperty)) {
|
||
// Check if current property belongs to one from parent stack.
|
||
// Since `snippets` array is sorted, items are perfectly aligned
|
||
// from shorthands to more specific variants
|
||
while (stack.length) {
|
||
prev = stack[stack.length - 1];
|
||
if (cur.property.startsWith(prev.property)
|
||
&& cur.property.charCodeAt(prev.property.length) === 45 /* - */) {
|
||
prev.dependencies.push(cur);
|
||
stack.push(cur);
|
||
break;
|
||
}
|
||
stack.pop();
|
||
}
|
||
if (!stack.length) {
|
||
stack.push(cur);
|
||
}
|
||
}
|
||
return snippets;
|
||
}
|
||
/**
|
||
* A sorting function for array of snippets
|
||
*/
|
||
function snippetsSort(a, b) {
|
||
if (a.key === b.key) {
|
||
return 0;
|
||
}
|
||
return a.key < b.key ? -1 : 1;
|
||
}
|
||
function parseValue(value) {
|
||
return parse$2(value.trim(), opt)[0].value;
|
||
}
|
||
function isProperty(snippet) {
|
||
return snippet.type === CSSSnippetType.Property;
|
||
}
|
||
function collectKeywords(cssVal, dest) {
|
||
for (const v of cssVal.value) {
|
||
if (v.type === 'Literal') {
|
||
dest[v.value] = v;
|
||
}
|
||
else if (v.type === 'FunctionCall') {
|
||
dest[v.name] = v;
|
||
}
|
||
else if (v.type === 'Field') {
|
||
// Create literal from field, if available
|
||
const value = v.name.trim();
|
||
if (value) {
|
||
dest[value] = { type: 'Literal', value };
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Calculates how close `str1` matches `str2` using fuzzy match.
|
||
* How matching works:
|
||
* – first characters of both `str1` and `str2` *must* match
|
||
* – `str1` length larger than `str2` length is allowed only when `unmatched` is true
|
||
* – ideal match is when `str1` equals to `str2` (score: 1)
|
||
* – next best match is `str2` starts with `str1` (score: 1 × percent of matched characters)
|
||
* – other scores depend on how close characters of `str1` to the beginning of `str2`
|
||
* @param partialMatch Allow length `str1` to be greater than `str2` length
|
||
*/
|
||
function scoreMatch(str1, str2, partialMatch = false) {
|
||
str1 = str1.toLowerCase();
|
||
str2 = str2.toLowerCase();
|
||
if (str1 === str2) {
|
||
return 1;
|
||
}
|
||
// Both strings MUST start with the same character
|
||
if (!str1 || !str2 || str1.charCodeAt(0) !== str2.charCodeAt(0)) {
|
||
return 0;
|
||
}
|
||
const str1Len = str1.length;
|
||
const str2Len = str2.length;
|
||
if (!partialMatch && str1Len > str2Len) {
|
||
return 0;
|
||
}
|
||
// Characters from `str1` which are closer to the beginning of a `str2` should
|
||
// have higher score.
|
||
// For example, if `str2` is `abcde`, it’s max score is:
|
||
// 5 + 4 + 3 + 2 + 1 = 15 (sum of character positions in reverse order)
|
||
// Matching `abd` against `abcde` should produce:
|
||
// 5 + 4 + 2 = 11
|
||
// Acronym bonus for match right after `-`. Matching `abd` against `abc-de`
|
||
// should produce:
|
||
// 6 + 5 + 4 (use `d` position in `abd`, not in abc-de`)
|
||
const minLength = Math.min(str1Len, str2Len);
|
||
const maxLength = Math.max(str1Len, str2Len);
|
||
let i = 1;
|
||
let j = 1;
|
||
let score = maxLength;
|
||
let ch1 = 0;
|
||
let ch2 = 0;
|
||
let found = false;
|
||
let acronym = false;
|
||
while (i < str1Len) {
|
||
ch1 = str1.charCodeAt(i);
|
||
found = false;
|
||
acronym = false;
|
||
while (j < str2Len) {
|
||
ch2 = str2.charCodeAt(j);
|
||
if (ch1 === ch2) {
|
||
found = true;
|
||
score += maxLength - (acronym ? i : j);
|
||
break;
|
||
}
|
||
// add acronym bonus for exactly next match after unmatched `-`
|
||
acronym = ch2 === 45 /* - */;
|
||
j++;
|
||
}
|
||
if (!found) {
|
||
if (!partialMatch) {
|
||
return 0;
|
||
}
|
||
break;
|
||
}
|
||
i++;
|
||
}
|
||
const matchRatio = i / maxLength;
|
||
const delta = maxLength - minLength;
|
||
const maxScore = sum(maxLength) - sum(delta);
|
||
return (score * matchRatio) / maxScore;
|
||
}
|
||
/**
|
||
* Calculates sum of first `n` numbers, e.g. 1+2+3+...n
|
||
*/
|
||
function sum(n) {
|
||
return n * (n + 1) / 2;
|
||
}
|
||
|
||
function color(token, shortHex) {
|
||
if (!token.r && !token.g && !token.b && !token.a) {
|
||
return 'transparent';
|
||
}
|
||
else if (token.a === 1) {
|
||
return asHex(token, shortHex);
|
||
}
|
||
return asRGB(token);
|
||
}
|
||
/**
|
||
* Output given color as hex value
|
||
* @param short Produce short value (e.g. #fff instead of #ffffff), if possible
|
||
*/
|
||
function asHex(token, short) {
|
||
const fn = (short && isShortHex(token.r) && isShortHex(token.g) && isShortHex(token.b))
|
||
? toShortHex : toHex;
|
||
return '#' + fn(token.r) + fn(token.g) + fn(token.b);
|
||
}
|
||
/**
|
||
* Output current color as `rgba?(...)` CSS color
|
||
*/
|
||
function asRGB(token) {
|
||
const values = [token.r, token.g, token.b];
|
||
if (token.a !== 1) {
|
||
values.push(frac(token.a, 8));
|
||
}
|
||
return `${values.length === 3 ? 'rgb' : 'rgba'}(${values.join(', ')})`;
|
||
}
|
||
function frac(num, digits = 4) {
|
||
return num.toFixed(digits).replace(/\.?0+$/, '');
|
||
}
|
||
function isShortHex(hex) {
|
||
return !(hex % 17);
|
||
}
|
||
function toShortHex(num) {
|
||
return (num >> 4).toString(16);
|
||
}
|
||
function toHex(num) {
|
||
return pad(num.toString(16), 2);
|
||
}
|
||
function pad(value, len) {
|
||
while (value.length < len) {
|
||
value = '0' + value;
|
||
}
|
||
return value;
|
||
}
|
||
|
||
const CSSAbbreviationScope = {
|
||
/** Include all possible snippets in match */
|
||
Global: '@@global',
|
||
/** Include raw snippets only (e.g. no properties) in abbreviation match */
|
||
Section: '@@section',
|
||
/** Include properties only in abbreviation match */
|
||
Property: '@@property',
|
||
/** Resolve abbreviation in context of CSS property value */
|
||
Value: '@@value',
|
||
};
|
||
function css(abbr, config) {
|
||
var _a;
|
||
const out = createOutputStream(config.options);
|
||
const format = config.options['output.format'];
|
||
if (((_a = config.context) === null || _a === void 0 ? void 0 : _a.name) === CSSAbbreviationScope.Section) {
|
||
// For section context, filter out unmatched snippets
|
||
abbr = abbr.filter(node => node.snippet);
|
||
}
|
||
for (let i = 0; i < abbr.length; i++) {
|
||
if (format && i !== 0) {
|
||
pushNewline(out, true);
|
||
}
|
||
property(abbr[i], out, config);
|
||
}
|
||
return out.value;
|
||
}
|
||
/**
|
||
* Outputs given abbreviation node into output stream
|
||
*/
|
||
function property(node, out, config) {
|
||
const isJSON = config.options['stylesheet.json'];
|
||
if (node.name) {
|
||
// It’s a CSS property
|
||
const name = isJSON ? toCamelCase(node.name) : node.name;
|
||
pushString(out, name + config.options['stylesheet.between']);
|
||
if (node.value.length) {
|
||
propertyValue(node, out, config);
|
||
}
|
||
else {
|
||
pushField(out, 0, '');
|
||
}
|
||
if (isJSON) {
|
||
// For CSS-in-JS, always finalize property with comma
|
||
// NB: seems like `important` is not available in CSS-in-JS syntaxes
|
||
push(out, ',');
|
||
}
|
||
else {
|
||
outputImportant(node, out, true);
|
||
push(out, config.options['stylesheet.after']);
|
||
}
|
||
}
|
||
else {
|
||
// It’s a regular snippet, output plain tokens without any additional formatting
|
||
for (const cssVal of node.value) {
|
||
for (const v of cssVal.value) {
|
||
outputToken(v, out, config);
|
||
}
|
||
}
|
||
outputImportant(node, out, node.value.length > 0);
|
||
}
|
||
}
|
||
function propertyValue(node, out, config) {
|
||
const isJSON = config.options['stylesheet.json'];
|
||
const num = isJSON ? getSingleNumeric(node) : null;
|
||
if (num && (!num.unit || num.unit === 'px')) {
|
||
// For CSS-in-JS, if property contains single numeric value, output it
|
||
// as JS number
|
||
push(out, String(num.value));
|
||
}
|
||
else {
|
||
const quote = getQuote(config);
|
||
isJSON && push(out, quote);
|
||
for (let i = 0; i < node.value.length; i++) {
|
||
if (i !== 0) {
|
||
push(out, ', ');
|
||
}
|
||
outputValue(node.value[i], out, config);
|
||
}
|
||
isJSON && push(out, quote);
|
||
}
|
||
}
|
||
function outputImportant(node, out, separator) {
|
||
if (node.important) {
|
||
if (separator) {
|
||
push(out, ' ');
|
||
}
|
||
push(out, '!important');
|
||
}
|
||
}
|
||
function outputValue(value, out, config) {
|
||
for (let i = 0, prevEnd = -1; i < value.value.length; i++) {
|
||
const token = value.value[i];
|
||
// Handle edge case: a field is written close to previous token like this: `foo${bar}`.
|
||
// We should not add delimiter here
|
||
if (i !== 0 && (token.type !== 'Field' || token.start !== prevEnd)) {
|
||
push(out, ' ');
|
||
}
|
||
outputToken(token, out, config);
|
||
prevEnd = token['end'];
|
||
}
|
||
}
|
||
function outputToken(token, out, config) {
|
||
if (token.type === 'ColorValue') {
|
||
push(out, color(token, config.options['stylesheet.shortHex']));
|
||
}
|
||
else if (token.type === 'Literal' || token.type === 'CustomProperty') {
|
||
pushString(out, token.value);
|
||
}
|
||
else if (token.type === 'NumberValue') {
|
||
pushString(out, frac(token.value, 4) + token.unit);
|
||
}
|
||
else if (token.type === 'StringValue') {
|
||
const quote = token.quote === 'double' ? '"' : '\'';
|
||
pushString(out, quote + token.value + quote);
|
||
}
|
||
else if (token.type === 'Field') {
|
||
pushField(out, token.index, token.name);
|
||
}
|
||
else if (token.type === 'FunctionCall') {
|
||
push(out, token.name + '(');
|
||
for (let i = 0; i < token.arguments.length; i++) {
|
||
if (i) {
|
||
push(out, ', ');
|
||
}
|
||
outputValue(token.arguments[i], out, config);
|
||
}
|
||
push(out, ')');
|
||
}
|
||
}
|
||
/**
|
||
* If value of given property is a single numeric value, returns this token
|
||
*/
|
||
function getSingleNumeric(node) {
|
||
if (node.value.length === 1) {
|
||
const cssVal = node.value[0];
|
||
if (cssVal.value.length === 1 && cssVal.value[0].type === 'NumberValue') {
|
||
return cssVal.value[0];
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Converts kebab-case string to camelCase
|
||
*/
|
||
function toCamelCase(str) {
|
||
return str.replace(/\-(\w)/g, (_, letter) => letter.toUpperCase());
|
||
}
|
||
function getQuote(config) {
|
||
return config.options['stylesheet.jsonDoubleQuotes'] ? '"' : '\'';
|
||
}
|
||
|
||
const gradientName = 'lg';
|
||
/**
|
||
* Parses given Emmet abbreviation into a final abbreviation tree with all
|
||
* required transformations applied
|
||
*/
|
||
function parse(abbr, config) {
|
||
var _a;
|
||
const snippets = ((_a = config.cache) === null || _a === void 0 ? void 0 : _a.stylesheetSnippets) || convertSnippets(config.snippets);
|
||
if (config.cache) {
|
||
config.cache.stylesheetSnippets = snippets;
|
||
}
|
||
if (typeof abbr === 'string') {
|
||
abbr = parse$2(abbr, { value: isValueScope(config) });
|
||
}
|
||
const filteredSnippets = getSnippetsForScope(snippets, config);
|
||
for (const node of abbr) {
|
||
resolveNode(node, filteredSnippets, config);
|
||
}
|
||
return abbr;
|
||
}
|
||
/**
|
||
* Converts given raw snippets into internal snippets representation
|
||
*/
|
||
function convertSnippets(snippets) {
|
||
const result = [];
|
||
for (const key of Object.keys(snippets)) {
|
||
result.push(createSnippet(key, snippets[key]));
|
||
}
|
||
return nest(result);
|
||
}
|
||
/**
|
||
* Resolves given node: finds matched CSS snippets using fuzzy match and resolves
|
||
* keyword aliases from node value
|
||
*/
|
||
function resolveNode(node, snippets, config) {
|
||
if (!resolveGradient(node, config)) {
|
||
const score = config.options['stylesheet.fuzzySearchMinScore'];
|
||
if (isValueScope(config)) {
|
||
// Resolve as value of given CSS property
|
||
const propName = config.context.name;
|
||
const snippet = snippets.find(s => s.type === CSSSnippetType.Property && s.property === propName);
|
||
resolveValueKeywords(node, config, snippet, score);
|
||
node.snippet = snippet;
|
||
}
|
||
else if (node.name) {
|
||
const snippet = findBestMatch(node.name, snippets, score, true);
|
||
node.snippet = snippet;
|
||
if (snippet) {
|
||
if (snippet.type === CSSSnippetType.Property) {
|
||
resolveAsProperty(node, snippet, config);
|
||
}
|
||
else {
|
||
resolveAsSnippet(node, snippet);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (node.name || config.context) {
|
||
// Resolve numeric values for CSS properties only
|
||
resolveNumericValue(node, config);
|
||
}
|
||
return node;
|
||
}
|
||
/**
|
||
* Resolves CSS gradient shortcut from given property, if possible
|
||
*/
|
||
function resolveGradient(node, config) {
|
||
let gradientFn = null;
|
||
const cssVal = node.value.length === 1 ? node.value[0] : null;
|
||
if (cssVal && cssVal.value.length === 1) {
|
||
const v = cssVal.value[0];
|
||
if (v.type === 'FunctionCall' && v.name === gradientName) {
|
||
gradientFn = v;
|
||
}
|
||
}
|
||
if (gradientFn || node.name === gradientName) {
|
||
if (!gradientFn) {
|
||
gradientFn = {
|
||
type: 'FunctionCall',
|
||
name: 'linear-gradient',
|
||
arguments: [cssValue(field(0, ''))]
|
||
};
|
||
}
|
||
else {
|
||
gradientFn = Object.assign(Object.assign({}, gradientFn), { name: 'linear-gradient' });
|
||
}
|
||
if (!config.context) {
|
||
node.name = 'background-image';
|
||
}
|
||
node.value = [cssValue(gradientFn)];
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
/**
|
||
* Resolves given parsed abbreviation node as CSS property
|
||
*/
|
||
function resolveAsProperty(node, snippet, config) {
|
||
const abbr = node.name;
|
||
// Check for unmatched part of abbreviation
|
||
// For example, in `dib` abbreviation the matched part is `d` and `ib` should
|
||
// be considered as inline value. If unmatched fragment exists, we should check
|
||
// if it matches actual value of snippet. If either explicit value is specified
|
||
// or unmatched fragment did not resolve to to a keyword, we should consider
|
||
// matched snippet as invalid
|
||
const inlineValue = getUnmatchedPart(abbr, snippet.key);
|
||
if (inlineValue) {
|
||
if (node.value.length) {
|
||
// Already have value: unmatched part indicates matched snippet is invalid
|
||
return node;
|
||
}
|
||
const kw = resolveKeyword(inlineValue, config, snippet);
|
||
if (!kw) {
|
||
return node;
|
||
}
|
||
node.value.push(cssValue(kw));
|
||
}
|
||
node.name = snippet.property;
|
||
if (node.value.length) {
|
||
// Replace keyword alias from current abbreviation node with matched keyword
|
||
resolveValueKeywords(node, config, snippet);
|
||
}
|
||
else if (snippet.value.length) {
|
||
const defaultValue = snippet.value[0];
|
||
// https://github.com/emmetio/emmet/issues/558
|
||
// We should auto-select inserted value only if there’s multiple value
|
||
// choice
|
||
node.value = snippet.value.length === 1 || defaultValue.some(hasField)
|
||
? defaultValue
|
||
: defaultValue.map(n => wrapWithField(n, config));
|
||
}
|
||
return node;
|
||
}
|
||
function resolveValueKeywords(node, config, snippet, minScore) {
|
||
for (const cssVal of node.value) {
|
||
const value = [];
|
||
for (const token of cssVal.value) {
|
||
if (token.type === 'Literal') {
|
||
value.push(resolveKeyword(token.value, config, snippet, minScore) || token);
|
||
}
|
||
else if (token.type === 'FunctionCall') {
|
||
// For function calls, we should find matching function call
|
||
// and merge arguments
|
||
const match = resolveKeyword(token.name, config, snippet, minScore);
|
||
if (match && match.type === 'FunctionCall') {
|
||
value.push(Object.assign(Object.assign({}, match), { arguments: token.arguments.concat(match.arguments.slice(token.arguments.length)) }));
|
||
}
|
||
else {
|
||
value.push(token);
|
||
}
|
||
}
|
||
else {
|
||
value.push(token);
|
||
}
|
||
}
|
||
cssVal.value = value;
|
||
}
|
||
}
|
||
/**
|
||
* Resolves given parsed abbreviation node as a snippet: a plain code chunk
|
||
*/
|
||
function resolveAsSnippet(node, snippet) {
|
||
// When resolving snippets, we have to do the following:
|
||
// 1. Replace field placeholders with actual field tokens.
|
||
// 2. If input values given, put them instead of fields
|
||
let offset = 0;
|
||
let m;
|
||
const reField = /\$\{(\d+)(:[^}]+)?\}/g;
|
||
const inputValue = node.value[0];
|
||
const outputValue = [];
|
||
while (m = reField.exec(snippet.value)) {
|
||
if (offset !== m.index) {
|
||
outputValue.push(literal(snippet.value.slice(offset, m.index)));
|
||
}
|
||
offset = m.index + m[0].length;
|
||
if (inputValue && inputValue.value.length) {
|
||
outputValue.push(inputValue.value.shift());
|
||
}
|
||
else {
|
||
outputValue.push(field(Number(m[1]), m[2] ? m[2].slice(1) : ''));
|
||
}
|
||
}
|
||
const tail = snippet.value.slice(offset);
|
||
if (tail) {
|
||
outputValue.push(literal(tail));
|
||
}
|
||
node.name = void 0;
|
||
node.value = [cssValue(...outputValue)];
|
||
return node;
|
||
}
|
||
/**
|
||
* Finds best matching item from `items` array
|
||
* @param abbr Abbreviation to match
|
||
* @param items List of items for match
|
||
* @param minScore The minimum score the best matched item should have to be a valid match.
|
||
*/
|
||
function findBestMatch(abbr, items, minScore = 0, partialMatch = false) {
|
||
let matchedItem = null;
|
||
let maxScore = 0;
|
||
for (const item of items) {
|
||
const score = scoreMatch(abbr, getScoringPart(item), partialMatch);
|
||
if (score === 1) {
|
||
// direct hit, no need to look further
|
||
return item;
|
||
}
|
||
if (score && score >= maxScore) {
|
||
maxScore = score;
|
||
matchedItem = item;
|
||
}
|
||
}
|
||
return maxScore >= minScore ? matchedItem : null;
|
||
}
|
||
function getScoringPart(item) {
|
||
return typeof item === 'string' ? item : item.key;
|
||
}
|
||
/**
|
||
* Returns a part of `abbr` that wasn’t directly matched against `str`.
|
||
* For example, if abbreviation `poas` is matched against `position`,
|
||
* the unmatched part will be `as` since `a` wasn’t found in string stream
|
||
*/
|
||
function getUnmatchedPart(abbr, str) {
|
||
for (let i = 0, lastPos = 0; i < abbr.length; i++) {
|
||
lastPos = str.indexOf(abbr[i], lastPos);
|
||
if (lastPos === -1) {
|
||
return abbr.slice(i);
|
||
}
|
||
lastPos++;
|
||
}
|
||
return '';
|
||
}
|
||
/**
|
||
* Resolves given keyword shorthand into matched snippet keyword or global keyword,
|
||
* if possible
|
||
*/
|
||
function resolveKeyword(kw, config, snippet, minScore) {
|
||
let ref;
|
||
if (snippet) {
|
||
if (ref = findBestMatch(kw, Object.keys(snippet.keywords), minScore)) {
|
||
return snippet.keywords[ref];
|
||
}
|
||
for (const dep of snippet.dependencies) {
|
||
if (ref = findBestMatch(kw, Object.keys(dep.keywords), minScore)) {
|
||
return dep.keywords[ref];
|
||
}
|
||
}
|
||
}
|
||
if (ref = findBestMatch(kw, config.options['stylesheet.keywords'], minScore)) {
|
||
return literal(ref);
|
||
}
|
||
return null;
|
||
}
|
||
/**
|
||
* Resolves numeric values in given abbreviation node
|
||
*/
|
||
function resolveNumericValue(node, config) {
|
||
const aliases = config.options['stylesheet.unitAliases'];
|
||
const unitless = config.options['stylesheet.unitless'];
|
||
for (const v of node.value) {
|
||
for (const t of v.value) {
|
||
if (t.type === 'NumberValue') {
|
||
if (t.unit) {
|
||
t.unit = aliases[t.unit] || t.unit;
|
||
}
|
||
else if (t.value !== 0 && !unitless.includes(node.name)) {
|
||
t.unit = t.rawValue.includes('.')
|
||
? config.options['stylesheet.floatUnit']
|
||
: config.options['stylesheet.intUnit'];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* Constructs CSS value token
|
||
*/
|
||
function cssValue(...args) {
|
||
return {
|
||
type: 'CSSValue',
|
||
value: args
|
||
};
|
||
}
|
||
/**
|
||
* Constructs literal token
|
||
*/
|
||
function literal(value) {
|
||
return { type: 'Literal', value };
|
||
}
|
||
/**
|
||
* Constructs field token
|
||
*/
|
||
function field(index, name) {
|
||
return { type: 'Field', index, name };
|
||
}
|
||
/**
|
||
* Check if given value contains fields
|
||
*/
|
||
function hasField(value) {
|
||
for (const v of value.value) {
|
||
if (v.type === 'Field' || (v.type === 'FunctionCall' && v.arguments.some(hasField))) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
/**
|
||
* Wraps tokens of given abbreviation with fields
|
||
*/
|
||
function wrapWithField(node, config, state = { index: 1 }) {
|
||
let value = [];
|
||
for (const v of node.value) {
|
||
switch (v.type) {
|
||
case 'ColorValue':
|
||
value.push(field(state.index++, color(v, config.options['stylesheet.shortHex'])));
|
||
break;
|
||
case 'Literal':
|
||
value.push(field(state.index++, v.value));
|
||
break;
|
||
case 'NumberValue':
|
||
value.push(field(state.index++, `${v.value}${v.unit}`));
|
||
break;
|
||
case 'StringValue':
|
||
const q = v.quote === 'single' ? '\'' : '"';
|
||
value.push(field(state.index++, q + v.value + q));
|
||
break;
|
||
case 'FunctionCall':
|
||
value.push(field(state.index++, v.name), literal('('));
|
||
for (let i = 0, il = v.arguments.length; i < il; i++) {
|
||
value = value.concat(wrapWithField(v.arguments[i], config, state).value);
|
||
if (i !== il - 1) {
|
||
value.push(literal(', '));
|
||
}
|
||
}
|
||
value.push(literal(')'));
|
||
break;
|
||
default:
|
||
value.push(v);
|
||
}
|
||
}
|
||
return Object.assign(Object.assign({}, node), { value });
|
||
}
|
||
/**
|
||
* Check if abbreviation should be expanded in CSS value context
|
||
*/
|
||
function isValueScope(config) {
|
||
if (config.context) {
|
||
return config.context.name === CSSAbbreviationScope.Value || !config.context.name.startsWith('@@');
|
||
}
|
||
return false;
|
||
}
|
||
/**
|
||
* Returns snippets for given scope
|
||
*/
|
||
function getSnippetsForScope(snippets, config) {
|
||
if (config.context) {
|
||
if (config.context.name === CSSAbbreviationScope.Section) {
|
||
return snippets.filter(s => s.type === CSSSnippetType.Raw);
|
||
}
|
||
if (config.context.name === CSSAbbreviationScope.Property) {
|
||
return snippets.filter(s => s.type === CSSSnippetType.Property);
|
||
}
|
||
}
|
||
return snippets;
|
||
}
|
||
|
||
var markupSnippets = {
|
||
"a": "a[href]",
|
||
"a:blank": "a[href='http://${0}' target='_blank' rel='noopener noreferrer']",
|
||
"a:link": "a[href='http://${0}']",
|
||
"a:mail": "a[href='mailto:${0}']",
|
||
"a:tel": "a[href='tel:+${0}']",
|
||
"abbr": "abbr[title]",
|
||
"acr|acronym": "acronym[title]",
|
||
"base": "base[href]/",
|
||
"basefont": "basefont/",
|
||
"br": "br/",
|
||
"frame": "frame/",
|
||
"hr": "hr/",
|
||
"bdo": "bdo[dir]",
|
||
"bdo:r": "bdo[dir=rtl]",
|
||
"bdo:l": "bdo[dir=ltr]",
|
||
"col": "col/",
|
||
"link": "link[rel=stylesheet href]/",
|
||
"link:css": "link[href='${1:style}.css']",
|
||
"link:print": "link[href='${1:print}.css' media=print]",
|
||
"link:favicon": "link[rel='shortcut icon' type=image/x-icon href='${1:favicon.ico}']",
|
||
"link:mf|link:manifest": "link[rel='manifest' href='${1:manifest.json}']",
|
||
"link:touch": "link[rel=apple-touch-icon href='${1:favicon.png}']",
|
||
"link:rss": "link[rel=alternate type=application/rss+xml title=RSS href='${1:rss.xml}']",
|
||
"link:atom": "link[rel=alternate type=application/atom+xml title=Atom href='${1:atom.xml}']",
|
||
"link:im|link:import": "link[rel=import href='${1:component}.html']",
|
||
"meta": "meta/",
|
||
"meta:utf": "meta[http-equiv=Content-Type content='text/html;charset=UTF-8']",
|
||
"meta:vp": "meta[name=viewport content='width=${1:device-width}, initial-scale=${2:1.0}']",
|
||
"meta:compat": "meta[http-equiv=X-UA-Compatible content='${1:IE=7}']",
|
||
"meta:edge": "meta:compat[content='${1:ie=edge}']",
|
||
"meta:redirect": "meta[http-equiv=refresh content='0; url=${1:http://example.com}']",
|
||
"meta:refresh": "meta[http-equiv=refresh content='${1:5}']",
|
||
"meta:kw": "meta[name=keywords content]",
|
||
"meta:desc": "meta[name=description content]",
|
||
"style": "style",
|
||
"script": "script",
|
||
"script:src": "script[src]",
|
||
"script:module": "script[type=module src]",
|
||
"img": "img[src alt]/",
|
||
"img:s|img:srcset": "img[srcset src alt]",
|
||
"img:z|img:sizes": "img[sizes srcset src alt]",
|
||
"picture": "picture",
|
||
"src|source": "source/",
|
||
"src:sc|source:src": "source[src type]",
|
||
"src:s|source:srcset": "source[srcset]",
|
||
"src:t|source:type": "source[srcset type='${1:image/}']",
|
||
"src:z|source:sizes": "source[sizes srcset]",
|
||
"src:m|source:media": "source[media='(${1:min-width: })' srcset]",
|
||
"src:mt|source:media:type": "source:media[type='${2:image/}']",
|
||
"src:mz|source:media:sizes": "source:media[sizes srcset]",
|
||
"src:zt|source:sizes:type": "source[sizes srcset type='${1:image/}']",
|
||
"iframe": "iframe[src frameborder=0]",
|
||
"embed": "embed[src type]/",
|
||
"object": "object[data type]",
|
||
"param": "param[name value]/",
|
||
"map": "map[name]",
|
||
"area": "area[shape coords href alt]/",
|
||
"area:d": "area[shape=default]",
|
||
"area:c": "area[shape=circle]",
|
||
"area:r": "area[shape=rect]",
|
||
"area:p": "area[shape=poly]",
|
||
"form": "form[action]",
|
||
"form:get": "form[method=get]",
|
||
"form:post": "form[method=post]",
|
||
"label": "label[for]",
|
||
"input": "input[type=${1:text}]/",
|
||
"inp": "input[name=${1} id=${1}]",
|
||
"input:h|input:hidden": "input[type=hidden name]",
|
||
"input:t|input:text": "inp[type=text]",
|
||
"input:search": "inp[type=search]",
|
||
"input:email": "inp[type=email]",
|
||
"input:url": "inp[type=url]",
|
||
"input:p|input:password": "inp[type=password]",
|
||
"input:datetime": "inp[type=datetime]",
|
||
"input:date": "inp[type=date]",
|
||
"input:datetime-local": "inp[type=datetime-local]",
|
||
"input:month": "inp[type=month]",
|
||
"input:week": "inp[type=week]",
|
||
"input:time": "inp[type=time]",
|
||
"input:tel": "inp[type=tel]",
|
||
"input:number": "inp[type=number]",
|
||
"input:color": "inp[type=color]",
|
||
"input:c|input:checkbox": "inp[type=checkbox]",
|
||
"input:r|input:radio": "inp[type=radio]",
|
||
"input:range": "inp[type=range]",
|
||
"input:f|input:file": "inp[type=file]",
|
||
"input:s|input:submit": "input[type=submit value]",
|
||
"input:i|input:image": "input[type=image src alt]",
|
||
"input:b|input:btn|input:button": "input[type=button value]",
|
||
"input:reset": "input:button[type=reset]",
|
||
"isindex": "isindex/",
|
||
"select": "select[name=${1} id=${1}]",
|
||
"select:d|select:disabled": "select[disabled.]",
|
||
"opt|option": "option[value]",
|
||
"textarea": "textarea[name=${1} id=${1} cols=${2:30} rows=${3:10}]",
|
||
"marquee": "marquee[behavior direction]",
|
||
"menu:c|menu:context": "menu[type=context]",
|
||
"menu:t|menu:toolbar": "menu[type=toolbar]",
|
||
"video": "video[src]",
|
||
"audio": "audio[src]",
|
||
"html:xml": "html[xmlns=http://www.w3.org/1999/xhtml]",
|
||
"keygen": "keygen/",
|
||
"command": "command/",
|
||
"btn:s|button:s|button:submit" : "button[type=submit]",
|
||
"btn:r|button:r|button:reset" : "button[type=reset]",
|
||
"btn:b|button:b|button:button" : "button[type=button]",
|
||
"btn:d|button:d|button:disabled" : "button[disabled.]",
|
||
"fst:d|fset:d|fieldset:d|fieldset:disabled" : "fieldset[disabled.]",
|
||
|
||
"bq": "blockquote",
|
||
"fig": "figure",
|
||
"figc": "figcaption",
|
||
"pic": "picture",
|
||
"ifr": "iframe",
|
||
"emb": "embed",
|
||
"obj": "object",
|
||
"cap": "caption",
|
||
"colg": "colgroup",
|
||
"fst": "fieldset",
|
||
"btn": "button",
|
||
"optg": "optgroup",
|
||
"tarea": "textarea",
|
||
"leg": "legend",
|
||
"sect": "section",
|
||
"art": "article",
|
||
"hdr": "header",
|
||
"ftr": "footer",
|
||
"adr": "address",
|
||
"dlg": "dialog",
|
||
"str": "strong",
|
||
"prog": "progress",
|
||
"mn": "main",
|
||
"tem": "template",
|
||
"fset": "fieldset",
|
||
"datal": "datalist",
|
||
"kg": "keygen",
|
||
"out": "output",
|
||
"det": "details",
|
||
"sum": "summary",
|
||
"cmd": "command",
|
||
"data": "data[value]",
|
||
"meter": "meter[value]",
|
||
"time": "time[datetime]",
|
||
|
||
"ri:d|ri:dpr": "img:s",
|
||
"ri:v|ri:viewport": "img:z",
|
||
"ri:a|ri:art": "pic>src:m+img",
|
||
"ri:t|ri:type": "pic>src:t+img",
|
||
|
||
"!!!": "{<!DOCTYPE html>}",
|
||
"doc": "html[lang=${lang}]>(head>meta[charset=${charset}]+meta:vp+title{${1:Document}})+body",
|
||
"!|html:5": "!!!+doc",
|
||
|
||
"c": "{<!-- ${0} -->}",
|
||
"cc:ie": "{<!--[if IE]>${0}<![endif]-->}",
|
||
"cc:noie": "{<!--[if !IE]><!-->${0}<!--<![endif]-->}"
|
||
};
|
||
|
||
var stylesheetSnippets = {
|
||
"@f": "@font-face {\n\tfont-family: ${1};\n\tsrc: url(${2});\n}",
|
||
"@ff": "@font-face {\n\tfont-family: '${1:FontName}';\n\tsrc: url('${2:FileName}.eot');\n\tsrc: url('${2:FileName}.eot?#iefix') format('embedded-opentype'),\n\t\t url('${2:FileName}.woff') format('woff'),\n\t\t url('${2:FileName}.ttf') format('truetype'),\n\t\t url('${2:FileName}.svg#${1:FontName}') format('svg');\n\tfont-style: ${3:normal};\n\tfont-weight: ${4:normal};\n}",
|
||
"@i|@import": "@import url(${0});",
|
||
"@kf": "@keyframes ${1:identifier} {\n\t${2}\n}",
|
||
"@m|@media": "@media ${1:screen} {\n\t${0}\n}",
|
||
"ac": "align-content:start|end|flex-start|flex-end|center|space-between|space-around|stretch|space-evenly",
|
||
"ai": "align-items:start|end|flex-start|flex-end|center|baseline|stretch",
|
||
"anim": "animation:${1:name} ${2:duration} ${3:timing-function} ${4:delay} ${5:iteration-count} ${6:direction} ${7:fill-mode}",
|
||
"animdel": "animation-delay:time",
|
||
"animdir": "animation-direction:normal|reverse|alternate|alternate-reverse",
|
||
"animdur": "animation-duration:${1:0}s",
|
||
"animfm": "animation-fill-mode:both|forwards|backwards",
|
||
"animic": "animation-iteration-count:1|infinite",
|
||
"animn": "animation-name",
|
||
"animps": "animation-play-state:running|paused",
|
||
"animtf": "animation-timing-function:linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(${1:0.1}, ${2:0.7}, ${3:1.0}, ${3:0.1})",
|
||
"ap": "appearance:none",
|
||
"as": "align-self:start|end|auto|flex-start|flex-end|center|baseline|stretch",
|
||
"b": "bottom",
|
||
"bd": "border:${1:1px} ${2:solid} ${3:#000}",
|
||
"bdb": "border-bottom:${1:1px} ${2:solid} ${3:#000}",
|
||
"bdbc": "border-bottom-color:${1:#000}",
|
||
"bdbi": "border-bottom-image:url(${0})",
|
||
"bdbk": "border-break:close",
|
||
"bdbli": "border-bottom-left-image:url(${0})|continue",
|
||
"bdblrs": "border-bottom-left-radius",
|
||
"bdbri": "border-bottom-right-image:url(${0})|continue",
|
||
"bdbrrs": "border-bottom-right-radius",
|
||
"bdbs": "border-bottom-style",
|
||
"bdbw": "border-bottom-width",
|
||
"bdc": "border-color:${1:#000}",
|
||
"bdci": "border-corner-image:url(${0})|continue",
|
||
"bdcl": "border-collapse:collapse|separate",
|
||
"bdf": "border-fit:repeat|clip|scale|stretch|overwrite|overflow|space",
|
||
"bdi": "border-image:url(${0})",
|
||
"bdl": "border-left:${1:1px} ${2:solid} ${3:#000}",
|
||
"bdlc": "border-left-color:${1:#000}",
|
||
"bdlen": "border-length",
|
||
"bdli": "border-left-image:url(${0})",
|
||
"bdls": "border-left-style",
|
||
"bdlw": "border-left-width",
|
||
"bdr": "border-right:${1:1px} ${2:solid} ${3:#000}",
|
||
"bdrc": "border-right-color:${1:#000}",
|
||
"bdri": "border-right-image:url(${0})",
|
||
"bdrs": "border-radius",
|
||
"bdrst": "border-right-style",
|
||
"bdrw": "border-right-width",
|
||
"bds": "border-style:none|hidden|dotted|dashed|solid|double|dot-dash|dot-dot-dash|wave|groove|ridge|inset|outset",
|
||
"bdsp": "border-spacing",
|
||
"bdt": "border-top:${1:1px} ${2:solid} ${3:#000}",
|
||
"bdtc": "border-top-color:${1:#000}",
|
||
"bdti": "border-top-image:url(${0})",
|
||
"bdtli": "border-top-left-image:url(${0})|continue",
|
||
"bdtlrs": "border-top-left-radius",
|
||
"bdtri": "border-top-right-image:url(${0})|continue",
|
||
"bdtrrs": "border-top-right-radius",
|
||
"bdts": "border-top-style",
|
||
"bdtw": "border-top-width",
|
||
"bdw": "border-width",
|
||
"bbs": "border-block-start",
|
||
"bbe": "border-block-end",
|
||
"bis": "border-inline-start",
|
||
"bie": "border-inline-end",
|
||
"bfv": "backface-visibility:hidden|visible",
|
||
"bg": "background:${1:#000}",
|
||
"bg:n": "background: none",
|
||
"bga": "background-attachment:fixed|scroll",
|
||
"bgbk": "background-break:bounding-box|each-box|continuous",
|
||
"bgc": "background-color:${1:#fff}",
|
||
"bgcp": "background-clip:padding-box|border-box|content-box|no-clip",
|
||
"bgi": "background-image:url(${0})",
|
||
"bgo": "background-origin:padding-box|border-box|content-box",
|
||
"bgp": "background-position:${1:0} ${2:0}",
|
||
"bgpx": "background-position-x",
|
||
"bgpy": "background-position-y",
|
||
"bgr": "background-repeat:no-repeat|repeat-x|repeat-y|space|round",
|
||
"bgsz": "background-size:contain|cover",
|
||
"bs": "block-size",
|
||
"bxsh": "box-shadow:${1:inset }${2:hoff} ${3:voff} ${4:blur} ${5:#000}|none",
|
||
"bxsz": "box-sizing:border-box|content-box|border-box",
|
||
"c": "color:${1:#000}",
|
||
"cr": "color:rgb(${1:0}, ${2:0}, ${3:0})",
|
||
"cra": "color:rgba(${1:0}, ${2:0}, ${3:0}, ${4:.5})",
|
||
"cl": "clear:both|left|right|none",
|
||
"cm": "/* ${0} */",
|
||
"cnt": "content:'${0}'|normal|open-quote|no-open-quote|close-quote|no-close-quote|attr(${0})|counter(${0})|counters(${0})",
|
||
"coi": "counter-increment",
|
||
"colm": "columns",
|
||
"colmc": "column-count",
|
||
"colmf": "column-fill",
|
||
"colmg": "column-gap",
|
||
"colmr": "column-rule",
|
||
"colmrc": "column-rule-color",
|
||
"colmrs": "column-rule-style",
|
||
"colmrw": "column-rule-width",
|
||
"colms": "column-span",
|
||
"colmw": "column-width",
|
||
"cor": "counter-reset",
|
||
"cp": "clip:auto|rect(${1:top} ${2:right} ${3:bottom} ${4:left})",
|
||
"cps": "caption-side:top|bottom",
|
||
"cur": "cursor:pointer|auto|default|crosshair|hand|help|move|pointer|text",
|
||
"d": "display:block|none|flex|inline-flex|inline|inline-block|grid|inline-grid|subgrid|list-item|run-in|contents|table|inline-table|table-caption|table-column|table-column-group|table-header-group|table-footer-group|table-row|table-row-group|table-cell|ruby|ruby-base|ruby-base-group|ruby-text|ruby-text-group",
|
||
"ec": "empty-cells:show|hide",
|
||
"f": "font:${1:1em} ${2:sans-serif}",
|
||
"fd": "font-display:auto|block|swap|fallback|optional",
|
||
"fef": "font-effect:none|engrave|emboss|outline",
|
||
"fem": "font-emphasize",
|
||
"femp": "font-emphasize-position:before|after",
|
||
"fems": "font-emphasize-style:none|accent|dot|circle|disc",
|
||
"ff": "font-family:serif|sans-serif|cursive|fantasy|monospace",
|
||
"fft": "font-family:\"Times New Roman\", Times, Baskerville, Georgia, serif",
|
||
"ffa": "font-family:Arial, \"Helvetica Neue\", Helvetica, sans-serif",
|
||
"ffv": "font-family:Verdana, Geneva, sans-serif",
|
||
"fl": "float:left|right|none",
|
||
"fs": "font-style:italic|normal|oblique",
|
||
"fsm": "font-smoothing:antialiased|subpixel-antialiased|none",
|
||
"fst": "font-stretch:normal|ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded",
|
||
"fv": "font-variant:normal|small-caps",
|
||
"fvs": "font-variation-settings:normal|inherit|initial|unset",
|
||
"fw": "font-weight:normal|bold|bolder|lighter",
|
||
"fx": "flex",
|
||
"fxb": "flex-basis:fill|max-content|min-content|fit-content|content",
|
||
"fxd": "flex-direction:row|row-reverse|column|column-reverse",
|
||
"fxf": "flex-flow",
|
||
"fxg": "flex-grow",
|
||
"fxsh": "flex-shrink",
|
||
"fxw": "flex-wrap:nowrap|wrap|wrap-reverse",
|
||
"fsz": "font-size",
|
||
"fsza": "font-size-adjust",
|
||
"g": "gap",
|
||
"gtc": "grid-template-columns:repeat(${0})|minmax()",
|
||
"gtr": "grid-template-rows:repeat(${0})|minmax()",
|
||
"gta": "grid-template-areas",
|
||
"gt": "grid-template",
|
||
"gg": "grid-gap",
|
||
"gcg": "grid-column-gap",
|
||
"grg": "grid-row-gap",
|
||
"gac": "grid-auto-columns:auto|minmax()",
|
||
"gar": "grid-auto-rows:auto|minmax()",
|
||
"gaf": "grid-auto-flow:row|column|dense|inherit|initial|unset",
|
||
"gd": "grid",
|
||
"gc": "grid-column",
|
||
"gcs": "grid-column-start",
|
||
"gce": "grid-column-end",
|
||
"gr": "grid-row",
|
||
"grs": "grid-row-start",
|
||
"gre": "grid-row-end",
|
||
"ga": "grid-area",
|
||
"h": "height",
|
||
"is": "inline-size",
|
||
"jc": "justify-content:start|end|stretch|flex-start|flex-end|center|space-between|space-around|space-evenly",
|
||
"ji": "justify-items:start|end|center|stretch",
|
||
"js": "justify-self:start|end|center|stretch",
|
||
"l": "left",
|
||
"lg": "background-image:linear-gradient(${1})",
|
||
"lh": "line-height",
|
||
"lis": "list-style",
|
||
"lisi": "list-style-image",
|
||
"lisp": "list-style-position:inside|outside",
|
||
"list": "list-style-type:disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman",
|
||
"lts": "letter-spacing:normal",
|
||
"m": "margin",
|
||
"mah": "max-height",
|
||
"mar": "max-resolution",
|
||
"maw": "max-width",
|
||
"mb": "margin-bottom",
|
||
"mih": "min-height",
|
||
"mir": "min-resolution",
|
||
"miw": "min-width",
|
||
"ml": "margin-left",
|
||
"mr": "margin-right",
|
||
"mt": "margin-top",
|
||
"mbs": "margin-block-start",
|
||
"mbe": "margin-block-end",
|
||
"mis": "margin-inline-start",
|
||
"mie": "margin-inline-end",
|
||
"ol": "outline",
|
||
"olc": "outline-color:${1:#000}|invert",
|
||
"olo": "outline-offset",
|
||
"ols": "outline-style:none|dotted|dashed|solid|double|groove|ridge|inset|outset",
|
||
"olw": "outline-width:thin|medium|thick",
|
||
"op|opa": "opacity",
|
||
"ord": "order",
|
||
"ori": "orientation:landscape|portrait",
|
||
"orp": "orphans",
|
||
"ov": "overflow:hidden|visible|hidden|scroll|auto",
|
||
"ovs": "overflow-style:scrollbar|auto|scrollbar|panner|move|marquee",
|
||
"ovx": "overflow-x:hidden|visible|hidden|scroll|auto",
|
||
"ovy": "overflow-y:hidden|visible|hidden|scroll|auto",
|
||
"p": "padding",
|
||
"pb": "padding-bottom",
|
||
"pgba": "page-break-after:auto|always|left|right",
|
||
"pgbb": "page-break-before:auto|always|left|right",
|
||
"pgbi": "page-break-inside:auto|avoid",
|
||
"pl": "padding-left",
|
||
"pos": "position:relative|absolute|relative|fixed|static",
|
||
"pr": "padding-right",
|
||
"pt": "padding-top",
|
||
"pbs": "padding-block-start",
|
||
"pbe": "padding-block-end",
|
||
"pis": "padding-inline-start",
|
||
"pie": "padding-inline-end",
|
||
"spbs": "scroll-padding-block-start",
|
||
"spbe": "scroll-padding-block-end",
|
||
"spis": "scroll-padding-inline-start",
|
||
"spie": "scroll-padding-inline-end",
|
||
"q": "quotes",
|
||
"qen": "quotes:'\\201C' '\\201D' '\\2018' '\\2019'",
|
||
"qru": "quotes:'\\00AB' '\\00BB' '\\201E' '\\201C'",
|
||
"r": "right",
|
||
"rsz": "resize:none|both|horizontal|vertical",
|
||
"t": "top",
|
||
"ta": "text-align:left|center|right|justify",
|
||
"tal": "text-align-last:left|center|right",
|
||
"tbl": "table-layout:fixed",
|
||
"td": "text-decoration:none|underline|overline|line-through",
|
||
"te": "text-emphasis:none|accent|dot|circle|disc|before|after",
|
||
"th": "text-height:auto|font-size|text-size|max-size",
|
||
"ti": "text-indent",
|
||
"tj": "text-justify:auto|inter-word|inter-ideograph|inter-cluster|distribute|kashida|tibetan",
|
||
"to": "text-outline:${1:0} ${2:0} ${3:#000}",
|
||
"tov": "text-overflow:ellipsis|clip",
|
||
"tr": "text-replace",
|
||
"trf": "transform:${1}|skewX(${1:angle})|skewY(${1:angle})|scale(${1:x}, ${2:y})|scaleX(${1:x})|scaleY(${1:y})|scaleZ(${1:z})|scale3d(${1:x}, ${2:y}, ${3:z})|rotate(${1:angle})|rotateX(${1:angle})|rotateY(${1:angle})|rotateZ(${1:angle})|translate(${1:x}, ${2:y})|translateX(${1:x})|translateY(${1:y})|translateZ(${1:z})|translate3d(${1:tx}, ${2:ty}, ${3:tz})",
|
||
"trfo": "transform-origin",
|
||
"trfs": "transform-style:preserve-3d",
|
||
"trs": "transition:${1:prop} ${2:time}",
|
||
"trsde": "transition-delay:${1:time}",
|
||
"trsdu": "transition-duration:${1:time}",
|
||
"trsp": "transition-property:${1:prop}",
|
||
"trstf": "transition-timing-function:${1:fn}",
|
||
"tsh": "text-shadow:${1:hoff} ${2:voff} ${3:blur} ${4:#000}",
|
||
"tt": "text-transform:uppercase|lowercase|capitalize|none",
|
||
"tw": "text-wrap:none|normal|unrestricted|suppress",
|
||
"us": "user-select:none",
|
||
"v": "visibility:hidden|visible|collapse",
|
||
"va": "vertical-align:top|super|text-top|middle|baseline|bottom|text-bottom|sub",
|
||
"w": "width",
|
||
"whs": "white-space:nowrap|pre|pre-wrap|pre-line|normal",
|
||
"whsc": "white-space-collapse:normal|keep-all|loose|break-strict|break-all",
|
||
"wid": "widows",
|
||
"wm": "writing-mode:lr-tb|lr-tb|lr-bt|rl-tb|rl-bt|tb-rl|tb-lr|bt-lr|bt-rl",
|
||
"wob": "word-break:normal|keep-all|break-all",
|
||
"wos": "word-spacing",
|
||
"wow": "word-wrap:none|unrestricted|suppress|break-word|normal",
|
||
"z": "z-index",
|
||
"zom": "zoom:1"
|
||
};
|
||
|
||
var xslSnippets = {
|
||
"tm|tmatch": "xsl:template[match mode]",
|
||
"tn|tname": "xsl:template[name]",
|
||
"call": "xsl:call-template[name]",
|
||
"ap": "xsl:apply-templates[select mode]",
|
||
"api": "xsl:apply-imports",
|
||
"imp": "xsl:import[href]",
|
||
"inc": "xsl:include[href]",
|
||
"ch": "xsl:choose",
|
||
"wh|xsl:when": "xsl:when[test]",
|
||
"ot": "xsl:otherwise",
|
||
"if": "xsl:if[test]",
|
||
"par": "xsl:param[name]",
|
||
"pare": "xsl:param[name select]",
|
||
"var": "xsl:variable[name]",
|
||
"vare": "xsl:variable[name select]",
|
||
"wp": "xsl:with-param[name select]",
|
||
"key": "xsl:key[name match use]",
|
||
"elem": "xsl:element[name]",
|
||
"attr": "xsl:attribute[name]",
|
||
"attrs": "xsl:attribute-set[name]",
|
||
"cp": "xsl:copy[select]",
|
||
"co": "xsl:copy-of[select]",
|
||
"val": "xsl:value-of[select]",
|
||
"for|each": "xsl:for-each[select]",
|
||
"tex": "xsl:text",
|
||
"com": "xsl:comment",
|
||
"msg": "xsl:message[terminate=no]",
|
||
"fall": "xsl:fallback",
|
||
"num": "xsl:number[value]",
|
||
"nam": "namespace-alias[stylesheet-prefix result-prefix]",
|
||
"pres": "xsl:preserve-space[elements]",
|
||
"strip": "xsl:strip-space[elements]",
|
||
"proc": "xsl:processing-instruction[name]",
|
||
"sort": "xsl:sort[select order]",
|
||
"choose": "xsl:choose>xsl:when+xsl:otherwise",
|
||
"xsl": "!!!+xsl:stylesheet[version=1.0 xmlns:xsl=http://www.w3.org/1999/XSL/Transform]>{\n|}",
|
||
"!!!": "{<?xml version=\"1.0\" encoding=\"UTF-8\"?>}"
|
||
};
|
||
|
||
var pugSnippets = {
|
||
"!!!": "{doctype html}"
|
||
};
|
||
|
||
var variables = {
|
||
"lang": "en",
|
||
"locale": "en-US",
|
||
"charset": "UTF-8",
|
||
"indentation": "\t",
|
||
"newline": "\n"
|
||
};
|
||
|
||
/**
|
||
* Default syntaxes for abbreviation types
|
||
*/
|
||
const defaultSyntaxes = {
|
||
markup: 'html',
|
||
stylesheet: 'css'
|
||
};
|
||
const defaultOptions$1 = {
|
||
'inlineElements': [
|
||
'a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo',
|
||
'big', 'br', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i',
|
||
'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'map', 'object', 'q',
|
||
's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup',
|
||
'textarea', 'tt', 'u', 'var'
|
||
],
|
||
'output.indent': '\t',
|
||
'output.baseIndent': '',
|
||
'output.newline': '\n',
|
||
'output.tagCase': '',
|
||
'output.attributeCase': '',
|
||
'output.attributeQuotes': 'double',
|
||
'output.format': true,
|
||
'output.formatLeafNode': false,
|
||
'output.formatSkip': ['html'],
|
||
'output.formatForce': ['body'],
|
||
'output.inlineBreak': 3,
|
||
'output.compactBoolean': false,
|
||
'output.booleanAttributes': [
|
||
'contenteditable', 'seamless', 'async', 'autofocus',
|
||
'autoplay', 'checked', 'controls', 'defer', 'disabled', 'formnovalidate',
|
||
'hidden', 'ismap', 'loop', 'multiple', 'muted', 'novalidate', 'readonly',
|
||
'required', 'reversed', 'selected', 'typemustmatch'
|
||
],
|
||
'output.reverseAttributes': false,
|
||
'output.selfClosingStyle': 'html',
|
||
'output.field': (index, placeholder) => placeholder,
|
||
'output.text': text => text,
|
||
'markup.href': true,
|
||
'comment.enabled': false,
|
||
'comment.trigger': ['id', 'class'],
|
||
'comment.before': '',
|
||
'comment.after': '\n<!-- /[#ID][.CLASS] -->',
|
||
'bem.enabled': false,
|
||
'bem.element': '__',
|
||
'bem.modifier': '_',
|
||
'jsx.enabled': false,
|
||
'stylesheet.keywords': ['auto', 'inherit', 'unset', 'none'],
|
||
'stylesheet.unitless': ['z-index', 'line-height', 'opacity', 'font-weight', 'zoom', 'flex', 'flex-grow', 'flex-shrink'],
|
||
'stylesheet.shortHex': true,
|
||
'stylesheet.between': ': ',
|
||
'stylesheet.after': ';',
|
||
'stylesheet.intUnit': 'px',
|
||
'stylesheet.floatUnit': 'em',
|
||
'stylesheet.unitAliases': { e: 'em', p: '%', x: 'ex', r: 'rem' },
|
||
'stylesheet.json': false,
|
||
'stylesheet.jsonDoubleQuotes': false,
|
||
'stylesheet.fuzzySearchMinScore': 0
|
||
};
|
||
const defaultConfig = {
|
||
type: 'markup',
|
||
syntax: 'html',
|
||
variables,
|
||
snippets: {},
|
||
options: defaultOptions$1
|
||
};
|
||
/**
|
||
* Default per-syntax config
|
||
*/
|
||
const syntaxConfig = {
|
||
markup: {
|
||
snippets: parseSnippets(markupSnippets),
|
||
},
|
||
xhtml: {
|
||
options: {
|
||
'output.selfClosingStyle': 'xhtml'
|
||
}
|
||
},
|
||
xml: {
|
||
options: {
|
||
'output.selfClosingStyle': 'xml'
|
||
}
|
||
},
|
||
xsl: {
|
||
snippets: parseSnippets(xslSnippets),
|
||
options: {
|
||
'output.selfClosingStyle': 'xml'
|
||
}
|
||
},
|
||
jsx: {
|
||
options: {
|
||
'jsx.enabled': true,
|
||
'markup.attributes': {
|
||
'class': 'className',
|
||
'class*': 'styleName',
|
||
'for': 'htmlFor'
|
||
},
|
||
'markup.valuePrefix': {
|
||
'class*': 'styles'
|
||
}
|
||
}
|
||
},
|
||
vue: {
|
||
options: {
|
||
'markup.attributes': {
|
||
'class*': ':class',
|
||
}
|
||
}
|
||
},
|
||
svelte: {
|
||
options: {
|
||
'jsx.enabled': true
|
||
}
|
||
},
|
||
pug: {
|
||
snippets: parseSnippets(pugSnippets)
|
||
},
|
||
stylesheet: {
|
||
snippets: parseSnippets(stylesheetSnippets)
|
||
},
|
||
sass: {
|
||
options: {
|
||
'stylesheet.after': ''
|
||
}
|
||
},
|
||
stylus: {
|
||
options: {
|
||
'stylesheet.between': ' ',
|
||
'stylesheet.after': '',
|
||
}
|
||
}
|
||
};
|
||
/**
|
||
* Parses raw snippets definitions with possibly multiple keys into a plan
|
||
* snippet map
|
||
*/
|
||
function parseSnippets(snippets) {
|
||
const result = {};
|
||
Object.keys(snippets).forEach(k => {
|
||
for (const name of k.split('|')) {
|
||
result[name] = snippets[k];
|
||
}
|
||
});
|
||
return result;
|
||
}
|
||
function resolveConfig(config = {}, globals = {}) {
|
||
const type = config.type || 'markup';
|
||
const syntax = config.syntax || defaultSyntaxes[type];
|
||
return Object.assign(Object.assign(Object.assign({}, defaultConfig), config), { type,
|
||
syntax, variables: mergedData(type, syntax, 'variables', config, globals), snippets: mergedData(type, syntax, 'snippets', config, globals), options: mergedData(type, syntax, 'options', config, globals) });
|
||
}
|
||
function mergedData(type, syntax, key, config, globals = {}) {
|
||
const typeDefaults = syntaxConfig[type];
|
||
const typeOverride = globals[type];
|
||
const syntaxDefaults = syntaxConfig[syntax];
|
||
const syntaxOverride = globals[syntax];
|
||
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, defaultConfig[key]), (typeDefaults && typeDefaults[key])), (syntaxDefaults && syntaxDefaults[key])), (typeOverride && typeOverride[key])), (syntaxOverride && syntaxOverride[key])), config[key]);
|
||
}
|
||
|
||
/**
|
||
* Creates structure for scanning given string in backward direction
|
||
*/
|
||
function backwardScanner(text, start = 0) {
|
||
return { text, start, pos: text.length };
|
||
}
|
||
/**
|
||
* Check if given scanner position is at start of scanned text
|
||
*/
|
||
function sol(scanner) {
|
||
return scanner.pos === scanner.start;
|
||
}
|
||
/**
|
||
* “Peeks” character code an current scanner location without advancing it
|
||
*/
|
||
function peek(scanner, offset = 0) {
|
||
return scanner.text.charCodeAt(scanner.pos - 1 + offset);
|
||
}
|
||
/**
|
||
* Returns current character code and moves character location one symbol back
|
||
*/
|
||
function previous(scanner) {
|
||
if (!sol(scanner)) {
|
||
return scanner.text.charCodeAt(--scanner.pos);
|
||
}
|
||
}
|
||
/**
|
||
* Consumes current character code if it matches given `match` code or function
|
||
*/
|
||
function consume(scanner, match) {
|
||
if (sol(scanner)) {
|
||
return false;
|
||
}
|
||
const ok = typeof match === 'function'
|
||
? match(peek(scanner))
|
||
: match === peek(scanner);
|
||
if (ok) {
|
||
scanner.pos--;
|
||
}
|
||
return !!ok;
|
||
}
|
||
function consumeWhile(scanner, match) {
|
||
const start = scanner.pos;
|
||
while (consume(scanner, match)) {
|
||
// empty
|
||
}
|
||
return scanner.pos < start;
|
||
}
|
||
|
||
var Chars$1;
|
||
(function (Chars) {
|
||
Chars[Chars["SingleQuote"] = 39] = "SingleQuote";
|
||
Chars[Chars["DoubleQuote"] = 34] = "DoubleQuote";
|
||
Chars[Chars["Escape"] = 92] = "Escape";
|
||
})(Chars$1 || (Chars$1 = {}));
|
||
/**
|
||
* Check if given character code is a quote
|
||
*/
|
||
function isQuote(c) {
|
||
return c === Chars$1.SingleQuote || c === Chars$1.DoubleQuote;
|
||
}
|
||
/**
|
||
* Consumes quoted value, if possible
|
||
* @return Returns `true` is value was consumed
|
||
*/
|
||
function consumeQuoted(scanner) {
|
||
const start = scanner.pos;
|
||
const quote = previous(scanner);
|
||
if (isQuote(quote)) {
|
||
while (!sol(scanner)) {
|
||
if (previous(scanner) === quote && peek(scanner) !== Chars$1.Escape) {
|
||
return true;
|
||
}
|
||
}
|
||
}
|
||
scanner.pos = start;
|
||
return false;
|
||
}
|
||
|
||
var Brackets;
|
||
(function (Brackets) {
|
||
Brackets[Brackets["SquareL"] = 91] = "SquareL";
|
||
Brackets[Brackets["SquareR"] = 93] = "SquareR";
|
||
Brackets[Brackets["RoundL"] = 40] = "RoundL";
|
||
Brackets[Brackets["RoundR"] = 41] = "RoundR";
|
||
Brackets[Brackets["CurlyL"] = 123] = "CurlyL";
|
||
Brackets[Brackets["CurlyR"] = 125] = "CurlyR";
|
||
})(Brackets || (Brackets = {}));
|
||
const bracePairs = {
|
||
[Brackets.SquareL]: Brackets.SquareR,
|
||
[Brackets.RoundL]: Brackets.RoundR,
|
||
[Brackets.CurlyL]: Brackets.CurlyR,
|
||
};
|
||
|
||
var Chars;
|
||
(function (Chars) {
|
||
Chars[Chars["Tab"] = 9] = "Tab";
|
||
Chars[Chars["Space"] = 32] = "Space";
|
||
/** `-` character */
|
||
Chars[Chars["Dash"] = 45] = "Dash";
|
||
/** `/` character */
|
||
Chars[Chars["Slash"] = 47] = "Slash";
|
||
/** `:` character */
|
||
Chars[Chars["Colon"] = 58] = "Colon";
|
||
/** `=` character */
|
||
Chars[Chars["Equals"] = 61] = "Equals";
|
||
/** `<` character */
|
||
Chars[Chars["AngleLeft"] = 60] = "AngleLeft";
|
||
/** `>` character */
|
||
Chars[Chars["AngleRight"] = 62] = "AngleRight";
|
||
})(Chars || (Chars = {}));
|
||
/**
|
||
* Check if given reader’s current position points at the end of HTML tag
|
||
*/
|
||
function isHtml(scanner) {
|
||
const start = scanner.pos;
|
||
if (!consume(scanner, Chars.AngleRight)) {
|
||
return false;
|
||
}
|
||
let ok = false;
|
||
consume(scanner, Chars.Slash); // possibly self-closed element
|
||
while (!sol(scanner)) {
|
||
consumeWhile(scanner, isWhiteSpace);
|
||
if (consumeIdent(scanner)) {
|
||
// ate identifier: could be a tag name, boolean attribute or unquoted
|
||
// attribute value
|
||
if (consume(scanner, Chars.Slash)) {
|
||
// either closing tag or invalid tag
|
||
ok = consume(scanner, Chars.AngleLeft);
|
||
break;
|
||
}
|
||
else if (consume(scanner, Chars.AngleLeft)) {
|
||
// opening tag
|
||
ok = true;
|
||
break;
|
||
}
|
||
else if (consume(scanner, isWhiteSpace)) {
|
||
// boolean attribute
|
||
continue;
|
||
}
|
||
else if (consume(scanner, Chars.Equals)) {
|
||
// simple unquoted value or invalid attribute
|
||
if (consumeIdent(scanner)) {
|
||
continue;
|
||
}
|
||
break;
|
||
}
|
||
else if (consumeAttributeWithUnquotedValue(scanner)) {
|
||
// identifier was a part of unquoted value
|
||
ok = true;
|
||
break;
|
||
}
|
||
// invalid tag
|
||
break;
|
||
}
|
||
if (consumeAttribute(scanner)) {
|
||
continue;
|
||
}
|
||
break;
|
||
}
|
||
scanner.pos = start;
|
||
return ok;
|
||
}
|
||
/**
|
||
* Consumes HTML attribute from given string.
|
||
* @return `true` if attribute was consumed.
|
||
*/
|
||
function consumeAttribute(scanner) {
|
||
return consumeAttributeWithQuotedValue(scanner) || consumeAttributeWithUnquotedValue(scanner);
|
||
}
|
||
function consumeAttributeWithQuotedValue(scanner) {
|
||
const start = scanner.pos;
|
||
if (consumeQuoted(scanner) && consume(scanner, Chars.Equals) && consumeIdent(scanner)) {
|
||
return true;
|
||
}
|
||
scanner.pos = start;
|
||
return false;
|
||
}
|
||
function consumeAttributeWithUnquotedValue(scanner) {
|
||
const start = scanner.pos;
|
||
const stack = [];
|
||
while (!sol(scanner)) {
|
||
const ch = peek(scanner);
|
||
if (isCloseBracket(ch)) {
|
||
stack.push(ch);
|
||
}
|
||
else if (isOpenBracket(ch)) {
|
||
if (stack.pop() !== bracePairs[ch]) {
|
||
// Unexpected open bracket
|
||
break;
|
||
}
|
||
}
|
||
else if (!isUnquotedValue(ch)) {
|
||
break;
|
||
}
|
||
scanner.pos--;
|
||
}
|
||
if (start !== scanner.pos && consume(scanner, Chars.Equals) && consumeIdent(scanner)) {
|
||
return true;
|
||
}
|
||
scanner.pos = start;
|
||
return false;
|
||
}
|
||
/**
|
||
* Consumes HTML identifier from stream
|
||
*/
|
||
function consumeIdent(scanner) {
|
||
return consumeWhile(scanner, isIdent);
|
||
}
|
||
/**
|
||
* Check if given character code belongs to HTML identifier
|
||
*/
|
||
function isIdent(ch) {
|
||
return ch === Chars.Colon || ch === Chars.Dash || isAlpha(ch) || isNumber(ch);
|
||
}
|
||
/**
|
||
* Check if given character code is alpha code (letter though A to Z)
|
||
*/
|
||
function isAlpha(ch) {
|
||
ch &= ~32; // quick hack to convert any char code to uppercase char code
|
||
return ch >= 65 && ch <= 90; // A-Z
|
||
}
|
||
/**
|
||
* Check if given code is a number
|
||
*/
|
||
function isNumber(ch) {
|
||
return ch > 47 && ch < 58;
|
||
}
|
||
/**
|
||
* Check if given code is a whitespace
|
||
*/
|
||
function isWhiteSpace(ch) {
|
||
return ch === Chars.Space || ch === Chars.Tab;
|
||
}
|
||
/**
|
||
* Check if given code may belong to unquoted attribute value
|
||
*/
|
||
function isUnquotedValue(ch) {
|
||
return !isNaN(ch) && ch !== Chars.Equals && !isWhiteSpace(ch) && !isQuote(ch);
|
||
}
|
||
function isOpenBracket(ch) {
|
||
return ch === Brackets.CurlyL || ch === Brackets.RoundL || ch === Brackets.SquareL;
|
||
}
|
||
function isCloseBracket(ch) {
|
||
return ch === Brackets.CurlyR || ch === Brackets.RoundR || ch === Brackets.SquareR;
|
||
}
|
||
|
||
const code = (ch) => ch.charCodeAt(0);
|
||
const specialChars = '#.*:$-_!@%^+>/'.split('').map(code);
|
||
const defaultOptions = {
|
||
type: 'markup',
|
||
lookAhead: true,
|
||
prefix: ''
|
||
};
|
||
/**
|
||
* Extracts Emmet abbreviation from given string.
|
||
* The goal of this module is to extract abbreviation from current editor’s line,
|
||
* e.g. like this: `<span>.foo[title=bar|]</span>` -> `.foo[title=bar]`, where
|
||
* `|` is a current caret position.
|
||
* @param line A text line where abbreviation should be expanded
|
||
* @param pos Caret position in line. If not given, uses end of line
|
||
* @param options Extracting options
|
||
*/
|
||
function extractAbbreviation(line, pos = line.length, options = {}) {
|
||
// make sure `pos` is within line range
|
||
const opt = Object.assign(Object.assign({}, defaultOptions), options);
|
||
pos = Math.min(line.length, Math.max(0, pos == null ? line.length : pos));
|
||
if (opt.lookAhead) {
|
||
pos = offsetPastAutoClosed(line, pos, opt);
|
||
}
|
||
let ch;
|
||
const start = getStartOffset(line, pos, opt.prefix || '');
|
||
if (start === -1) {
|
||
return void 0;
|
||
}
|
||
const scanner = backwardScanner(line, start);
|
||
scanner.pos = pos;
|
||
const stack = [];
|
||
while (!sol(scanner)) {
|
||
ch = peek(scanner);
|
||
if (stack.includes(Brackets.CurlyR)) {
|
||
if (ch === Brackets.CurlyR) {
|
||
stack.push(ch);
|
||
scanner.pos--;
|
||
continue;
|
||
}
|
||
if (ch !== Brackets.CurlyL) {
|
||
scanner.pos--;
|
||
continue;
|
||
}
|
||
}
|
||
if (isCloseBrace(ch, opt.type)) {
|
||
stack.push(ch);
|
||
}
|
||
else if (isOpenBrace(ch, opt.type)) {
|
||
if (stack.pop() !== bracePairs[ch]) {
|
||
// unexpected brace
|
||
break;
|
||
}
|
||
}
|
||
else if (stack.includes(Brackets.SquareR) || stack.includes(Brackets.CurlyR)) {
|
||
// respect all characters inside attribute sets or text nodes
|
||
scanner.pos--;
|
||
continue;
|
||
}
|
||
else if (isHtml(scanner) || !isAbbreviation(ch)) {
|
||
break;
|
||
}
|
||
scanner.pos--;
|
||
}
|
||
if (!stack.length && scanner.pos !== pos) {
|
||
// Found something, remove some invalid symbols from the
|
||
// beginning and return abbreviation
|
||
const abbreviation = line.slice(scanner.pos, pos).replace(/^[*+>^]+/, '');
|
||
return {
|
||
abbreviation,
|
||
location: pos - abbreviation.length,
|
||
start: options.prefix
|
||
? start - options.prefix.length
|
||
: pos - abbreviation.length,
|
||
end: pos
|
||
};
|
||
}
|
||
}
|
||
/**
|
||
* Returns new `line` index which is right after characters beyound `pos` that
|
||
* editor will likely automatically close, e.g. }, ], and quotes
|
||
*/
|
||
function offsetPastAutoClosed(line, pos, options) {
|
||
// closing quote is allowed only as a next character
|
||
if (isQuote(line.charCodeAt(pos))) {
|
||
pos++;
|
||
}
|
||
// offset pointer until non-autoclosed character is found
|
||
while (isCloseBrace(line.charCodeAt(pos), options.type)) {
|
||
pos++;
|
||
}
|
||
return pos;
|
||
}
|
||
/**
|
||
* Returns start offset (left limit) in `line` where we should stop looking for
|
||
* abbreviation: it’s nearest to `pos` location of `prefix` token
|
||
*/
|
||
function getStartOffset(line, pos, prefix) {
|
||
if (!prefix) {
|
||
return 0;
|
||
}
|
||
const scanner = backwardScanner(line);
|
||
const compiledPrefix = prefix.split('').map(code);
|
||
scanner.pos = pos;
|
||
let result;
|
||
while (!sol(scanner)) {
|
||
if (consumePair(scanner, Brackets.SquareR, Brackets.SquareL) || consumePair(scanner, Brackets.CurlyR, Brackets.CurlyL)) {
|
||
continue;
|
||
}
|
||
result = scanner.pos;
|
||
if (consumeArray(scanner, compiledPrefix)) {
|
||
return result;
|
||
}
|
||
scanner.pos--;
|
||
}
|
||
return -1;
|
||
}
|
||
/**
|
||
* Consumes full character pair, if possible
|
||
*/
|
||
function consumePair(scanner, close, open) {
|
||
const start = scanner.pos;
|
||
if (consume(scanner, close)) {
|
||
while (!sol(scanner)) {
|
||
if (consume(scanner, open)) {
|
||
return true;
|
||
}
|
||
scanner.pos--;
|
||
}
|
||
}
|
||
scanner.pos = start;
|
||
return false;
|
||
}
|
||
/**
|
||
* Consumes all character codes from given array, right-to-left, if possible
|
||
*/
|
||
function consumeArray(scanner, arr) {
|
||
const start = scanner.pos;
|
||
let consumed = false;
|
||
for (let i = arr.length - 1; i >= 0 && !sol(scanner); i--) {
|
||
if (!consume(scanner, arr[i])) {
|
||
break;
|
||
}
|
||
consumed = i === 0;
|
||
}
|
||
if (!consumed) {
|
||
scanner.pos = start;
|
||
}
|
||
return consumed;
|
||
}
|
||
function isAbbreviation(ch) {
|
||
return (ch > 64 && ch < 91) // uppercase letter
|
||
|| (ch > 96 && ch < 123) // lowercase letter
|
||
|| (ch > 47 && ch < 58) // number
|
||
|| specialChars.includes(ch); // special character
|
||
}
|
||
function isOpenBrace(ch, syntax) {
|
||
return ch === Brackets.RoundL || (syntax === 'markup' && (ch === Brackets.SquareL || ch === Brackets.CurlyL));
|
||
}
|
||
function isCloseBrace(ch, syntax) {
|
||
return ch === Brackets.RoundR || (syntax === 'markup' && (ch === Brackets.SquareR || ch === Brackets.CurlyR));
|
||
}
|
||
|
||
function expandAbbreviation(abbr, config) {
|
||
const resolvedConfig = resolveConfig(config);
|
||
return resolvedConfig.type === 'stylesheet'
|
||
? stylesheet(abbr, resolvedConfig)
|
||
: markup(abbr, resolvedConfig);
|
||
}
|
||
/**
|
||
* Expands given *markup* abbreviation (e.g. regular Emmet abbreviation that
|
||
* produces structured output like HTML) and outputs it according to options
|
||
* provided in config
|
||
*/
|
||
function markup(abbr, config) {
|
||
return stringify(parse$1(abbr, config), config);
|
||
}
|
||
/**
|
||
* Expands given *stylesheet* abbreviation (a special Emmet abbreviation designed for
|
||
* stylesheet languages like CSS, SASS etc.) and outputs it according to options
|
||
* provided in config
|
||
*/
|
||
function stylesheet(abbr, config) {
|
||
return css(parse(abbr, config), config);
|
||
}
|
||
|
||
exports.CSSAbbreviationScope = CSSAbbreviationScope;
|
||
exports.default = expandAbbreviation;
|
||
exports.extract = extractAbbreviation;
|
||
exports.markup = markup;
|
||
exports.markupAbbreviation = parseAbbreviation;
|
||
exports.parseMarkup = parse$1;
|
||
exports.parseStylesheet = parse;
|
||
exports.parseStylesheetSnippets = convertSnippets;
|
||
exports.resolveConfig = resolveConfig;
|
||
exports.stringifyMarkup = stringify;
|
||
exports.stringifyStylesheet = css;
|
||
exports.stylesheet = stylesheet;
|
||
exports.stylesheetAbbreviation = parse$2;
|
||
//# sourceMappingURL=emmet.cjs.map
|