`
* @type {Boolean}
*/
compactBooleanAttributes: false,
/**
* A set of boolean attributes
* @type {Set}
*/
booleanAttributes: ['contenteditable', 'seamless', 'async', 'autofocus',
'autoplay', 'checked', 'controls', 'defer', 'disabled', 'formnovalidate',
'hidden', 'ismap', 'loop', 'multiple', 'muted', 'novalidate', 'readonly',
'required', 'reversed', 'selected', 'typemustmatch'],
/**
* Style of self-closing tags:
* 'html' –
* 'xml' –
* 'xhtml' –
* @type {String}
*/
selfClosingStyle: 'html',
/**
* A set of inline-level elements
* @type {Set}
*/
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']
};
/**
* Creates output profile for given options (@see defaults)
* @param {defaults} options
*/
class Profile {
constructor(options) {
this.options = Object.assign({}, defaultOptions, options);
this.quoteChar = this.options.attributeQuotes === 'single' ? '\'' : '"';
}
/**
* Returns value of given option name
* @param {String} name
* @return {*}
*/
get(name) {
return this.options[name];
}
/**
* Quote given string according to profile
* @param {String} str String to quote
* @return {String}
*/
quote(str) {
return `${this.quoteChar}${str != null ? str : ''}${this.quoteChar}`;
}
/**
* Output given tag name accoding to options
* @param {String} name
* @return {String}
*/
name(name) {
return strcase(name, this.options.tagCase);
}
/**
* Outputs attribute name accoding to current settings
* @param {String} Attribute name
* @return {String}
*/
attribute(attr) {
return strcase(attr, this.options.attributeCase);
}
/**
* Check if given attribute is boolean
* @param {Attribute} attr
* @return {Boolean}
*/
isBooleanAttribute(attr) {
return attr.options.boolean
|| this.get('booleanAttributes').indexOf((attr.name || '').toLowerCase()) !== -1;
}
/**
* Returns a token for self-closing tag, depending on current options
* @return {String}
*/
selfClose() {
switch (this.options.selfClosingStyle) {
case 'xhtml': return ' /';
case 'xml': return '/';
default: return '';
}
}
/**
* Returns indent for given level
* @param {Number} level Indentation level
* @return {String}
*/
indent(level) {
level = level || 0;
let output = '';
while (level--) {
output += this.options.indent;
}
return output;
}
/**
* Check if given tag name belongs to inline-level element
* @param {Node|String} node Parsed node or tag name
* @return {Boolean}
*/
isInline(node) {
if (typeof node === 'string') {
return this.get('inlineElements').indexOf(node.toLowerCase()) !== -1;
}
// inline node is a node either with inline-level name or text-only node
return node.name != null ? this.isInline(node.name) : node.isTextOnly;
}
/**
* Outputs formatted field for given params
* @param {Number} index Field index
* @param {String} [placeholder] Field placeholder, can be empty
* @return {String}
*/
field(index, placeholder) {
return this.options.field(index, placeholder);
}
}
function strcase(string, type) {
if (type) {
string = type === 'upper' ? string.toUpperCase() : string.toLowerCase();
}
return string;
}
class Snippet {
constructor(key, value) {
this.key = key;
this.value = value;
}
}
class SnippetsStorage {
constructor(data) {
this._string = new Map();
this._regexp = new Map();
this._disabled = false;
this.load(data);
}
get disabled() {
return this._disabled;
}
/**
* Disables current store. A disabled store always returns `undefined`
* on `get()` method
*/
disable() {
this._disabled = true;
}
/**
* Enables current store.
*/
enable() {
this._disabled = false;
}
/**
* Registers a new snippet item
* @param {String|Regexp} key
* @param {String|Function} value
*/
set(key, value) {
if (typeof key === 'string') {
key.split('|').forEach(k => this._string.set(k, new Snippet(k, value)));
} else if (key instanceof RegExp) {
this._regexp.set(key, new Snippet(key, value));
} else {
throw new Error('Unknow snippet key: ' + key);
}
return this;
}
/**
* Returns a snippet matching given key. It first tries to find snippet
* exact match in a string key map, then tries to match one with regexp key
* @param {String} key
* @return {Snippet}
*/
get(key) {
if (this.disabled) {
return undefined;
}
if (this._string.has(key)) {
return this._string.get(key);
}
const keys = Array.from(this._regexp.keys());
for (let i = 0, il = keys.length; i < il; i++) {
if (keys[i].test(key)) {
return this._regexp.get(keys[i]);
}
}
}
/**
* Batch load of snippets data
* @param {Object|Map} data
*/
load(data) {
this.reset();
if (data instanceof Map) {
data.forEach((value, key) => this.set(key, value));
} else if (data && typeof data === 'object') {
Object.keys(data).forEach(key => this.set(key, data[key]));
}
}
/**
* Clears all stored snippets
*/
reset() {
this._string.clear();
this._regexp.clear();
}
/**
* Returns all available snippets from given store
*/
values() {
if (this.disabled) {
return [];
}
const string = Array.from(this._string.values());
const regexp = Array.from(this._regexp.values());
return string.concat(regexp);
}
}
/**
* A snippets registry. Contains snippets, separated by store and sorted by
* priority: a store with higher priority takes precedence when resolving snippet
* for given key
*/
class SnippetsRegistry {
/**
* Creates snippets registry, filled with given `data`
* @param {Object|Array} data Registry snippets. If array is given, adds items
* from array in order of precedence, registers global snippets otherwise
*/
constructor(data) {
this._registry = [];
if (Array.isArray(data)) {
data.forEach((snippets, level) => this.add(level, snippets));
} else if (typeof data === 'object') {
this.add(data);
}
}
/**
* Return store for given level
* @param {Number} level
* @return {SnippetsStorage}
*/
get(level) {
for (let i = 0; i < this._registry.length; i++) {
const item = this._registry[i];
if (item.level === level) {
return item.store;
}
}
}
/**
* Adds new store for given level
* @param {Number} [level] Store level (priority). Store with higher level
* takes precedence when resolving snippets
* @param {Object} [snippets] A snippets data for new store
* @return {SnipetsStorage}
*/
add(level, snippets) {
if (level != null && typeof level === 'object') {
snippets = level;
level = 0;
}
const store = new SnippetsStorage(snippets);
// remove previous store from same level
this.remove(level);
this._registry.push({level, store});
this._registry.sort((a, b) => b.level - a.level);
return store;
}
/**
* Remove registry with given level or store
* @param {Number|SnippetsStorage} data Either level or snippets store
*/
remove(data) {
this._registry = this._registry
.filter(item => item.level !== data && item.store !== data);
}
/**
* Returns snippet from registry that matches given name
* @param {String} name
* @return {Snippet}
*/
resolve(name) {
for (let i = 0; i < this._registry.length; i++) {
const snippet = this._registry[i].store.get(name);
if (snippet) {
return snippet;
}
}
}
/**
* Returns all available snippets from current registry. Snippets with the
* same key are resolved by their storage priority.
* @param {Object} options
* @param {Object} options.type Return snippets only of given type: 'string'
* or 'regexp'. Returns all snippets if not defined
* @return {Array}
*/
all(options) {
options = options || {};
const result = new Map();
const fillResult = snippet => {
const type = snippet.key instanceof RegExp ? 'regexp' : 'string';
if ((!options.type || options.type === type) && !result.has(snippet.key)) {
result.set(snippet.key, snippet);
}
};
this._registry.forEach(item => {
item.store.values().forEach(fillResult);
});
return Array.from(result.values());
}
/**
* Removes all stores from registry
*/
clear() {
this._registry.length = 0;
}
}
/**
* Methods for consuming quoted values
*/
const SINGLE_QUOTE = 39; // '
const DOUBLE_QUOTE = 34; // "
const defaultOptions$1 = {
escape: 92, // \ character
throws: false
};
/**
* Consumes 'single' or "double"-quoted string from given string, if possible
* @param {StreamReader} stream
* @param {Number} options.escape A character code of quote-escape symbol
* @param {Boolean} options.throws Throw error if quotes string can’t be properly consumed
* @return {Boolean} `true` if quoted string was consumed. The contents
* of quoted string will be availabe as `stream.current()`
*/
var eatQuoted = function(stream, options) {
options = options ? Object.assign({}, defaultOptions$1, options) : defaultOptions$1;
const start = stream.pos;
const quote = stream.peek();
if (stream.eat(isQuote)) {
while (!stream.eof()) {
switch (stream.next()) {
case quote:
stream.start = start;
return true;
case options.escape:
stream.next();
break;
}
}
// If we’re here then stream wasn’t properly consumed.
// Revert stream and decide what to do
stream.pos = start;
if (options.throws) {
throw stream.error('Unable to consume quoted string');
}
}
return false;
};
function isQuote(code) {
return code === SINGLE_QUOTE || code === DOUBLE_QUOTE;
}
/**
* Check if given code is a number
* @param {Number} code
* @return {Boolean}
*/
function isNumber(code) {
return code > 47 && code < 58;
}
/**
* Check if given character code is alpha code (letter through A to Z)
* @param {Number} code
* @param {Number} [from]
* @param {Number} [to]
* @return {Boolean}
*/
function isAlpha(code, from, to) {
from = from || 65; // A
to = to || 90; // Z
code &= ~32; // quick hack to convert any char code to uppercase char code
return code >= from && code <= to;
}
/**
* Check if given character code is alpha-numeric (letter through A to Z or number)
* @param {Number} code
* @return {Boolean}
*/
function isAlphaNumeric(code) {
return isNumber(code) || isAlpha(code);
}
function isWhiteSpace(code) {
return code === 32 /* space */
|| code === 9 /* tab */
|| code === 160; /* non-breaking space */
}
/**
* Check if given character code is a space
* @param {Number} code
* @return {Boolean}
*/
function isSpace(code) {
return isWhiteSpace(code)
|| code === 10 /* LF */
|| code === 13; /* CR */
}
/**
* Attribute descriptor of parsed abbreviation node
* @param {String} name Attribute name
* @param {String} value Attribute value
* @param {Object} options Additional custom attribute options
* @param {Boolean} options.boolean Attribute is boolean (e.g. name equals value)
* @param {Boolean} options.implied Attribute is implied (e.g. must be outputted
* only if contains non-null value)
*/
class Attribute {
constructor(name, value, options) {
this.name = name;
this.value = value != null ? value : null;
this.options = options || {};
}
/**
* Create a copy of current attribute
* @return {Attribute}
*/
clone() {
return new Attribute(this.name, this.value, Object.assign({}, this.options));
}
/**
* A string representation of current node
*/
valueOf() {
return `${this.name}="${this.value}"`;
}
}
/**
* A parsed abbreviation AST node. Nodes build up an abbreviation AST tree
*/
class Node {
/**
* Creates a new node
* @param {String} [name] Node name
* @param {Array} [attributes] Array of attributes to add
*/
constructor(name, attributes) {
// own properties
this.name = name || null;
this.value = null;
this.repeat = null;
this.selfClosing = false;
this.children = [];
/** @type {Node} Pointer to parent node */
this.parent = null;
/** @type {Node} Pointer to next sibling */
this.next = null;
/** @type {Node} Pointer to previous sibling */
this.previous = null;
this._attributes = [];
if (Array.isArray(attributes)) {
attributes.forEach(attr => this.setAttribute(attr));
}
}
/**
* Array of current node attributes
* @return {Attribute[]} Array of attributes
*/
get attributes() {
return this._attributes;
}
/**
* A shorthand to retreive node attributes as map
* @return {Object}
*/
get attributesMap() {
return this.attributes.reduce((out, attr) => {
out[attr.name] = attr.options.boolean ? attr.name : attr.value;
return out;
}, {});
}
/**
* Check if current node is a grouping one, e.g. has no actual representation
* and is used for grouping subsequent nodes only
* @return {Boolean}
*/
get isGroup() {
return !this.name && !this.value && !this._attributes.length;
}
/**
* Check if given node is a text-only node, e.g. contains only value
* @return {Boolean}
*/
get isTextOnly() {
return !this.name && !!this.value && !this._attributes.length;
}
/**
* Returns first child node
* @return {Node}
*/
get firstChild() {
return this.children[0];
}
/**
* Returns last child of current node
* @return {Node}
*/
get lastChild() {
return this.children[this.children.length - 1];
}
/**
* Return index of current node in its parent child list
* @return {Number} Returns -1 if current node is a root one
*/
get childIndex() {
return this.parent ? this.parent.children.indexOf(this) : -1;
}
/**
* Returns next sibling of current node
* @return {Node}
*/
get nextSibling() {
return this.next;
}
/**
* Returns previous sibling of current node
* @return {Node}
*/
get previousSibling() {
return this.previous;
}
/**
* Returns array of unique class names in current node
* @return {String[]}
*/
get classList() {
const attr = this.getAttribute('class');
return attr && attr.value
? attr.value.split(/\s+/g).filter(uniqueClass)
: [];
}
/**
* Convenient alias to create a new node instance
* @param {String} [name] Node name
* @param {Object} [attributes] Attributes hash
* @return {Node}
*/
create(name, attributes) {
return new Node(name, attributes);
}
/**
* Sets given attribute for current node
* @param {String|Object|Attribute} name Attribute name or attribute object
* @param {String} [value] Attribute value
*/
setAttribute(name, value) {
const attr = createAttribute(name, value);
const curAttr = this.getAttribute(name);
if (curAttr) {
this.replaceAttribute(curAttr, attr);
} else {
this._attributes.push(attr);
}
}
/**
* Check if attribute with given name exists in node
* @param {String} name
* @return {Boolean}
*/
hasAttribute(name) {
return !!this.getAttribute(name);
}
/**
* Returns attribute object by given name
* @param {String} name
* @return {Attribute}
*/
getAttribute(name) {
if (typeof name === 'object') {
name = name.name;
}
for (var i = 0; i < this._attributes.length; i++) {
const attr = this._attributes[i];
if (attr.name === name) {
return attr;
}
}
}
/**
* Replaces attribute with new instance
* @param {String|Attribute} curAttribute Current attribute name or instance
* to replace
* @param {String|Object|Attribute} newName New attribute name or attribute object
* @param {String} [newValue] New attribute value
*/
replaceAttribute(curAttribute, newName, newValue) {
if (typeof curAttribute === 'string') {
curAttribute = this.getAttribute(curAttribute);
}
const ix = this._attributes.indexOf(curAttribute);
if (ix !== -1) {
this._attributes.splice(ix, 1, createAttribute(newName, newValue));
}
}
/**
* Removes attribute with given name
* @param {String|Attribute} attr Atrtibute name or instance
*/
removeAttribute(attr) {
if (typeof attr === 'string') {
attr = this.getAttribute(attr);
}
const ix = this._attributes.indexOf(attr);
if (ix !== -1) {
this._attributes.splice(ix, 1);
}
}
/**
* Removes all attributes from current node
*/
clearAttributes() {
this._attributes.length = 0;
}
/**
* Adds given class name to class attribute
* @param {String} token Class name token
*/
addClass(token) {
token = normalize(token);
if (!this.hasAttribute('class')) {
this.setAttribute('class', token);
} else if (token && !this.hasClass(token)) {
this.setAttribute('class', this.classList.concat(token).join(' '));
}
}
/**
* Check if current node contains given class name
* @param {String} token Class name token
* @return {Boolean}
*/
hasClass(token) {
return this.classList.indexOf(normalize(token)) !== -1;
}
/**
* Removes given class name from class attribute
* @param {String} token Class name token
*/
removeClass(token) {
token = normalize(token);
if (this.hasClass(token)) {
this.setAttribute('class', this.classList.filter(name => name !== token).join(' '));
}
}
/**
* Appends child to current node
* @param {Node} node
*/
appendChild(node) {
this.insertAt(node, this.children.length);
}
/**
* Inserts given `newNode` before `refNode` child node
* @param {Node} newNode
* @param {Node} refNode
*/
insertBefore(newNode, refNode) {
this.insertAt(newNode, this.children.indexOf(refNode));
}
/**
* Insert given `node` at `pos` position of child list
* @param {Node} node
* @param {Number} pos
*/
insertAt(node, pos) {
if (pos < 0 || pos > this.children.length) {
throw new Error('Unable to insert node: position is out of child list range');
}
const prev = this.children[pos - 1];
const next = this.children[pos];
node.remove();
node.parent = this;
this.children.splice(pos, 0, node);
if (prev) {
node.previous = prev;
prev.next = node;
}
if (next) {
node.next = next;
next.previous = node;
}
}
/**
* Removes given child from current node
* @param {Node} node
*/
removeChild(node) {
const ix = this.children.indexOf(node);
if (ix !== -1) {
this.children.splice(ix, 1);
if (node.previous) {
node.previous.next = node.next;
}
if (node.next) {
node.next.previous = node.previous;
}
node.parent = node.next = node.previous = null;
}
}
/**
* Removes current node from its parent
*/
remove() {
if (this.parent) {
this.parent.removeChild(this);
}
}
/**
* Creates a detached copy of current node
* @param {Boolean} deep Clone node contents as well
* @return {Node}
*/
clone(deep) {
const clone = new Node(this.name);
clone.value = this.value;
clone.selfClosing = this.selfClosing;
if (this.repeat) {
clone.repeat = Object.assign({}, this.repeat);
}
this._attributes.forEach(attr => clone.setAttribute(attr.clone()));
if (deep) {
this.children.forEach(child => clone.appendChild(child.clone(true)));
}
return clone;
}
/**
* Walks on each descendant node and invokes given `fn` function on it.
* The function receives two arguments: the node itself and its depth level
* from current node. If function returns `false`, it stops walking
* @param {Function} fn
*/
walk(fn, _level) {
_level = _level || 0;
let ctx = this.firstChild;
while (ctx) {
// in case if context node will be detached during `fn` call
const next = ctx.next;
if (fn(ctx, _level) === false || ctx.walk(fn, _level + 1) === false) {
return false;
}
ctx = next;
}
}
/**
* A helper method for transformation chaining: runs given `fn` function on
* current node and returns the same node
*/
use(fn) {
const args = [this];
for (var i = 1; i < arguments.length; i++) {
args.push(arguments[i]);
}
fn.apply(null, args);
return this;
}
toString() {
const attrs = this.attributes.map(attr => {
attr = this.getAttribute(attr.name);
const opt = attr.options;
let out = `${opt && opt.implied ? '!' : ''}${attr.name || ''}`;
if (opt && opt.boolean) {
out += '.';
} else if (attr.value != null) {
out += `="${attr.value}"`;
}
return out;
});
let out = `${this.name || ''}`;
if (attrs.length) {
out += `[${attrs.join(' ')}]`;
}
if (this.value != null) {
out += `{${this.value}}`;
}
if (this.selfClosing) {
out += '/';
}
if (this.repeat) {
out += `*${this.repeat.count ? this.repeat.count : ''}`;
if (this.repeat.value != null) {
out += `@${this.repeat.value}`;
}
}
return out;
}
}
/**
* Attribute factory
* @param {String|Attribute|Object} name Attribute name or attribute descriptor
* @param {*} value Attribute value
* @return {Attribute}
*/
function createAttribute(name, value) {
if (name instanceof Attribute) {
return name;
}
if (typeof name === 'string') {
return new Attribute(name, value);
}
if (name && typeof name === 'object') {
return new Attribute(name.name, name.value, name.options);
}
}
/**
* @param {String} str
* @return {String}
*/
function normalize(str) {
return String(str).trim();
}
function uniqueClass(item, i, arr) {
return item && arr.indexOf(item) === i;
}
/**
* A streaming, character code-based string reader
*/
class StreamReader {
constructor(string, start, end) {
if (end == null && typeof string === 'string') {
end = string.length;
}
this.string = string;
this.pos = this.start = start || 0;
this.end = end;
}
/**
* Returns true only if the stream is at the end of the file.
* @returns {Boolean}
*/
eof() {
return this.pos >= this.end;
}
/**
* Creates a new stream instance which is limited to given `start` and `end`
* range. E.g. its `eof()` method will look at `end` property, not actual
* stream end
* @param {Point} start
* @param {Point} end
* @return {StreamReader}
*/
limit(start, end) {
return new this.constructor(this.string, start, end);
}
/**
* Returns the next character code in the stream without advancing it.
* Will return NaN at the end of the file.
* @returns {Number}
*/
peek() {
return this.string.charCodeAt(this.pos);
}
/**
* Returns the next character in the stream and advances it.
* Also returns
undefined
when no more characters are available.
* @returns {Number}
*/
next() {
if (this.pos < this.string.length) {
return this.string.charCodeAt(this.pos++);
}
}
/**
* `match` can be a character code or a function that takes a character code
* and returns a boolean. If the next character in the stream 'matches'
* the given argument, it is consumed and returned.
* Otherwise, `false` is returned.
* @param {Number|Function} match
* @returns {Boolean}
*/
eat(match) {
const ch = this.peek();
const ok = typeof match === 'function' ? match(ch) : ch === match;
if (ok) {
this.next();
}
return ok;
}
/**
* Repeatedly calls
eat
with the given argument, until it
* fails. Returns
true
if any characters were eaten.
* @param {Object} match
* @returns {Boolean}
*/
eatWhile(match) {
const start = this.pos;
while (!this.eof() && this.eat(match)) {}
return this.pos !== start;
}
/**
* Backs up the stream n characters. Backing it up further than the
* start of the current token will cause things to break, so be careful.
* @param {Number} n
*/
backUp(n) {
this.pos -= (n || 1);
}
/**
* Get the string between the start of the current token and the
* current stream position.
* @returns {String}
*/
current() {
return this.substring(this.start, this.pos);
}
/**
* Returns substring for given range
* @param {Number} start
* @param {Number} [end]
* @return {String}
*/
substring(start, end) {
return this.string.slice(start, end);
}
/**
* Creates error object with current stream state
* @param {String} message
* @return {Error}
*/
error(message) {
const err = new Error(`${message} at char ${this.pos + 1}`);
err.originalMessage = message;
err.pos = this.pos;
err.string = this.string;
return err;
}
}
const ASTERISK = 42; // *
/**
* Consumes node repeat token from current stream position and returns its
* parsed value
* @param {StringReader} stream
* @return {Object}
*/
function consumeRepeat(stream) {
if (stream.eat(ASTERISK)) {
stream.start = stream.pos;
// XXX think about extending repeat syntax with through numbering
return { count: stream.eatWhile(isNumber) ? +stream.current() : null };
}
}
const opt = { throws: true };
/**
* Consumes quoted literal from current stream position and returns it’s inner,
* unquoted, value
* @param {StringReader} stream
* @return {String} Returns `null` if unable to consume quoted value from current
* position
*/
function consumeQuoted(stream) {
if (eatQuoted(stream, opt)) {
return stream.current().slice(1, -1);
}
}
const TEXT_START = 123; // {
const TEXT_END = 125; // }
const ESCAPE = 92; // \ character
/**
* Consumes text node `{...}` from stream
* @param {StreamReader} stream
* @return {String} Returns consumed text value (without surrounding braces) or
* `null` if there’s no text at starting position
*/
function consumeText(stream) {
// NB using own implementation instead of `eatPair()` from @emmetio/stream-reader-utils
// to disable quoted value consuming
const start = stream.pos;
if (stream.eat(TEXT_START)) {
let stack = 1, ch;
let result = '';
let offset = stream.pos;
while (!stream.eof()) {
ch = stream.next();
if (ch === TEXT_START) {
stack++;
} else if (ch === TEXT_END) {
stack--;
if (!stack) {
stream.start = start;
return result + stream.substring(offset, stream.pos - 1);
}
} else if (ch === ESCAPE) {
ch = stream.next();
if (ch === TEXT_START || ch === TEXT_END) {
result += stream.substring(offset, stream.pos - 2) + String.fromCharCode(ch);
offset = stream.pos;
}
}
}
// If we’re here then paired character can’t be consumed
stream.pos = start;
throw stream.error(`Unable to find closing ${String.fromCharCode(TEXT_END)} for text start`);
}
return null;
}
const EXCL = 33; // .
const DOT = 46; // .
const EQUALS = 61; // =
const ATTR_OPEN = 91; // [
const ATTR_CLOSE = 93; // ]
const reAttributeName = /^\!?[\w\-:\$@]+\.?$|^\!?\[[\w\-:\$@]+\]\.?$/;
/**
* Consumes attributes defined in square braces from given stream.
* Example:
* [attr col=3 title="Quoted string" selected. support={react}]
* @param {StringReader} stream
* @returns {Array} Array of consumed attributes
*/
function consumeAttributes(stream) {
if (!stream.eat(ATTR_OPEN)) {
return null;
}
const result = [];
let token, attr;
while (!stream.eof()) {
stream.eatWhile(isWhiteSpace);
if (stream.eat(ATTR_CLOSE)) {
return result; // End of attribute set
} else if ((token = consumeQuoted(stream)) != null) {
// Consumed quoted value: anonymous attribute
result.push({
name: null,
value: token
});
} else if (eatUnquoted(stream)) {
// Consumed next word: could be either attribute name or unquoted default value
token = stream.current();
// In angular attribute names can be surrounded by []
if (token[0] === '[' && stream.peek() === ATTR_CLOSE) {
stream.next();
token = stream.current();
}
if (!reAttributeName.test(token)) {
// anonymous attribute
result.push({ name: null, value: token });
} else {
// Looks like a regular attribute
attr = parseAttributeName(token);
result.push(attr);
if (stream.eat(EQUALS)) {
// Explicitly defined value. Could be a word, a quoted string
// or React-like expression
if ((token = consumeQuoted(stream)) != null) {
attr.value = token;
} else if ((token = consumeText(stream)) != null) {
attr.value = token;
attr.options = {
before: '{',
after: '}'
};
} else if (eatUnquoted(stream)) {
attr.value = stream.current();
}
}
}
} else {
throw stream.error('Expected attribute name');
}
}
throw stream.error('Expected closing "]" brace');
}
function parseAttributeName(name) {
const options = {};
// If a first character in attribute name is `!` — it’s an implied
// default attribute
if (name.charCodeAt(0) === EXCL) {
name = name.slice(1);
options.implied = true;
}
// Check for last character: if it’s a `.`, user wants boolean attribute
if (name.charCodeAt(name.length - 1) === DOT) {
name = name.slice(0, name.length - 1);
options.boolean = true;
}
const attr = { name };
if (Object.keys(options).length) {
attr.options = options;
}
return attr;
}
/**
* Eats token that can be an unquoted value from given stream
* @param {StreamReader} stream
* @return {Boolean}
*/
function eatUnquoted(stream) {
const start = stream.pos;
if (stream.eatWhile(isUnquoted)) {
stream.start = start;
return true;
}
}
function isUnquoted(code) {
return !isSpace(code) && !isQuote(code)
&& code !== ATTR_CLOSE && code !== EQUALS;
}
const HASH = 35; // #
const DOT$1 = 46; // .
const SLASH = 47; // /
/**
* Consumes a single element node from current abbreviation stream
* @param {StringReader} stream
* @return {Node}
*/
function consumeElement(stream) {
// consume element name, if provided
const start = stream.pos;
const node = new Node(eatName(stream));
let next;
while (!stream.eof()) {
if (stream.eat(DOT$1)) {
node.addClass(eatName(stream));
} else if (stream.eat(HASH)) {
node.setAttribute('id', eatName(stream));
} else if (stream.eat(SLASH)) {
// A self-closing indicator must be at the end of non-grouping node
if (node.isGroup) {
stream.backUp(1);
throw stream.error('Unexpected self-closing indicator');
}
node.selfClosing = true;
if (next = consumeRepeat(stream)) {
node.repeat = next;
}
break;
} else if (next = consumeAttributes(stream)) {
for (let i = 0, il = next.length; i < il; i++) {
node.setAttribute(next[i]);
}
} else if ((next = consumeText(stream)) !== null) {
node.value = next;
} else if (next = consumeRepeat(stream)) {
node.repeat = next;
} else {
break;
}
}
if (start === stream.pos) {
throw stream.error(`Unable to consume abbreviation node, unexpected ${stream.peek()}`);
}
return node;
}
function eatName(stream) {
stream.start = stream.pos;
stream.eatWhile(isName);
return stream.current();
}
function isName(code) {
return isAlphaNumeric(code)
|| code === 45 /* - */
|| code === 58 /* : */
|| code === 36 /* $ */
|| code === 64 /* @ */
|| code === 33 /* ! */
|| code === 95 /* _ */
|| code === 37 /* % */;
}
const GROUP_START = 40; // (
const GROUP_END = 41; // )
const OP_SIBLING = 43; // +
const OP_CHILD = 62; // >
const OP_CLIMB = 94; // ^
/**
* Parses given string into a node tree
* @param {String} str Abbreviation to parse
* @return {Node}
*/
function parse(str) {
const stream = new StreamReader(str.trim());
const root = new Node();
let ctx = root, groupStack = [], ch;
while (!stream.eof()) {
ch = stream.peek();
if (ch === GROUP_START) { // start of group
// The grouping node should be detached to properly handle
// out-of-bounds `^` operator. Node will be attached right on group end
const node = new Node();
groupStack.push([node, ctx, stream.pos]);
ctx = node;
stream.next();
continue;
} else if (ch === GROUP_END) { // end of group
const lastGroup = groupStack.pop();
if (!lastGroup) {
throw stream.error('Unexpected ")" group end');
}
const node = lastGroup[0];
ctx = lastGroup[1];
stream.next();
// a group can have a repeater
if (node.repeat = consumeRepeat(stream)) {
ctx.appendChild(node);
} else {
// move all children of group into parent node
while (node.firstChild) {
ctx.appendChild(node.firstChild);
}
}
// for convenience, groups can be joined with optional `+` operator
stream.eat(OP_SIBLING);
continue;
}
const node = consumeElement(stream);
ctx.appendChild(node);
if (stream.eof()) {
break;
}
switch (stream.peek()) {
case OP_SIBLING:
stream.next();
continue;
case OP_CHILD:
stream.next();
ctx = node;
continue;
case OP_CLIMB:
// it’s perfectly valid to have multiple `^` operators
while (stream.eat(OP_CLIMB)) {
ctx = ctx.parent || ctx;
}
continue;
}
}
if (groupStack.length) {
stream.pos = groupStack.pop()[2];
throw stream.error('Expected group close');
}
return root;
}
/**
* Parses given abbreviation and un-rolls it into a full tree: recursively
* replaces repeated elements with actual nodes
* @param {String} abbr
* @return {Node}
*/
function index(abbr) {
const tree = parse(abbr);
tree.walk(unroll);
return tree;
}
function unroll(node) {
if (!node.repeat || !node.repeat.count) {
return;
}
const parent = node.parent;
let ix = parent.children.indexOf(node);
for (let i = 0; i < node.repeat.count; i++) {
const clone = node.clone(true);
clone.repeat.value = i + 1;
clone.walk(unroll);
if (clone.isGroup) {
while (clone.children.length > 0) {
clone.firstChild.repeat = clone.repeat;
parent.insertAt(clone.firstChild, ix++);
}
} else {
parent.insertAt(clone, ix++);
}
}
node.parent.removeChild(node);
}
/**
* For every node in given `tree`, 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, attribues and so on, possibly a complex
* abbreviation with multiple elements. So we have to get snippet, parse it
* and recursively resolve it.
*
* @param {Node} tree Parsed Emmet abbreviation
* @param {SnippetsRegistry} registry Registry with all available snippets
* @return {Node} Updated tree
*/
var index$1 = function(tree, registry) {
tree.walk(node => resolveNode(node, registry));
return tree;
};
function resolveNode(node, registry) {
const stack = new Set();
const resolve = node => {
const snippet = registry.resolve(node.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.has(snippet)) {
return;
}
// In case if matched snippet is a function, pass control into it
if (typeof snippet.value === 'function') {
return snippet.value(node, registry, resolve);
}
const tree = index(snippet.value);
stack.add(snippet);
tree.walk(resolve);
stack.delete(snippet);
// move current node contents into new tree
const childTarget = findDeepestNode(tree);
merge(childTarget, node);
while (tree.firstChild) {
node.parent.insertBefore(tree.firstChild, node);
}
childTarget.parent.insertBefore(node, childTarget);
childTarget.remove();
};
resolve(node);
}
/**
* Adds data from first node into second node and returns it
* @param {Node} from
* @param {Node} to
* @return {Node}
*/
function merge(from, to) {
to.name = from.name;
if (from.selfClosing) {
to.selfClosing = true;
}
if (from.value != null) {
to.value = from.value;
}
if (from.repeat) {
to.repeat = Object.assign({}, from.repeat);
}
return mergeAttributes(from, to);
}
/**
* Transfer attributes from first element to second one and preserve first
* element’s attributes order
* @param {Node} from
* @param {Node} to
* @return {Node}
*/
function mergeAttributes(from, to) {
mergeClassNames(from, to);
// It’s important to preserve attributes order: ones in `from` have higher
// pripority than in `to`. Collect attributes in map in order they should
// appear in `to`
const attrMap = new Map();
let attrs = from.attributes;
for (let i = 0; i < attrs.length; i++) {
attrMap.set(attrs[i].name, attrs[i].clone());
}
attrs = to.attributes.slice();
for (let i = 0, attr, a; i < attrs.length; i++) {
attr = attrs[i];
if (attrMap.has(attr.name)) {
a = attrMap.get(attr.name);
a.value = attr.value;
// If user explicitly wrote attribute in abbreviation, it’s no longer
// implied and should be outputted even if value is empty
if (a.options.implied) {
a.options.implied = false;
}
} else {
attrMap.set(attr.name, attr);
}
to.removeAttribute(attr);
}
const newAttrs = Array.from(attrMap.values());
for (let i = 0; i < newAttrs.length; i++) {
to.setAttribute(newAttrs[i]);
}
return to;
}
/**
* Adds class names from first node to second one
* @param {Node} from
* @param {Node} to
* @return {Node}
*/
function mergeClassNames(from, to) {
const classNames = from.classList;
for (let i = 0; i < classNames.length; i++) {
to.addClass(classNames[i]);
}
return to;
}
/**
* Finds node which is the deepest for in current node or node iteself.
* @param {Node} node
* @return {Node}
*/
function findDeepestNode(node) {
while (node.children.length) {
node = node.children[node.children.length - 1];
}
return node;
}
const inlineElements = new Set('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'.split(','));
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'
};
/**
* Returns best child node name for given parent node name
* @param {String} parentName Name of parent node
* @return {String}
*/
function resolveImplicitName(parentName) {
parentName = (parentName || '').toLowerCase();
return elementMap[parentName]
|| (inlineElements.has(parentName) ? 'span' : 'div');
}
/**
* Adds missing tag names for given tree depending on node’s parent name
*/
var implicitTags = function(tree) {
tree.walk(node => {
// resolve only nameless nodes without content
if (node.name == null && node.attributes.length) {
node.name = resolveImplicitName(node.parent.name);
}
});
return tree;
};
/**
* Locates all occurances of given `token` which are not escaped (e.g. are not
* preceded with `\`) given in `str`
* @param {String} str
* @return {Array} Array of token ranges
*/
function findUnescapedTokens(str, token) {
const result = new Set();
const tlen = token.length;
// 1. Find all occurances of tokens
let pos = 0;
while ((pos = str.indexOf(token, pos)) !== -1) {
result.add(pos);
pos += tlen;
}
if (result.size) {
// 2. Remove ones that escaped
let pos = 0;
const len = str.length;
while (pos < len) {
if (str[pos++] === '\\') {
result.delete(pos++);
}
}
}
return Array.from(result).map(ix => range(ix, tlen));
}
/**
* Replaces `ranges`, generated by `range()` function, with given `value` in `str`
* @param {String} str Where to replace ranges
* @param {Array} ranges Ranes, created by `range()` function
* @param {String|Function} value Replacement value. If it’s a function, it
* will take a range value as argument and should return a new string
* @return {String}
*/
function replaceRanges(str, ranges, value) {
// should walk from the end of array to keep ranges valid after replacement
for (let i = ranges.length - 1; i >= 0; i--) {
const r = ranges[i];
let offset = 0;
let offsetLength = 0;
if (str.substr(r[0] + r[1], 1) === '@'){
const matches = str.substr(r[0] + r[1] + 1).match(/^(\d+)/);
if (matches) {
offsetLength = matches[1].length + 1;
offset = parseInt(matches[1]) - 1;
}
}
str = str.substring(0, r[0])
+ (typeof value === 'function' ? value(str.substr(r[0], r[1]), offset) : value)
+ str.substring(r[0] + r[1] + offsetLength);
}
return str;
}
function range(start, length) {
return [start, length];
}
const numberingToken = '$';
/**
* Numbering of expanded abbreviation: finds all nodes with `$` in value
* or attributes and replaces its occurances with repeater value
*/
var applyNumbering = function(tree) {
tree.walk(applyNumbering$1);
return tree;
};
/**
* Applies numbering for given node: replaces occurances of numbering token
* in node’s name, content and attributes
* @param {Node} node
* @return {Node}
*/
function applyNumbering$1(node) {
const repeater = findRepeater(node);
if (repeater && repeater.value != null) {
// NB replace numbering in nodes with explicit repeater only:
// it solves issues with abbreviations like `xsl:if[test=$foo]` where
// `$foo` is preferred output
const value = repeater.value;
node.name = replaceNumbering(node.name, value);
node.value = replaceNumbering(node.value, value);
node.attributes.forEach(attr => {
const copy = node.getAttribute(attr.name).clone();
copy.name = replaceNumbering(attr.name, value);
copy.value = replaceNumbering(attr.value, value);
node.replaceAttribute(attr.name, copy);
});
}
return node;
}
/**
* Returns repeater object for given node
* @param {Node} node
* @return {Object}
*/
function findRepeater(node) {
while (node) {
if (node.repeat) {
return node.repeat;
}
node = node.parent;
}
}
/**
* Replaces numbering in given string
* @param {String} str
* @param {Number} value
* @return {String}
*/
function replaceNumbering(str, value) {
// replace numbering in strings only: skip explicit wrappers that could
// contain unescaped numbering tokens
if (typeof str === 'string') {
const ranges = getNumberingRanges(str);
return replaceNumberingRanges(str, ranges, value);
}
return str;
}
/**
* Returns numbering ranges, e.g. ranges of `$` occurances, in given string.
* Multiple adjacent ranges are combined
* @param {String} str
* @return {Array}
*/
function getNumberingRanges(str) {
return findUnescapedTokens(str || '', numberingToken)
.reduce((out, range$$1) => {
// skip ranges that actually belongs to output placeholder or tabstops
if (!/[#{]/.test(str[range$$1[0] + 1] || '')) {
const lastRange = out[out.length - 1];
if (lastRange && lastRange[0] + lastRange[1] === range$$1[0]) {
lastRange[1] += range$$1[1];
} else {
out.push(range$$1);
}
}
return out;
}, []);
}
/**
* @param {String} str
* @param {Array} ranges
* @param {Number} value
* @return {String}
*/
function replaceNumberingRanges(str, ranges, value) {
const replaced = replaceRanges(str, ranges, (token, offset) => {
let _value = String(value + offset);
// pad values for multiple numbering tokens, e.g. 3 for $$$ becomes 003
while (_value.length < token.length) {
_value = '0' + _value;
}
return _value;
});
// unescape screened numbering tokens
return unescapeString(replaced);
}
/**
* Unescapes characters, screened with `\`, in given string
* @param {String} str
* @return {String}
*/
function unescapeString(str) {
let i = 0, result = '';
const len = str.length;
while (i < len) {
const ch = str[i++];
result += (ch === '\\') ? (str[i++] || '') : ch;
}
return result;
}
/** Placeholder for inserted content */
const placeholder = '$#';
/** Placeholder for caret */
const caret = '|';
const reUrl = /^((?:https?|ftp|file):\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
const reEmail = /^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$/;
const reProto = /^([a-z]+:)?\/\//i;
/**
* Inserts content into node with implicit repeat count: this node is then
* duplicated for each content item and content itself is inserted either into
* deepest child or instead of a special token.
*
* This method uses two distinct steps: `prepare()` and `insert()` since most
* likely these steps will be used separately to properly insert content
* with unescaped `$` numbering markers.
*
* @param {Node} tree Parsed abbreviation
* @param {String[]} content Array of content items to insert
* @return {Node}
*/
/**
* Finds nodes with implicit repeat and creates `amount` copies of it in tree
* @param {Node} tree
* @param {Number} amount
* @return {Node}
*/
function prepare(tree, amount) {
amount = amount || 1;
tree.walk(node => {
if (node.repeat && node.repeat.count === null) {
for (let i = 0; i < amount; i++) {
const clone = node.clone(true);
clone.repeat.implicit = true;
clone.repeat.count = amount;
clone.repeat.value = i + 1;
clone.repeat.index = i;
node.parent.insertBefore(clone, node);
}
node.remove();
}
});
return tree;
}
/**
* Inserts content into implicitly repeated nodes, created by `prepare()` method
* @param {Node} tree
* @param {String[]} content
* @return {Node}
*/
function insert(tree, content) {
if (Array.isArray(content) && content.length) {
let updated = false;
tree.walk(node => {
if (node.repeat && node.repeat.implicit) {
updated = true;
insertContent(node, content[node.repeat.index]);
}
});
if (!updated) {
// no node with implicit repeat was found, insert content as
// deepest child
setNodeContent(findDeepestNode$1(tree), content.join('\n'));
}
}
return tree;
}
/**
* Inserts `content` into given `node`: either replaces output placeholders
* or inserts it into deepest child node
* @param {Node} node
* @param {String} content
* @return {Node}
*/
function insertContent(node, content) {
let inserted = insertContentIntoPlaceholder(node, content);
node.walk(child => inserted |= insertContentIntoPlaceholder(child, content));
if (!inserted) {
// no placeholders were found in node, insert content into deepest child
setNodeContent(findDeepestNode$1(node), content);
}
return node;
}
/**
* Inserts given `content` into placeholders for given `node`. Placeholders
* might be available in attribute values and node content
* @param {Node} node
* @param {String} content
* @return {Boolean} Returns `true` if placeholders were found and replaced in node
*/
function insertContentIntoPlaceholder(node, content) {
const state = {replaced: false};
node.value = replacePlaceholder(node.value, content, state);
node.attributes.forEach(attr => {
if (attr.value) {
node.setAttribute(attr.name, replacePlaceholder(attr.value, content, state));
}
});
return state.replaced;
}
/**
* Replaces all placeholder occurances in given `str` with `value`
* @param {String} str
* @param {String} value
* @param {Object} [_state] If provided, set `replaced` property of given
* object to `true` if placeholder was found and replaced
* @return {String}
*/
function replacePlaceholder(str, value, _state) {
if (typeof str === 'string') {
const ranges = findUnescapedTokens(str, placeholder);
if (ranges.length) {
if (_state) {
_state.replaced = true;
}
str = replaceRanges(str, ranges, value);
}
}
return str;
}
/**
* Finds node which is the deepest for in current node or node iteself.
* @param {Node} node
* @return {Node}
*/
function findDeepestNode$1(node) {
while (node.children.length) {
node = node.children[node.children.length - 1];
}
return node;
}
/**
* Updates content of given node
* @param {Node} node
* @param {String} content
*/
function setNodeContent(node, content) {
// find caret position and replace it with content, if possible
if (node.value) {
const ranges = findUnescapedTokens(node.value, caret);
if (ranges.length) {
node.value = replaceRanges(node.value, ranges, content);
return;
}
}
if (node.name.toLowerCase() === 'a' || node.hasAttribute('href')) {
// special case: inserting content into `
` tag
if (reUrl.test(content)) {
node.setAttribute('href', (reProto.test(content) ? '' : 'http://') + content);
} else if (reEmail.test(content)) {
node.setAttribute('href', 'mailto:' + content);
}
}
node.value = content;
}
const defaultOptions$2 = {
element: '__',
modifier: '_'
};
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);
/**
* BEM transformer: updates class names written as `-element` and
* `_modifier` into full class names as described in BEM specs. Also adds missing
* class names: fir example, if node contains `.block_modifier` class, ensures
* that element contains `.block` class as well
*/
var bem = function(tree, options) {
options = Object.assign({}, defaultOptions$2, options);
tree.walk(node => expandClassNames(node, options));
const lookup = createBlockLookup(tree);
tree.walk(node => expandShortNotation(node, lookup, options));
return tree;
};
/**
* 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
* @param {Node} node
* @param {Object} options
* @return {Set}
*/
function expandClassNames(node, options) {
const classNames = node.classList.reduce((out, cl) => {
// remove all modifiers and element prefixes from class name to get a base element name
const ix = cl.indexOf('_');
if (ix > 0 && !cl.startsWith('-')) {
out.add(cl.slice(0, ix));
out.add(cl.slice(ix));
return out;
}
return out.add(cl);
}, new Set());
if (classNames.size) {
node.setAttribute('class', Array.from(classNames).join(' '));
}
}
/**
* Expands short BEM notation, e.g. `-element` and `_modifier`
* @param {Node} node Parsed Emmet abbreviation node
* @param {Map} lookup BEM block name lookup
* @param {Object} options
*/
function expandShortNotation(node, lookup, options) {
const classNames = node.classList.reduce((out, cl) => {
let prefix, m;
const originalClass = cl;
// parse element definition (could be only one)
if (m = cl.match(reElement)) {
prefix = getBlockName(node, lookup, m[1]) + options.element + m[2];
out.add(prefix);
cl = cl.slice(m[0].length);
}
// parse modifiers definitions (may contain multiple)
while (m = cl.match(reModifier)) {
if (!prefix) {
prefix = getBlockName(node, lookup, m[1]);
out.add(prefix);
}
out.add(`${prefix}${options.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
out.add(originalClass);
}
return out;
}, new Set());
const arrClassNames = Array.from(classNames).filter(Boolean);
if (arrClassNames.length) {
node.setAttribute('class', arrClassNames.join(' '));
}
}
/**
* Creates block name lookup for each node in given tree, e.g. finds block
* name explicitly for each node
* @param {Node} tree
* @return {Map}
*/
function createBlockLookup(tree) {
const lookup = new Map();
tree.walk(node => {
const classNames = node.classList;
if (classNames.length) {
// guess best block name from class or use parent’s block name
lookup.set(node,
find(classNames, blockCandidates1)
|| find(classNames, blockCandidates2)
|| lookup.get(node.parent)
);
}
});
return lookup;
}
/**
* Returns block name for given `node` by `prefix`, which tells the depth of
* of parent node lookup
* @param {Node} node
* @param {Map} lookup
* @param {String} prefix
* @return {String}
*/
function getBlockName(node, lookup, prefix) {
let depth = prefix.length > 1 ? prefix.length : 0;
// NB don’t walk up to root node, stay at first root child in case of
// too deep prefix
while (node.parent && node.parent.parent && depth--) {
node = node.parent;
}
return lookup.get(node) || '';
}
function find(arr, filter) {
return arr.filter(filter)[0];
}
/**
* JSX transformer: replaces `class` and `for` attributes with `className` and
* `htmlFor` attributes respectively
*/
var jsx = function(tree) {
tree.walk(node => {
replace(node, 'class', 'className');
replace(node, 'for', 'htmlFor');
});
return tree;
};
function replace(node, oldName, newName) {
let attr = node.getAttribute(oldName);
if (attr) {
attr.name = newName;
}
}
const reSupporterNames = /^xsl:(variable|with\-param)$/i;
/**
* XSL transformer: removes `select` attributes from certain nodes that contain
* children
*/
var xsl = function(tree) {
tree.walk(node => {
if (reSupporterNames.test(node.name || '') && (node.children.length || node.value)) {
node.removeAttribute('select');
}
});
return tree;
};
const supportedAddons = { bem, jsx, xsl };
/**
* Runs additional transforms on given tree.
* These transforms may introduce side-effects and unexpected result
* so they are not applied by default, authors must specify which addons
* in `addons` argument as `{addonName: addonOptions}`
* @param {Node} tree Parsed Emmet abbreviation
* @param {Object} addons Add-ons to apply and their options
*/
var addons = function(tree, addons) {
Object.keys(addons || {}).forEach(key => {
if (key in supportedAddons) {
const addonOpt = typeof addons[key] === 'object' ? addons[key] : null;
tree = tree.use(supportedAddons[key], addonOpt);
}
});
return tree;
};
/**
* Applies basic HTML-specific transformations for given parsed abbreviation:
* – resolve implied tag names
* – insert repeated content
* – resolve node numbering
*/
var index$2 = function(tree, content, appliedAddons) {
if (typeof content === 'string') {
content = [content];
} else if (content && typeof content === 'object' && !Array.isArray(content)) {
appliedAddons = content;
content = null;
}
return tree
.use(implicitTags)
.use(prepare, Array.isArray(content) ? content.length : null)
.use(applyNumbering)
.use(insert, content)
.use(addons, appliedAddons);
};
/**
* Replaces all unescaped ${variable} occurances in given parsed abbreviation
* `tree` with values provided in `variables` hash. Precede `$` with `\` to
* escape it and skip replacement
* @param {Node} tree Parsed abbreviation tree
* @param {Object} variables Variables values
* @return {Node}
*/
function replaceVariables(tree, variables) {
variables = variables || {};
tree.walk(node => replaceInNode(node, variables));
return tree;
}
function replaceInNode(node, variables) {
// Replace variables in attributes.
const attrs = node.attributes;
for (let i = 0, il = attrs.length; i < il; i++) {
const attr = attrs[i];
if (typeof attr.value === 'string') {
node.setAttribute(attr.name, replaceInString(attr.value, variables));
}
}
if (node.value != null) {
node.value = replaceInString(node.value, variables);
}
return node;
}
/**
* Replaces all unescaped `${variable}` occurances in given string with values
* from `variables` object
* @param {String} string
* @param {Object} variables
* @return {String}
*/
function replaceInString(string, variables) {
const model = createModel(string);
let offset = 0;
let output = '';
for (let i = 0, il = model.variables.length; i < il; i++) {
const v = model.variables[i];
let value = v.name in variables ? variables[v.name] : v.name;
if (typeof value === 'function') {
value = value(model.string, v, offset + v.location);
}
output += model.string.slice(offset, v.location) + value;
offset = v.location + v.length;
}
return output + model.string.slice(offset);
}
/**
* Creates variable model from given string. The model contains a `string` with
* all escaped variable tokens written without escape symbol and `variables`
* property with all unescaped variables and their ranges
* @param {String} string
* @return {Object}
*/
function createModel(string) {
const reVariable = /\$\{([a-z][\w\-]*)\}/ig;
const escapeCharCode = 92; // `\` symbol
const variables = [];
// We have to replace unescaped (e.g. not preceded with `\`) tokens.
// Instead of writing a stream parser, we’ll cut some edges here:
// 1. Find all tokens
// 2. Walk string char-by-char and resolve only tokens that are not escaped
const tokens = new Map();
let m;
while (m = reVariable.exec(string)) {
tokens.set(m.index, m);
}
if (tokens.size) {
let start = 0, pos = 0, len = string.length;
let output = '';
while (pos < len) {
if (string.charCodeAt(pos) === escapeCharCode && tokens.has(pos + 1)) {
// Found escape symbol that escapes variable: we should
// omit this symbol in output string and skip variable
const token = tokens.get(pos + 1);
output += string.slice(start, pos) + token[0];
start = pos = token.index + token[0].length;
tokens.delete(pos + 1);
continue;
}
pos++;
}
string = output + string.slice(start);
// Not using `.map()` here to reduce memory allocations
const validMatches = Array.from(tokens.values());
for (let i = 0, il = validMatches.length; i < il; i++) {
const token = validMatches[i];
variables.push({
name: token[1],
location: token.index,
length: token[0].length
});
}
}
return {string, variables};
}
const DOLLAR = 36; // $
const COLON = 58; // :
const ESCAPE$1 = 92; // \
const OPEN_BRACE = 123; // {
const CLOSE_BRACE = 125; // }
/**
* Finds fields in given string and returns object with field-less string
* and array of fileds found
* @param {String} string
* @return {Object}
*/
function parse$2(string) {
const stream = new StreamReader(string);
const fields = [];
let cleanString = '', offset = 0, pos = 0;
let code, field;
while (!stream.eof()) {
code = stream.peek();
pos = stream.pos;
if (code === ESCAPE$1) {
stream.next();
stream.next();
} else if (field = consumeField(stream, cleanString.length + pos - offset)) {
fields.push(field);
cleanString += stream.string.slice(offset, pos) + field.placeholder;
offset = stream.pos;
} else {
stream.next();
}
}
return new FieldString(cleanString + stream.string.slice(offset), fields);
}
/**
* Marks given `string` with `fields`: wraps each field range with
* `${index:placeholder}` (by default) or any other token produced by `token`
* function, if provided
* @param {String} string String to mark
* @param {Array} fields Array of field descriptor. A field descriptor is a
* `{index, location, length}` array. It is important that fields in array
* must be ordered by their location in string: some fields my refer the same
* location so they must appear in order that user expects.
* @param {Function} [token] Function that generates field token. This function
* received two arguments: `index` and `placeholder` and should return string
* @return {String} String with marked fields
*/
function mark(string, fields, token) {
token = token || createToken;
// order fields by their location and appearence
// NB field ranges should not overlap! (not supported yet)
const ordered = fields
.map((field, order) => ({order, field, end: field.location + field.length}))
.sort((a, b) => (a.end - b.end) || (a.order - b.order));
// mark ranges in string
let offset = 0;
const result = ordered.map(item => {
const placeholder = string.substr(item.field.location, item.field.length);
const prefix = string.slice(offset, item.field.location);
offset = item.end;
return prefix + token(item.field.index, placeholder);
});
return result.join('') + string.slice(offset);
}
/**
* Creates field token for string
* @param {Number} index Field index
* @param {String} placeholder Field placeholder, could be empty string
* @return {String}
*/
function createToken(index, placeholder) {
return placeholder ? `\${${index}:${placeholder}}` : `\${${index}}`;
}
/**
* Consumes field from current stream position: it can be an `$index` or
* or `${index}` or `${index:placeholder}`
* @param {StreamReader} stream
* @param {Number} location Field location in *clean* string
* @return {Object} Object with `index` and `placeholder` properties if
* fieald was successfully consumed, `null` otherwise
*/
function consumeField(stream, location) {
const start = stream.pos;
if (stream.eat(DOLLAR)) {
// Possible start of field
let index = consumeIndex(stream);
let placeholder = '';
// consumed $index placeholder
if (index != null) {
return new Field(index, placeholder, location);
}
if (stream.eat(OPEN_BRACE)) {
index = consumeIndex(stream);
if (index != null) {
if (stream.eat(COLON)) {
placeholder = consumePlaceholder(stream);
}
if (stream.eat(CLOSE_BRACE)) {
return new Field(index, placeholder, location);
}
}
}
}
// If we reached here then there’s no valid field here, revert
// back to starting position
stream.pos = start;
}
/**
* Consumes a placeholder: value right after `:` in field. Could be empty
* @param {StreamReader} stream
* @return {String}
*/
function consumePlaceholder(stream) {
let code;
const stack = [];
stream.start = stream.pos;
while (!stream.eof()) {
code = stream.peek();
if (code === OPEN_BRACE) {
stack.push(stream.pos);
} else if (code === CLOSE_BRACE) {
if (!stack.length) {
break;
}
stack.pop();
}
stream.next();
}
if (stack.length) {
throw stream.error('Unable to find matching "}" for curly brace at ' + stack.pop());
}
return stream.current();
}
/**
* Consumes integer from current stream position
* @param {StreamReader} stream
* @return {Number}
*/
function consumeIndex(stream) {
stream.start = stream.pos;
if (stream.eatWhile(isNumber)) {
return Number(stream.current());
}
}
class Field {
constructor(index, placeholder, location) {
this.index = index;
this.placeholder = placeholder;
this.location = location;
this.length = this.placeholder.length;
}
}
class FieldString {
/**
* @param {String} string
* @param {Field[]} fields
*/
constructor(string, fields) {
this.string = string;
this.fields = fields;
}
mark(token) {
return mark(this.string, this.fields, token);
}
toString() {
return string;
}
}
const defaultFieldsRenderer = text => text;
/**
* Output node is an object containing generated output for given Emmet
* abbreviation node. Output node can be passed to various processors that
* may shape-up final node output. The final output is simply a concatenation
* of `.open`, `.text` and `.close` properties and its `.before*` and `.after*`
* satellites
* @param {Node} node Parsed Emmet abbreviation node
* @param {Function} fieldsRenderer A function for rendering fielded text (text with
* tabstops) for current node. @see ./render.js for details
*/
class OutputNode {
constructor(node, fieldsRenderer, options) {
if (typeof fieldsRenderer === 'object') {
options = fieldsRenderer;
fieldsRenderer = null;
}
this.node = node;
this._fieldsRenderer = fieldsRenderer || defaultFieldsRenderer;
this.open = null;
this.beforeOpen = '';
this.afterOpen = '';
this.close = null;
this.beforeClose = '';
this.afterClose = '';
this.text = null;
this.beforeText = '';
this.afterText = '';
this.indent = '';
this.newline = '';
if (options) {
Object.assign(this, options);
}
}
clone() {
return new this.constructor(this.node, this);
}
/**
* Properly indents given multiline text
* @param {String} text
*/
indentText(text) {
const lines = splitByLines(text);
if (lines.length === 1) {
// no newlines, nothing to indent
return text;
}
// No newline and no indent means no formatting at all:
// in this case we should replace newlines with spaces
const nl = (!this.newline && !this.indent) ? ' ' : this.newline;
return lines.map((line, i) => i ? this.indent + line : line).join(nl);
}
/**
* Renders given text with fields
* @param {String} text
* @return {String}
*/
renderFields(text) {
return this._fieldsRenderer(text);
}
toString(children) {
const open = this._wrap(this.open, this.beforeOpen, this.afterOpen);
const close = this._wrap(this.close, this.beforeClose, this.afterClose);
const text = this._wrap(this.text, this.beforeText, this.afterText);
return open + text + (children != null ? children : '') + close;
}
_wrap(str, before, after) {
before = before != null ? before : '';
after = after != null ? after : '';
// automatically trim whitespace for non-empty wraps
if (str != null) {
str = before ? str.replace(/^\s+/, '') : str;
str = after ? str.replace(/\s+$/, '') : str;
return before + this.indentText(str) + after;
}
return '';
}
}
/**
* Splits given text by lines
* @param {String} text
* @return {String[]}
*/
function splitByLines(text) {
return (text || '').split(/\r\n|\r|\n/g);
}
/**
* Default output of field (tabstop)
* @param {Number} index Field index
* @param {String} placeholder Field placeholder, can be null
* @return {String}
*/
const defaultField = (index, placeholder) => (placeholder || '');
/**
* Renders given parsed abbreviation `tree` via `formatter` function.
* @param {Node} tree Parsed Emmet abbreviation
* @param {Function} [field] Optional function to format field/tabstop (@see `defaultField`)
* @param {Function} formatter Output formatter function. It takes an output node—
* a special wrapper for parsed node that holds formatting and output properties—
* and updates its output properties to shape-up node’s output.
* Function arguments:
* – `outNode`: OutputNode
* – `renderFields`: a helper function that parses fields/tabstops from given
* text and replaces them with `field` function output.
* It also takes care about field indicies and ensures that the same indicies
* from different nodes won’t collide
*/
function render(tree, field, formatter) {
if (typeof formatter === 'undefined') {
formatter = field;
field = null;
}
field = field || defaultField;
// Each node may contain fields like `${1:placeholder}`.
// Since most modern editors will link all fields with the same
// index, we have to ensure that different nodes has their own indicies.
// We’ll use this `fieldState` object to globally increment field indices
// during output
const fieldState = { index: 1 };
const fieldsRenderer = text => text == null
? field(fieldState.index++)
: getFieldsModel(text, fieldState).mark(field);
return run(tree.children, formatter, fieldsRenderer);
}
function run(nodes, formatter, fieldsRenderer) {
return nodes.map(node => {
const outNode = formatter(new OutputNode(node, fieldsRenderer));
return outNode ? outNode.toString(run(node.children, formatter, fieldsRenderer)) : '';
}).join('');
}
/**
* Returns fields (tab-stops) model with properly updated indices that won’t
* collide with fields in other nodes of foprmatted tree
* @param {String|Object} text Text to get fields model from or model itself
* @param {Object} fieldState Abbreviation tree-wide field state reference
* @return {Object} Field model
*/
function getFieldsModel(text, fieldState) {
const model = typeof text === 'object' ? text : parse$2(text);
let largestIndex = -1;
model.fields.forEach(field => {
field.index += fieldState.index;
if (field.index > largestIndex) {
largestIndex = field.index;
}
});
if (largestIndex !== -1) {
fieldState.index = largestIndex + 1;
}
return model;
}
const TOKEN = /^(.*?)([A-Z_]+)(.*?)$/;
const TOKEN_OPEN = 91; // [
const TOKEN_CLOSE = 93; // ]
/**
* A basic templating engine.
* Takes every `[TOKEN]` from given string and replaces it with
* `TOKEN` value from given `data` attribute. The token itself may contain
* various characters between `[`, token name and `]`. Contents of `[...]` will
* be outputted only if `TOKEN` value is not empty. Also, only `TOKEN` name will
* be replaced with actual value, all other characters will remain as is.
*
* Example:
* ```
* template('[]', {NAME: 'foo'}) -> ""
* template('[]', {}) -> ""
* ```
*/
function template(str, data) {
if (str == null) {
return str;
}
// NB since token may contain inner `[` and `]`, we can’t just use regexp
// for replacement, should manually parse string instead
const stack = [];
const replacer = (str, left, token, right) =>
data[token] != null ? left + data[token] + right : '';
let output = '';
let offset = 0, i = 0;
let code, lastPos;
while (i < str.length) {
code = str.charCodeAt(i);
if (code === TOKEN_OPEN) {
stack.push(i);
} else if (code === TOKEN_CLOSE) {
lastPos = stack.pop();
if (!stack.length) {
output += str.slice(offset, lastPos) +
str.slice(lastPos + 1, i).replace(TOKEN, replacer);
offset = i + 1;
}
}
i++;
}
return output + str.slice(offset);
}
/**
* Various utility methods used by formatters
*/
/**
* Splits given text by lines
* @param {String} text
* @return {String[]}
*/
function splitByLines$1(text) {
return (text || '').split(/\r\n|\r|\n/g);
}
/**
* Check if given node is a first child in its parent
* @param {Node} node
* @return {Boolean}
*/
function isFirstChild(node) {
return node.parent.firstChild === node;
}
/**
* Check if given node is a last child in its parent node
* @param {Node} node
* @return {Boolean}
*/
/**
* Check if given node is a root node
* @param {Node} node
* @return {Boolean}
*/
function isRoot(node) {
return node && !node.parent;
}
/**
* Check if given node is a pseudo-snippet: a text-only node with explicitly
* defined children
* @param {Node} node
* @return {Boolean}
*/
function isPseudoSnippet(node) {
return node.isTextOnly && !!node.children.length;
}
/**
* Handles pseudo-snippet node.
* A pseudo-snippet is a text-only node with explicitly defined children.
* For such case, we have to figure out if pseudo-snippet contains fields
* (tab-stops) in node value and “split” it: make contents before field with
* lowest index node’s “open” part and contents after lowest index — “close”
* part. With this trick a final output will look like node’s children
* are nested inside node value
* @param {OutputNode} outNode
* @return {Boolean} Returns “true” if given node is a pseudo-snippets,
* `false` otherwise
*/
function handlePseudoSnippet(outNode) {
const node = outNode.node; // original abbreviaiton node
if (isPseudoSnippet(node)) {
const fieldsModel = parse$2(node.value);
const field = findLowestIndexField(fieldsModel);
if (field) {
const parts = splitFieldsModel(fieldsModel, field);
outNode.open = outNode.renderFields(parts[0]);
outNode.close = outNode.renderFields(parts[1]);
} else {
outNode.text = outNode.renderFields(fieldsModel);
}
return true;
}
return false;
}
/**
* Finds field with lowest index in given text
* @param {Object} model
* @return {Object}
*/
function findLowestIndexField(model) {
return model.fields.reduce((result, field) =>
!result || field.index < result.index ? field : result
, null);
}
/**
* Splits given fields model in two parts by given field
* @param {Object} model
* @param {Object} field
* @return {Array} Two-items array
*/
function splitFieldsModel(model, field) {
const ix = model.fields.indexOf(field);
const left = new model.constructor(
model.string.slice(0, field.location),
model.fields.slice(0, ix)
);
const right = new model.constructor(
model.string.slice(field.location + field.length),
model.fields.slice(ix + 1)
);
return [left, right];
}
const commentOptions = {
// enable node commenting
enabled: false,
// attributes that should trigger node commenting on specific node,
// if commenting is enabled
trigger: ['id', 'class'],
// comment before opening tag
before: '',
// comment after closing tag
after: '\n'
};
/**
* Renders given parsed Emmet abbreviation as HTML, formatted according to
* `profile` options
* @param {Node} tree Parsed Emmet abbreviation
* @param {Profile} profile Output profile
* @param {Object} [options] Additional formatter options
* @return {String}
*/
function html(tree, profile, options) {
options = Object.assign({}, options);
options.comment = Object.assign({}, commentOptions, options.comment);
return render(tree, options.field, outNode => {
outNode = setFormatting(outNode, profile);
if (!handlePseudoSnippet(outNode)) {
const node = outNode.node;
if (node.name) {
const name = profile.name(node.name);
const attrs = formatAttributes(outNode, profile);
outNode.open = `<${name}${attrs}${node.selfClosing ? profile.selfClose() : ''}>`;
if (!node.selfClosing) {
outNode.close = `${name}>`;
}
commentNode(outNode, options.comment);
}
// Do not generate fields for nodes with empty value and children
// or if node is self-closed
if (node.value || (!node.children.length && !node.selfClosing) ) {
outNode.text = outNode.renderFields(node.value);
}
}
return outNode;
});
}
/**
* Updates formatting properties for given output node
* @param {OutputNode} outNode Output wrapper of farsed abbreviation node
* @param {Profile} profile Output profile
* @return {OutputNode}
*/
function setFormatting(outNode, profile) {
const node = outNode.node;
if (shouldFormatNode(node, profile)) {
outNode.indent = profile.indent(getIndentLevel(node, profile));
outNode.newline = '\n';
const prefix = outNode.newline + outNode.indent;
// do not format the very first node in output
if (!isRoot(node.parent) || !isFirstChild(node)) {
outNode.beforeOpen = prefix;
if (node.isTextOnly) {
outNode.beforeText = prefix;
}
}
if (hasInnerFormatting(node, profile)) {
if (!node.isTextOnly) {
outNode.beforeText = prefix + profile.indent(1);
}
outNode.beforeClose = prefix;
}
}
return outNode;
}
/**
* Check if given node should be formatted
* @param {Node} node
* @param {Profile} profile
* @return {Boolean}
*/
function shouldFormatNode(node, profile) {
if (!profile.get('format')) {
return false;
}
if (node.parent.isTextOnly
&& node.parent.children.length === 1
&& parse$2(node.parent.value).fields.length) {
// Edge case: do not format the only child of text-only node,
// but only if parent contains fields
return false;
}
return isInline(node, profile) ? shouldFormatInline(node, profile) : true;
}
/**
* Check if given inline node should be formatted as well, e.g. it contains
* enough adjacent siblings that should force formatting
* @param {Node} node
* @param {Profile} profile
* @return {Boolean}
*/
function shouldFormatInline(node, profile) {
if (!isInline(node, profile)) {
return false;
}
if (isPseudoSnippet(node)) {
return true;
}
// check if inline node is the next sibling of block-level node
if (node.childIndex === 0) {
// first node in parent: format if it’s followed by a block-level element
let next = node;
while (next = next.nextSibling) {
if (!isInline(next, profile)) {
return true;
}
}
} else if (!isInline(node.previousSibling, profile)) {
// node is right after block-level element
return true;
}
if (profile.get('inlineBreak')) {
// check for adjacent inline elements before and after current element
let adjacentInline = 1;
let before = node, after = node;
while (isInlineElement((before = before.previousSibling), profile)) {
adjacentInline++;
}
while (isInlineElement((after = after.nextSibling), profile)) {
adjacentInline++;
}
if (adjacentInline >= profile.get('inlineBreak')) {
return true;
}
}
// Another edge case: inline node contains node that should receive foramtting
for (let i = 0, il = node.children.length; i < il; i++) {
if (shouldFormatNode(node.children[i], profile)) {
return true;
}
}
return false;
}
/**
* Check if given node contains inner formatting, e.g. any of its children should
* be formatted
* @param {Node} node
* @param {Profile} profile
* @return {Boolean}
*/
function hasInnerFormatting(node, profile) {
// check if node if forced for inner formatting
const nodeName = (node.name || '').toLowerCase();
if (profile.get('formatForce').indexOf(nodeName) !== -1) {
return true;
}
// check if any of children should receive formatting
// NB don’t use `childrent.some()` to reduce memory allocations
for (let i = 0; i < node.children.length; i++) {
if (shouldFormatNode(node.children[i], profile)) {
return true;
}
}
return false;
}
/**
* Outputs attributes of given abbreviation node as HTML attributes
* @param {OutputNode} outNode
* @param {Profile} profile
* @return {String}
*/
function formatAttributes(outNode, profile) {
const node = outNode.node;
return node.attributes.map(attr => {
if (attr.options.implied && attr.value == null) {
return null;
}
const attrName = profile.attribute(attr.name);
let attrValue = null;
// handle boolean attributes
if (attr.options.boolean || profile.get('booleanAttributes').indexOf(attrName.toLowerCase()) !== -1) {
if (profile.get('compactBooleanAttributes') && attr.value == null) {
return ` ${attrName}`;
} else if (attr.value == null) {
attrValue = attrName;
}
}
if (attrValue == null) {
attrValue = outNode.renderFields(attr.value);
}
// For https://github.com/Microsoft/vscode/issues/63703
// https://github.com/emmetio/markup-formatters/pull/2/files
return attr.options.before && attr.options.after
? ` ${attrName}=${attr.options.before+attrValue+attr.options.after}`
: ` ${attrName}=${profile.quote(attrValue)}`;
}).join('');
}
/**
* Check if given node is inline-level
* @param {Node} node
* @param {Profile} profile
* @return {Boolean}
*/
function isInline(node, profile) {
return (node && node.isTextOnly) || isInlineElement(node, profile);
}
/**
* Check if given node is inline-level element, e.g. element with explicitly
* defined node name
* @param {Node} node
* @param {Profile} profile
* @return {Boolean}
*/
function isInlineElement(node, profile) {
return node && profile.isInline(node);
}
/**
* Computes indent level for given node
* @param {Node} node
* @param {Profile} profile
* @param {Number} level
* @return {Number}
*/
function getIndentLevel(node, profile) {
// Increase indent level IF NOT:
// * parent is text-only node
// * there’s a parent node with a name that is explicitly set to decrease level
const skip = profile.get('formatSkip') || [];
let level = node.parent.isTextOnly ? -2 : -1;
let ctx = node;
while (ctx = ctx.parent) {
if (skip.indexOf( (ctx.name || '').toLowerCase() ) === -1) {
level++;
}
}
return level < 0 ? 0 : level;
}
/**
* Comments given output node, if required
* @param {OutputNode} outNode
* @param {Object} options
*/
function commentNode(outNode, options) {
const node = outNode.node;
if (!options.enabled || !options.trigger || !node.name) {
return;
}
const attrs = outNode.node.attributes.reduce((out, attr) => {
if (attr.name && attr.value != null) {
out[attr.name.toUpperCase().replace(/-/g, '_')] = attr.value;
}
return out;
}, {});
// add comment only if attribute trigger is present
for (let i = 0, il = options.trigger.length; i < il; i++) {
if (options.trigger[i].toUpperCase() in attrs) {
outNode.open = template(options.before, attrs) + outNode.open;
if (outNode.close) {
outNode.close += template(options.after, attrs);
}
break;
}
}
}
/**
* Common utility methods for indent-based syntaxes (Slim, Pug, etc.)
*/
const reId = /^id$/i;
const reClass = /^class$/i;
const defaultAttrOptions = {
primary: attrs => attrs.join(''),
secondary: attrs => attrs.map(attr => attr.isBoolean ? attr.name : `${attr.name}=${attr.value}`).join(', ')
};
const defaultNodeOptions = {
open: null,
close: null,
omitName: /^div$/i,
attributes: defaultAttrOptions
};
function indentFormat(outNode, profile, options) {
options = Object.assign({}, defaultNodeOptions, options);
const node = outNode.node;
outNode.indent = profile.indent(getIndentLevel$1(node, profile));
outNode.newline = '\n';
// Do not format the very first node in output
if (!isRoot(node.parent) || !isFirstChild(node)) {
outNode.beforeOpen = outNode.newline + outNode.indent;
}
if (node.name) {
const data = Object.assign({
NAME: profile.name(node.name),
SELF_CLOSE: node.selfClosing ? options.selfClose : null
}, getAttributes(outNode, profile, options.attributes));
// omit tag name if node has primary attributes
if (options.omitName && options.omitName.test(data.NAME) && data.PRIMARY_ATTRS) {
data.NAME = null;
}
if (options.open != null) {
outNode.open = template(options.open, data);
}
if (options.close != null) {
outNode.close = template(options.close, data);
}
}
return outNode;
}
/**
* Formats attributes of given node into a string.
* @param {OutputNode} node Output node wrapper
* @param {Profile} profile Output profile
* @param {Object} options Additional formatting options
* @return {String}
*/
function getAttributes(outNode, profile, options) {
options = Object.assign({}, defaultAttrOptions, options);
const primary = [], secondary = [];
const node = outNode.node;
node.attributes.forEach(attr => {
if (attr.options.implied && attr.value == null) {
return null;
}
const name = profile.attribute(attr.name);
const value = outNode.renderFields(attr.value);
if (reId.test(name)) {
value && primary.push(`#${value}`);
} else if (reClass.test(name)) {
value && primary.push(`.${value.replace(/\s+/g, '.')}`);
} else {
const isBoolean = attr.value == null
&& (attr.options.boolean || profile.get('booleanAttributes').indexOf(name.toLowerCase()) !== -1);
secondary.push({ name, value, isBoolean });
}
});
return {
PRIMARY_ATTRS: options.primary(primary) || null,
SECONDARY_ATTRS: options.secondary(secondary) || null
};
}
/**
* Computes indent level for given node
* @param {Node} node
* @param {Profile} profile
* @param {Number} level
* @return {Number}
*/
function getIndentLevel$1(node, profile) {
let level = node.parent.isTextOnly ? -2 : -1;
let ctx = node;
while (ctx = ctx.parent) {
level++;
}
return level < 0 ? 0 : level;
}
const reNl = /\n|\r/;
/**
* Renders given parsed Emmet abbreviation as HAML, formatted according to
* `profile` options
* @param {Node} tree Parsed Emmet abbreviation
* @param {Profile} profile Output profile
* @param {Object} [options] Additional formatter options
* @return {String}
*/
function haml(tree, profile, options) {
options = options || {};
const nodeOptions = {
open: '[%NAME][PRIMARY_ATTRS][(SECONDARY_ATTRS)][SELF_CLOSE]',
selfClose: '/',
attributes: {
secondary(attrs) {
return attrs.map(attr => attr.isBoolean
? `${attr.name}${profile.get('compactBooleanAttributes') ? '' : '=true'}`
: `${attr.name}=${profile.quote(attr.value)}`
).join(' ');
}
}
};
return render(tree, options.field, outNode => {
outNode = indentFormat(outNode, profile, nodeOptions);
outNode = updateFormatting(outNode, profile);
if (!handlePseudoSnippet(outNode)) {
const node = outNode.node;
// Do not generate fields for nodes with empty value and children
// or if node is self-closed
if (node.value || (!node.children.length && !node.selfClosing) ) {
outNode.text = outNode.renderFields(formatNodeValue(node, profile));
}
}
return outNode;
});
}
/**
* Updates formatting properties for given output node
* NB Unlike HTML, HAML is indent-based format so some formatting options from
* `profile` will not take effect, otherwise output will be broken
* @param {OutputNode} outNode Output wrapper of parsed abbreviation node
* @param {Profile} profile Output profile
* @return {OutputNode}
*/
function updateFormatting(outNode, profile) {
const node = outNode.node;
if (!node.isTextOnly && node.value) {
// node with text: put a space before single-line text
outNode.beforeText = reNl.test(node.value)
? outNode.newline + outNode.indent + profile.indent(1)
: ' ';
}
return outNode;
}
/**
* Formats value of given node: for multiline text we should add a ` |` suffix
* at the end of each line. Also ensure that text is perfectly aligned.
* @param {Node} node
* @param {Profile} profile
* @return {String|null}
*/
function formatNodeValue(node, profile) {
if (node.value != null && reNl.test(node.value)) {
const lines = splitByLines$1(node.value);
const indent = profile.indent(1);
const maxLength = lines.reduce((prev, line) => Math.max(prev, line.length), 0);
return lines.map((line, i) => `${i ? indent : ''}${pad(line, maxLength)} |`).join('\n');
}
return node.value;
}
function pad(text, len) {
while (text.length < len) {
text += ' ';
}
return text;
}
const reNl$1 = /\n|\r/;
const secondaryAttrs = {
none: '[ SECONDARY_ATTRS]',
round: '[(SECONDARY_ATTRS)]',
curly: '[{SECONDARY_ATTRS}]',
square: '[[SECONDARY_ATTRS]'
};
/**
* Renders given parsed Emmet abbreviation as Slim, formatted according to
* `profile` options
* @param {Node} tree Parsed Emmet abbreviation
* @param {Profile} profile Output profile
* @param {Object} [options] Additional formatter options
* @return {String}
*/
function slim(tree, profile, options) {
options = options || {};
const SECONDARY_ATTRS = options.attributeWrap
&& secondaryAttrs[options.attributeWrap]
|| secondaryAttrs.none;
const booleanAttr = SECONDARY_ATTRS === secondaryAttrs.none
? attr => `${attr.name}=true`
: attr => attr.name;
const nodeOptions = {
open: `[NAME][PRIMARY_ATTRS]${SECONDARY_ATTRS}[SELF_CLOSE]`,
selfClose: '/',
attributes: {
secondary(attrs) {
return attrs.map(attr => attr.isBoolean
? booleanAttr(attr)
: `${attr.name}=${profile.quote(attr.value)}`
).join(' ');
}
}
};
return render(tree, options.field, (outNode, renderFields) => {
outNode = indentFormat(outNode, profile, nodeOptions);
outNode = updateFormatting$1(outNode, profile);
if (!handlePseudoSnippet(outNode)) {
const node = outNode.node;
// Do not generate fields for nodes with empty value and children
// or if node is self-closed
if (node.value || (!node.children.length && !node.selfClosing) ) {
outNode.text = outNode.renderFields(formatNodeValue$1(node, profile));
}
}
return outNode;
});
}
/**
* Updates formatting properties for given output node
* NB Unlike HTML, Slim is indent-based format so some formatting options from
* `profile` will not take effect, otherwise output will be broken
* @param {OutputNode} outNode Output wrapper of farsed abbreviation node
* @param {Profile} profile Output profile
* @return {OutputNode}
*/
function updateFormatting$1(outNode, profile) {
const node = outNode.node;
const parent = node.parent;
// Edge case: a single inline-level child inside node without text:
// allow it to be inlined
if (profile.get('inlineBreak') === 0 && isInline$1(node, profile)
&& !isRoot(parent) && parent.value == null && parent.children.length === 1) {
outNode.beforeOpen = ': ';
}
if (!node.isTextOnly && node.value) {
// node with text: put a space before single-line text
outNode.beforeText = reNl$1.test(node.value)
? outNode.newline + outNode.indent + profile.indent(1)
: ' ';
}
return outNode;
}
/**
* Formats value of given node: for multiline text we should precede each
* line with `| ` with one-level deep indent
* @param {Node} node
* @param {Profile} profile
* @return {String|null}
*/
function formatNodeValue$1(node, profile) {
if (node.value != null && reNl$1.test(node.value)) {
const indent = profile.indent(1);
return splitByLines$1(node.value).map((line, i) => `${indent}${i ? ' ' : '|'} ${line}`).join('\n');
}
return node.value;
}
/**
* Check if given node is inline-level
* @param {Node} node
* @param {Profile} profile
* @return {Boolean}
*/
function isInline$1(node, profile) {
return node && (node.isTextOnly || profile.isInline(node));
}
const reNl$2 = /\n|\r/;
/**
* Renders given parsed Emmet abbreviation as Pug, formatted according to
* `profile` options
* @param {Node} tree Parsed Emmet abbreviation
* @param {Profile} profile Output profile
* @param {Object} [options] Additional formatter options
* @return {String}
*/
function pug(tree, profile, options) {
options = options || {};
const nodeOptions = {
open: '[NAME][PRIMARY_ATTRS][(SECONDARY_ATTRS)]',
attributes: {
secondary(attrs) {
return attrs.map(attr => attr.isBoolean ? attr.name : `${attr.name}=${profile.quote(attr.value)}`).join(', ');
}
}
};
return render(tree, options.field, outNode => {
outNode = indentFormat(outNode, profile, nodeOptions);
outNode = updateFormatting$2(outNode, profile);
if (!handlePseudoSnippet(outNode)) {
const node = outNode.node;
// Do not generate fields for nodes with empty value and children
// or if node is self-closed
if (node.value || (!node.children.length && !node.selfClosing) ) {
outNode.text = outNode.renderFields(formatNodeValue$2(node, profile));
}
}
return outNode;
});
}
/**
* Updates formatting properties for given output node
* NB Unlike HTML, Pug is indent-based format so some formatting options from
* `profile` will not take effect, otherwise output will be broken
* @param {OutputNode} outNode Output wrapper of parsed abbreviation node
* @param {Profile} profile Output profile
* @return {OutputNode}
*/
function updateFormatting$2(outNode, profile) {
const node = outNode.node;
if (!node.isTextOnly && node.value) {
// node with text: put a space before single-line text
outNode.beforeText = reNl$2.test(node.value)
? outNode.newline + outNode.indent + profile.indent(1)
: ' ';
}
return outNode;
}
/**
* Formats value of given node: for multiline text we should precede each
* line with `| ` with one-level deep indent
* @param {Node} node
* @param {Profile} profile
* @return {String|null}
*/
function formatNodeValue$2(node, profile) {
if (node.value != null && reNl$2.test(node.value)) {
const indent = profile.indent(1);
return splitByLines$1(node.value).map(line => `${indent}| ${line}`).join('\n');
}
return node.value;
}
const supportedSyntaxed = { html, haml, slim, pug };
/**
* Outputs given parsed abbreviation in specified syntax
* @param {Node} tree Parsed abbreviation tree
* @param {Profile} profile Output profile
* @param {String} [syntax] Output syntax. If not given, `html` syntax is used
* @param {Function} options.field A function to output field/tabstop for
* host editor. This function takes two arguments: `index` and `placeholder` and
* should return a string that represents tabstop in host editor. By default
* only a placeholder is returned
* @example
* {
* field(index, placeholder) {
* // return field in TextMate-style, e.g. ${1} or ${2:foo}
* return `\${${index}${placeholder ? ':' + placeholder : ''}}`;
* }
* }
* @return {String}
*/
var index$3 = function(tree, profile, syntax, options) {
if (typeof syntax === 'object') {
options = syntax;
syntax = null;
}
if (!supports(syntax)) {
// fallback to HTML if given syntax is not supported
syntax = 'html';
}
return supportedSyntaxed[syntax](tree, profile, options);
};
/**
* Check if given syntax is supported
* @param {String} syntax
* @return {Boolean}
*/
function supports(syntax) {
return !!syntax && syntax in supportedSyntaxed;
}
/**
* Expands given abbreviation into code
* @param {String|Node} abbr Abbreviation to parse or already parsed abbreviation
* @param {Object} options
* @return {String}
*/
function expand(abbr, options) {
options = options || {};
if (typeof abbr === 'string') {
abbr = parse$3(abbr, options);
}
return index$3(abbr, options.profile, options.syntax, options.format);
}
/**
* Parses given Emmet abbreviation into a final abbreviation tree with all
* required transformations applied
* @param {String} Abbreviation to parse
* @param {Object} options
* @return {Node}
*/
function parse$3(abbr, options) {
return index(abbr)
.use(index$1, options.snippets)
.use(replaceVariables, options.variables)
.use(index$2, options.text, options.addons);
}
/**
* A wrapper for holding CSS value
*/
class CSSValue {
constructor() {
this.type = 'css-value';
this.value = [];
}
get size() {
return this.value.length;
}
add(value) {
this.value.push(value);
}
has(value) {
return this.value.indexOf(value) !== -1;
}
toString() {
return this.value.join(' ');
}
}
const HASH$1 = 35; // #
const DOT$2 = 46; // .
/**
* Consumes a color token from given string
* @param {StreamReader} stream
* @return {Color} Returns consumend color object, `undefined` otherwise
*/
var consumeColor = function(stream) {
// supported color variations:
// #abc → #aabbccc
// #0 → #000000
// #fff.5 → rgba(255, 255, 255, 0.5)
// #t → transparent
if (stream.peek() === HASH$1) {
stream.start = stream.pos;
stream.next();
stream.eat(116) /* t */ || stream.eatWhile(isHex);
const base = stream.current();
// a hex color can be followed by `.num` alpha value
stream.start = stream.pos;
if (stream.eat(DOT$2) && !stream.eatWhile(isNumber)) {
throw stream.error('Unexpected character for alpha value of color');
}
return new Color(base, stream.current());
}
};
class Color {
constructor(value, alpha) {
this.type = 'color';
this.raw = value;
this.alpha = Number(alpha != null && alpha !== '' ? alpha : 1);
value = value.slice(1); // remove #
let r = 0, g = 0, b = 0;
if (value === 't') {
this.alpha = 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);
}
}
this.r = parseInt(r, 16);
this.g = parseInt(g, 16);
this.b = parseInt(b, 16);
}
/**
* Output current color as hex value
* @param {Boolean} shor Produce short value (e.g. #fff instead of #ffffff), if possible
* @return {String}
*/
toHex(short) {
const fn = (short && isShortHex(this.r) && isShortHex(this.g) && isShortHex(this.b))
? toShortHex : toHex;
return '#' + fn(this.r) + fn(this.g) + fn(this.b);
}
/**
* Output current color as `rgba?(...)` CSS color
* @return {String}
*/
toRGB() {
const values = [this.r, this.g, this.b];
if (this.alpha !== 1) {
values.push(this.alpha.toFixed(8).replace(/\.?0+$/, ''));
}
return `${values.length === 3 ? 'rgb' : 'rgba'}(${values.join(', ')})`;
}
toString(short) {
if (!this.r && !this.g && !this.b && !this.alpha) {
return 'transparent';
}
return this.alpha === 1 ? this.toHex(short) : this.toRGB();
}
}
/**
* Check if given code is a hex value (/0-9a-f/)
* @param {Number} code
* @return {Boolean}
*/
function isHex(code) {
return isNumber(code) || isAlpha(code, 65, 70); // A-F
}
function isShortHex(hex) {
return !(hex % 17);
}
function toShortHex(num) {
return (num >> 4).toString(16);
}
function toHex(num) {
return pad$1(num.toString(16), 2);
}
function pad$1(value, len) {
while (value.length < len) {
value = '0' + value;
}
return value;
}
/**
* @param {Number} code
* @return {Boolean}
*/
function isAlphaNumericWord(code) {
return isNumber(code) || isAlphaWord(code);
}
/**
* @param {Number} code
* @return {Boolean}
*/
function isAlphaWord(code) {
return code === 95 /* _ */ || isAlpha(code);
}
const PERCENT = 37; // %
const DOT$1$1 = 46; // .
const DASH$1 = 45; // -
/**
* Consumes numeric CSS value (number with optional unit) from current stream,
* if possible
* @param {StreamReader} stream
* @return {NumericValue}
*/
var consumeNumericValue = function(stream) {
stream.start = stream.pos;
if (eatNumber(stream)) {
const num = stream.current();
stream.start = stream.pos;
// eat unit, which can be a % or alpha word
stream.eat(PERCENT) || stream.eatWhile(isAlphaWord);
return new NumericValue(num, stream.current());
}
};
/**
* A numeric CSS value with optional unit
*/
class NumericValue {
constructor(value, unit) {
this.type = 'numeric';
this.value = Number(value);
this.unit = unit || '';
}
toString() {
return `${this.value}${this.unit}`;
}
}
/**
* Eats number value from given stream
* @param {StreamReader} stream
* @return {Boolean} Returns `true` if number was consumed
*/
function eatNumber(stream) {
const start = stream.pos;
const negative = stream.eat(DASH$1);
let hadDot = false, consumed = false, code;
while (!stream.eof()) {
code = stream.peek();
// either a second dot or not a number: stop parsing
if (code === DOT$1$1 ? hadDot : !isNumber(code)) {
break;
}
consumed = true;
if (code === DOT$1$1) {
hadDot = true;
}
stream.next();
}
if (negative && !consumed) {
// edge case: consumed dash only, bail out
stream.pos = start;
}
return start !== stream.pos;
}
const DOLLAR$1 = 36; // $
const DASH$2 = 45; // -
const AT$1 = 64; // @
/**
* Consumes a keyword: either a variable (a word that starts with $ or @) or CSS
* keyword or shorthand
* @param {StreamReader} stream
* @param {Boolean} [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
* @return {String} Consumed variable
*/
var consumeKeyword = function(stream, short) {
stream.start = stream.pos;
if (stream.eat(DOLLAR$1) || stream.eat(AT$1)) {
// SCSS or LESS variable
stream.eatWhile(isVariableName);
} else if (short) {
stream.eatWhile(isAlphaWord);
} else {
stream.eatWhile(isKeyword);
}
return stream.start !== stream.pos ? new Keyword(stream.current()) : null;
};
class Keyword {
constructor(value) {
this.type = 'keyword';
this.value = value;
}
toString() {
return this.value;
}
}
function isKeyword(code) {
return isAlphaNumericWord(code) || code === DASH$2;
}
function isVariableName(code) {
return code === 45 /* - */ || isAlphaNumericWord(code);
}
const opt$1 = { throws: true };
/**
* Consumes 'single' or "double"-quoted string from given string, if possible
* @param {StreamReader} stream
* @return {String}
*/
var consumeQuoted$1 = function(stream) {
if (eatQuoted(stream, opt$1)) {
return new QuotedString(stream.current());
}
};
class QuotedString {
constructor(value) {
this.type = 'string';
this.value = value;
}
toString() {
return this.value;
}
}
const LBRACE = 40; // (
const RBRACE = 41; // )
const COMMA = 44; // ,
/**
* Consumes arguments from given string.
* Arguments are comma-separated list of CSS values inside round braces, e.g.
* `(1, a2, 'a3')`. Nested lists and quoted strings are supported
* @param {StreamReader} stream
* @return {Array} Array of arguments, `null` if arguments cannot be consumed
*/
function consumeArgumentList(stream) {
if (!stream.eat(LBRACE)) {
// not an argument list
return null;
}
let level = 1, code, arg;
const argsList = [];
while (!stream.eof()) {
if (arg = consumeArgument(stream)) {
argsList.push(arg);
} else {
// didn’t consumed argument, expect argument separator or end-of-arguments
stream.eatWhile(isWhiteSpace);
if (stream.eat(RBRACE)) {
// end of arguments list
break;
}
if (!stream.eat(COMMA)) {
throw stream.error('Expected , or )');
}
}
}
return argsList;
}
/**
* Consumes a single argument. An argument is a `CSSValue`, e.g. it could be
* a space-separated string of value
* @param {StreamReader} stream
* @return {CSSValue}
*/
function consumeArgument(stream) {
const result = new CSSValue();
let value;
while (!stream.eof()) {
stream.eatWhile(isWhiteSpace);
value = consumeNumericValue(stream) || consumeColor(stream)
|| consumeQuoted$1(stream) || consumeKeywordOrFunction(stream);
if (!value) {
break;
}
result.add(value);
}
return result.size ? result : null;
}
/**
* Consumes either function call like `foo()` or keyword like `foo`
* @param {StreamReader} stream
* @return {Keyword|FunctionCall}
*/
function consumeKeywordOrFunction(stream) {
const kw = consumeKeyword(stream);
if (kw) {
const args = consumeArgumentList(stream);
return args ? new FunctionCall(kw.toString(), args) : kw;
}
}
class FunctionCall {
/**
* @param {String} name Function name
* @param {Array} args Function arguments
*/
constructor(name, args) {
this.type = 'function';
this.name = name;
this.args = args || [];
}
toString() {
return `${this.name}(${this.args.join(', ')})`;
}
}
const EXCL$1 = 33; // !
const DOLLAR$2 = 36; // $
const PLUS = 43; // +
const DASH = 45; // -
const COLON$1 = 58; // :
const AT = 64; // @
/**
* Parses given Emmet CSS abbreviation and returns it as parsed Node tree
* @param {String} abbr
* @return {Node}
*/
var index$4 = function(abbr) {
const root = new Node();
const stream = new StreamReader(abbr);
while (!stream.eof()) {
let node = new Node(consumeIdent(stream));
node.value = consumeValue(stream);
const args = consumeArgumentList(stream);
if (args) {
// technically, arguments in CSS are anonymous Emmet Node attributes,
// but since Emmet can support only one anonymous, `null`-name
// attribute (for good reasons), we’ll use argument index as name
for (let i = 0; i < args.length; i++) {
node.setAttribute(String(i), args[i]);
}
}
// Consume `!important` modifier at the end of expression
if (stream.eat(EXCL$1)) {
node.value.add('!');
}
root.appendChild(node);
// CSS abbreviations cannot be nested, only listed
if (!stream.eat(PLUS)) {
break;
}
}
if (!stream.eof()) {
throw stream.error('Unexpected character');
}
return root;
};
/**
* Consumes CSS property identifier from given stream
* @param {StreamReader} stream
* @return {String}
*/
function consumeIdent(stream) {
stream.start = stream.pos;
stream.eatWhile(isIdentPrefix);
stream.eatWhile(isIdent);
return stream.start !== stream.pos ? stream.current() : null;
}
/**
* Consumes embedded value from Emmet CSS abbreviation stream
* @param {StreamReader} stream
* @return {CSSValue}
*/
function consumeValue(stream) {
const values = new CSSValue();
let value;
while (!stream.eof()) {
// use colon as value separator
stream.eat(COLON$1);
if (value = consumeNumericValue(stream) || consumeColor(stream)) {
// edge case: a dash after unit-less numeric value or color should
// be treated as value separator, not negative sign
if (!value.unit) {
stream.eat(DASH);
}
} else {
stream.eat(DASH);
value = consumeKeyword(stream, true);
}
if (!value) {
break;
}
values.add(value);
}
return values;
}
/**
* @param {Number} code
* @return {Boolean}
*/
function isIdent(code) {
return isAlphaWord(code);
}
/**
* @param {Number} code
* @return {Boolean}
*/
function isIdentPrefix(code) {
return code === AT || code === DOLLAR$2 || code === EXCL$1;
}
const DASH$3 = 45; // -
/**
* Calculates fuzzy match score of how close `abbr` matches given `string`.
* @param {String} abbr Abbreviation to score
* @param {String} string String to match
* @param {Number} [fuzziness] Fuzzy factor
* @return {Number} Match score
*/
var stringScore = function(abbr, string) {
abbr = abbr.toLowerCase();
string = string.toLowerCase();
if (abbr === string) {
return 1;
}
// a string MUST start with the same character as abbreviation
if (!string || abbr.charCodeAt(0) !== string.charCodeAt(0)) {
return 0;
}
const abbrLength = abbr.length;
const stringLength = string.length;
let i = 1, j = 1, score = stringLength;
let ch1, ch2, found, acronym;
while (i < abbrLength) {
ch1 = abbr.charCodeAt(i);
found = false;
acronym = false;
while (j < stringLength) {
ch2 = string.charCodeAt(j);
if (ch1 === ch2) {
found = true;
score += (stringLength - j) * (acronym ? 2 : 1);
break;
}
// add acronym bonus for exactly next match after unmatched `-`
acronym = ch2 === DASH$3;
j++;
}
if (!found) {
break;
}
i++;
}
return score && score * (i / abbrLength) / sum(stringLength);
};
/**
* Calculates sum of first `n` natural numbers, e.g. 1+2+3+...n
* @param {Number} n
* @return {Number}
*/
function sum(n) {
return n * (n + 1) / 2;
}
const reProperty = /^([a-z\-]+)(?:\s*:\s*([^\n\r]+))?$/;
const DASH$1$1 = 45; // -
/**
* Creates a special structure for resolving CSS properties from plain CSS
* snippets.
* Almost all CSS snippets are aliases for real CSS properties with available
* value variants, optionally separated by `|`. Most values are keywords that
* can be fuzzy-resolved as well. Some CSS properties are shorthands for other,
* more specific properties, like `border` and `border-style`. For such cases
* keywords from more specific properties should be available in shorthands too.
* @param {Snippet[]} snippets
* @return {CSSSnippet[]}
*/
var cssSnippets = function(snippets) {
return nest( snippets.map(snippet => new CSSSnippet(snippet.key, snippet.value)) );
};
class CSSSnippet {
constructor(key, value) {
this.key = key;
this.value = value;
this.property = null;
// detect if given snippet is a property
const m = value && value.match(reProperty);
if (m) {
this.property = m[1];
this.value = m[2];
}
this.dependencies = [];
}
addDependency(dep) {
this.dependencies.push(dep);
}
get defaulValue() {
return this.value != null ? splitValue(this.value)[0] : null;
}
/**
* Returns list of unique keywords for current CSS snippet and its dependencies
* @return {String[]}
*/
keywords() {
const stack = [];
const keywords = new Set();
let i = 0, item, candidates;
if (this.property) {
// scan valid CSS-properties only
stack.push(this);
}
while (i < stack.length) {
// NB Keep items in stack instead of push/pop to avoid possible
// circular references
item = stack[i++];
if (item.value) {
candidates = splitValue(item.value).filter(isKeyword$1);
// extract possible keywords from snippet value
for (let j = 0; j < candidates.length; j++) {
keywords.add(candidates[j].trim());
}
// add dependencies into scan stack
for (let j = 0, deps = item.dependencies; j < deps.length; j++) {
if (stack.indexOf(deps[j]) === -1) {
stack.push(deps[j]);
}
}
}
}
return Array.from(keywords);
}
}
/**
* Nests more specific CSS properties into shorthand ones, e.g.
* background-position-x -> background-position -> background
* @param {CSSSnippet[]} snippets
* @return {CSSSnippet[]}
*/
function nest(snippets) {
snippets = snippets.sort(snippetsSort);
const stack = [];
// For sorted list of CSS properties, create dependency graph where each
// shorthand property contains its more specific one, e.g.
// backgound -> background-position -> background-position-x
for (let i = 0, cur, prev; i < snippets.length; i++) {
cur = snippets[i];
if (!cur.property) {
// not a CSS property, skip it
continue;
}
// 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.indexOf(prev.property) === 0
&& cur.property.charCodeAt(prev.property.length) === DASH$1$1) {
prev.addDependency(cur);
stack.push(cur);
break;
}
stack.pop();
}
if (!stack.length) {
stack.push(cur);
}
}
return snippets;
}
/**
* A sorting function for array of snippets
* @param {CSSSnippet} a
* @param {CSSSnippet} b
* @return {Number}
*/
function snippetsSort(a, b) {
if (a.key === b.key) {
return 0;
}
return a.key < b.key ? -1 : 1;
}
/**
* Check if given string is a keyword candidate
* @param {String} str
* @return {Boolean}
*/
function isKeyword$1(str) {
return /^\s*[\w\-]+/.test(str);
}
function splitValue(value) {
return String(value).split('|');
}
const globalKeywords = ['auto', 'inherit', 'unset'];
const unitlessProperties = [
'z-index', 'line-height', 'opacity', 'font-weight', 'zoom',
'flex', 'flex-grow', 'flex-shrink'
];
const defaultOptions$3 = {
intUnit: 'px',
floatUnit: 'em',
unitAliases: {
e :'em',
p: '%',
x: 'ex',
r: 'rem'
},
fuzzySearchMinScore: 0
};
/**
* For every node in given `tree`, finds matching snippet from `registry` and
* updates node with snippet data.
*
* This resolver uses fuzzy matching for searching matched snippets and their
* keyword values.
*/
var index$5 = function(tree, registry, options) {
const snippets = convertToCSSSnippets(registry);
options = {
intUnit: (options && options.intUnit) || defaultOptions$3.intUnit,
floatUnit: (options && options.floatUnit) || defaultOptions$3.floatUnit,
unitAliases: Object.assign({}, defaultOptions$3.unitAliases, options && options.unitAliases),
fuzzySearchMinScore: (options && options.fuzzySearchMinScore) || defaultOptions$3.fuzzySearchMinScore
};
tree.walk(node => resolveNode$1(node, snippets, options));
return tree;
};
function convertToCSSSnippets(registry) {
return cssSnippets(registry.all({type: 'string'}))
}
/**
* Resolves given node: finds matched CSS snippets using fuzzy match and resolves
* keyword aliases from node value
* @param {Node} node
* @param {CSSSnippet[]} snippets
* @param {Object} options
* @return {Node}
*/
function resolveNode$1(node, snippets, options) {
const snippet = findBestMatch(node.name, snippets, 'key', options.fuzzySearchMinScore);
if (!snippet) {
// Edge case: `!important` snippet
return node.name === '!' ? setNodeAsText(node, '!important') : node;
}
return snippet.property
? resolveAsProperty(node, snippet, options)
: resolveAsSnippet(node, snippet);
}
/**
* Resolves given parsed abbreviation node as CSS propery
* @param {Node} node
* @param {CSSSnippet} snippet
* @param {Object} formatOptions
* @return {Node}
*/
function resolveAsProperty(node, snippet, formatOptions) {
const abbr = node.name;
node.name = snippet.property;
if (node.value && typeof node.value === 'object') {
// resolve keyword shortcuts
const keywords = snippet.keywords();
if (!node.value.size) {
// no value defined, try to resolve unmatched part as a keyword alias
let kw = findBestMatch(getUnmatchedPart(abbr, snippet.key), keywords);
if (!kw) {
// no matching value, try to get default one
kw = snippet.defaulValue;
if (kw && kw.indexOf('${') === -1) {
// Quick and dirty test for existing field. If not, wrap
// default value in a field
kw = `\${1:${kw}}`;
}
}
if (kw) {
node.value.add(kw);
}
} else {
// replace keyword aliases in current node value
for (let i = 0, token; i < node.value.value.length; i++) {
token = node.value.value[i];
if (token === '!') {
token = `${!i ? '${1} ' : ''}!important`;
} else if (isKeyword$2(token)) {
token = findBestMatch(token.value, keywords)
|| findBestMatch(token.value, globalKeywords)
|| token;
} else if (isNumericValue(token)) {
token = resolveNumericValue(node.name, token, formatOptions);
}
node.value.value[i] = token;
}
}
}
return node;
}
/**
* Resolves given parsed abbreviation node as a snippet: a plain code chunk
* @param {Node} node
* @param {CSSSnippet} snippet
* @return {Node}
*/
function resolveAsSnippet(node, snippet) {
return setNodeAsText(node, snippet.value);
}
/**
* Sets given parsed abbreviation node as a text snippet
* @param {Node} node
* @param {String} text
* @return {Node}
*/
function setNodeAsText(node, text) {
node.name = null;
node.value = text;
return node;
}
/**
* Finds best matching item from `items` array
* @param {String} abbr Abbreviation to match
* @param {Array} items List of items for match
* @param {String} [key] If `items` is a list of objects, use `key` as object
* property to test against
* @param {Number} fuzzySearchMinScore The minimum score the best matched item should have to be a valid match.
* @return {*}
*/
function findBestMatch(abbr, items, key, fuzzySearchMinScore) {
if (!abbr) {
return null;
}
let matchedItem = null;
let maxScore = 0;
fuzzySearchMinScore = fuzzySearchMinScore || 0;
for (let i = 0, item; i < items.length; i++) {
item = items[i];
const score = stringScore(abbr, getScoringPart(item, key));
if (score === 1) {
// direct hit, no need to look further
return item;
}
if (score && score >= maxScore) {
maxScore = score;
matchedItem = item;
}
}
return maxScore >= fuzzySearchMinScore ? matchedItem : null;
}
function getScoringPart(item, key) {
const value = item && typeof item === 'object' ? item[key] : item;
const m = (value || '').match(/^[\w-@]+/);
return m ? m[0] : value;
}
/**
* Returns a part of `abbr` that wasn’t directly matched agains `string`.
* For example, if abbreviation `poas` is matched against `position`, the unmatched part will be `as`
* since `a` wasn’t found in string stream
* @param {String} abbr
* @param {String} string
* @return {String}
*/
function getUnmatchedPart(abbr, string) {
for (let i = 0, lastPos = 0; i < abbr.length; i++) {
lastPos = string.indexOf(abbr[i], lastPos);
if (lastPos === -1) {
return abbr.slice(i);
}
lastPos++;
}
return '';
}
/**
* Check if given CSS value token is a keyword
* @param {*} token
* @return {Boolean}
*/
function isKeyword$2(token) {
return tokenTypeOf(token, 'keyword');
}
/**
* Check if given CSS value token is a numeric value
* @param {*} token
* @return {Boolean}
*/
function isNumericValue(token) {
return tokenTypeOf(token, 'numeric');
}
function tokenTypeOf(token, type) {
return token && typeof token === 'object' && token.type === type;
}
/**
* Resolves numeric value for given CSS property
* @param {String} property CSS property name
* @param {NumericValue} token CSS numeric value token
* @param {Object} formatOptions Formatting options for units
* @return {NumericValue}
*/
function resolveNumericValue(property, token, formatOptions) {
if (token.unit) {
token.unit = formatOptions.unitAliases[token.unit] || token.unit;
} else if (token.value !== 0 && unitlessProperties.indexOf(property) === -1) {
// use `px` for integers, `em` for floats
// NB: num|0 is a quick alternative to Math.round(0)
token.unit = token.value === (token.value|0) ? formatOptions.intUnit : formatOptions.floatUnit;
}
return token;
}
const defaultOptions$4 = {
shortHex: true,
format: {
between: ': ',
after: ';'
}
};
/**
* Renders given parsed Emmet CSS abbreviation as CSS-like
* stylesheet, formatted according to `profile` options
* @param {Node} tree Parsed Emmet abbreviation
* @param {Profile} profile Output profile
* @param {Object} [options] Additional formatter options
* @return {String}
*/
function css(tree, profile, options) {
options = Object.assign({}, defaultOptions$4, options);
return render(tree, options.field, outNode => {
const node = outNode.node;
let value = String(node.value || '');
if (node.attributes.length) {
const fieldValues = node.attributes.map(attr => stringifyAttribute(attr, options));
value = injectFields(value, fieldValues);
}
outNode.open = node.name && profile.name(node.name);
outNode.afterOpen = options.format.between;
outNode.text = outNode.renderFields(value || null);
if (outNode.open && (!outNode.text || !outNode.text.endsWith(';'))) {
outNode.afterText = options.format.after;
}
if (profile.get('format')) {
outNode.newline = '\n';
if (tree.lastChild !== node) {
outNode.afterText += outNode.newline;
}
}
return outNode;
});
}
/**
* Injects given field values at each field of given string
* @param {String} string
* @param {String[]} attributes
* @return {FieldString}
*/
function injectFields(string, values) {
const fieldsModel = parse$2(string);
const fieldsAmount = fieldsModel.fields.length;
if (fieldsAmount) {
values = values.slice();
if (values.length > fieldsAmount) {
// More values that output fields: collapse rest values into
// a single token
values = values.slice(0, fieldsAmount - 1)
.concat(values.slice(fieldsAmount - 1).join(', '));
}
while (values.length) {
const value = values.shift();
const field = fieldsModel.fields.shift();
const delta = value.length - field.length;
fieldsModel.string = fieldsModel.string.slice(0, field.location)
+ value
+ fieldsModel.string.slice(field.location + field.length);
// Update location of the rest fields in string
for (let i = 0, il = fieldsModel.fields.length; i < il; i++) {
fieldsModel.fields[i].location += delta;
}
}
}
return fieldsModel;
}
function stringifyAttribute(attr, options) {
if (attr.value && typeof attr.value === 'object' && attr.value.type === 'css-value') {
return attr.value.value
.map(token => {
if (token && typeof token === 'object') {
return token.type === 'color'
? token.toString(options.shortHex)
: token.toString();
}
return String(token);
})
.join(' ');
}
return attr.value != null ? String(attr.value) : '';
}
const syntaxFormat = {
css: {
between: ': ',
after: ';'
},
scss: 'css',
less: 'css',
sass: {
between: ': ',
after: ''
},
stylus: {
between: ' ',
after: ''
}
};
/**
* Outputs given parsed abbreviation in specified stylesheet syntax
* @param {Node} tree Parsed abbreviation tree
* @param {Profile} profile Output profile
* @param {String} [syntax] Output syntax. If not given, `css` syntax is used
* @param {Function} options.field A function to output field/tabstop for
* host editor. This function takes two arguments: `index` and `placeholder` and
* should return a string that represents tabstop in host editor. By default
* only a placeholder is returned
* @example
* {
* field(index, placeholder) {
* // return field in TextMate-style, e.g. ${1} or ${2:foo}
* return `\${${index}${placeholder ? ':' + placeholder : ''}}`;
* }
* }
* @return {String}
*/
var index$6 = function(tree, profile, syntax, options) {
if (typeof syntax === 'object') {
options = syntax;
syntax = null;
}
if (!supports$1(syntax)) {
// fallback to CSS if given syntax is not supported
syntax = 'css';
}
options = Object.assign({}, options, {
format: getFormat(syntax, options)
});
// CSS abbreviations doesn’t support nesting so simply
// output root node children
return css(tree, profile, options);
};
/**
* Check if given syntax is supported
* @param {String} syntax
* @return {Boolean}
*/
function supports$1(syntax) {
return !!syntax && syntax in syntaxFormat;
}
/**
* Returns formatter object for given syntax
* @param {String} syntax
* @param {Object} [options]
* @return {Object} Formatter object as defined in `syntaxFormat`
*/
function getFormat(syntax, options) {
let format = syntaxFormat[syntax];
if (typeof format === 'string') {
format = syntaxFormat[format];
}
return Object.assign({}, format, options && options.stylesheet);
}
/**
* Expands given abbreviation into code
* @param {String|Node} abbr Abbreviation to parse or already parsed abbreviation
* @param {Object} options
* @return {String}
*/
function expand$1(abbr, options) {
options = options || {};
if (typeof abbr === 'string') {
abbr = parse$4(abbr, options);
}
return index$6(abbr, options.profile, options.syntax, options.format);
}
/**
* Parses given Emmet abbreviation into a final abbreviation tree with all
* required transformations applied
* @param {String|Node} abbr Abbreviation to parse or already parsed abbreviation
* @param {Object} options
* @return {Node}
*/
function parse$4(abbr, options) {
if (typeof abbr === 'string') {
abbr = index$4(abbr);
}
return abbr.use(index$5, options.snippets, options.format ? options.format.stylesheet : {});
}
var html$1 = {
"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:kw": "meta[name=keywords content]",
"meta:desc": "meta[name=description content]",
"style": "style",
"script": "script",
"script:src": "script[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: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: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",
"datag": "datagrid",
"datal": "datalist",
"kg": "keygen",
"out": "output",
"det": "details",
"cmd": "command",
"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",
"!!!": "{}",
"doc": "html[lang=${lang}]>(head>meta[charset=${charset}]+meta:vp+title{${1:Document}})+body",
"!|html:5": "!!!+doc",
"c": "{}",
"cc:ie": "{}",
"cc:noie": "{${0}}"
};
var css$1 = {
"@f": "@font-face {\n\tfont-family: ${1};\n\tsrc: url(${1});\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",
"bfv": "backface-visibility:hidden|visible",
"bg": "background:${1:#000}",
"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",
"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}",
"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:grid|inline-grid|subgrid|block|none|flex|inline-flex|inline|inline-block|list-item|run-in|compact|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",
"gtc": "grid-template-columns:repeat()|minmax()",
"gtr": "grid-template-rows:repeat()|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",
"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",
"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": "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",
"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 xsl$1 = {
"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|}",
"!!!": "{}"
};
var index$7 = { html: html$1, css: css$1, xsl: xsl$1 };
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 langs = { latin, ru, sp };
const defaultOptions$5 = {
wordCount: 30,
skipCommon: false,
lang: 'latin'
};
/**
* Replaces given parsed Emmet abbreviation node with nodes filled with
* Lorem Ipsum stub text.
* @param {Node} node
* @return {Node}
*/
var index$8 = function(node, options) {
options = Object.assign({}, defaultOptions$5, options);
const dict = langs[options.lang] || langs.latin;
const startWithCommon = !options.skipCommon && !isRepeating(node);
if (!node.repeat && !isRoot$1(node.parent)) {
// non-repeating element, insert text stub as a content of parent node
// and remove current one
node.parent.value = paragraph(dict, options.wordCount, startWithCommon);
node.remove();
} else {
// Replace named node with generated content
node.value = paragraph(dict, options.wordCount, startWithCommon);
node.name = node.parent.name ? resolveImplicitName(node.parent.name) : null;
}
return node;
};
function isRoot$1(node) {
return !node.parent;
}
/**
* Returns random integer between from
and to
values
* @param {Number} from
* @param {Number} to
* @returns {Number}
*/
function rand(from, to) {
return Math.floor(Math.random() * (to - from) + from);
}
/**
* @param {Array} arr
* @param {Number} count
* @returns {Array}
*/
function sample(arr, count) {
const len = arr.length;
const iterations = Math.min(len, count);
const result = new Set();
while (result.size < iterations) {
result.add(arr[rand(0, len)]);
}
return Array.from(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
* @param {Array} words
*/
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 {Object} dict Words dictionary (see `lang/*.json`)
* @param {Number} wordCount Words count in paragraph
* @param {Boolean} startWithCommon Should paragraph start with common
* "lorem ipsum" sentence.
* @returns {String}
*/
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(' ');
}
/**
* Check if given node is in repeating context, e.g. node itself or one of its
* parent is repeated
* @param {Node} node
* @return {Boolean}
*/
function isRepeating(node) {
while (node.parent) {
if (node.repeat && node.repeat.value && node.repeat.value > 1) {
return true;
}
node = node.parent;
}
return false;
}
const reLorem = /^lorem([a-z]*)(\d*)$/i;
/**
* Constructs a snippets registry, filled with snippets, for given options
* @param {String} syntax Abbreviation syntax
* @param {Object|Object[]} snippets Additional snippets
* @return {SnippetsRegistry}
*/
function snippetsRegistryFactory(syntax, snippets) {
const registrySnippets = [index$7[syntax] || index$7.html];
if (Array.isArray(snippets)) {
snippets.forEach(item => {
// if array item is a string, treat it as a reference to globally
// defined snippets
registrySnippets.push(typeof item === 'string' ? index$7[item] : item);
});
} else if (typeof snippets === 'object') {
registrySnippets.push(snippets);
}
const registry = new SnippetsRegistry(registrySnippets.filter(Boolean));
// for non-stylesheet syntaxes add Lorem Ipsum generator
if (syntax !== 'css') {
registry.get(0).set(reLorem, loremGenerator);
}
return registry;
}
function loremGenerator(node) {
const options = {};
const m = node.name.match(reLorem);
if (m[1]) {
options.lang = m[1];
}
if (m[2]) {
options.wordCount = +m[2];
}
return index$8(node, options);
}
/**
* Default variables used in snippets to insert common values into predefined snippets
* @type {Object}
*/
const defaultVariables = {
lang: 'en',
locale: 'en-US',
charset: 'UTF-8'
};
/**
* A list of syntaxes that should use Emmet CSS abbreviations:
* a variations of default abbreivation that holds values right in abbreviation name
* @type {Set}
*/
const stylesheetSyntaxes = new Set(['css', 'sass', 'scss', 'less', 'stylus', 'sss']);
const defaultOptions$6 = {
/**
* Abbreviation output syntax
* @type {String}
*/
syntax: 'html',
/**
* Field/tabstop generator for editor. Most editors support TextMate-style
* fields: ${0} or ${1:item}. So for TextMate-style fields this function
* will look like this:
* @example
* (index, placeholder) => `\${${index}${placeholder ? ':' + placeholder : ''}}`
*
* @param {Number} index Placeholder index. Fields with the same indices
* should be linked
* @param {String} [placeholder] Field placeholder
* @return {String}
*/
field: (index, placeholder) => placeholder || '',
/**
* Insert given text string(s) into expanded abbreviation
* If array of strings is given, the implicitly repeated element (e.g. `li*`)
* will be repeated by the amount of items in array
* @type {String|String[]}
*/
text: null,
/**
* Either predefined output profile or options for output profile. Used for
* abbreviation output
* @type {Profile|Object}
*/
profile: null,
/**
* Custom variables for variable resolver
* @see @emmetio/variable-resolver
* @type {Object}
*/
variables: {},
/**
* Custom predefined snippets for abbreviation. The expanded abbreviation
* will try to match given snippets that may contain custom elements,
* predefined attributes etc.
* May also contain array of items: either snippets (Object) or references
* to default syntax snippets (String; the key in default snippets hash)
* @see @emmetio/snippets
* @type {Object|SnippetsRegistry}
*/
snippets: {},
/**
* Hash of additional transformations that should be applied to expanded
* abbreviation, like BEM or JSX. Since these transformations introduce
* side-effect, they are disabled by default and should be enabled by
* providing a transform name as a key and transform options as value:
* @example
* {
* bem: {element: '--'},
* jsx: true // no options, just enable transform
* }
* @see @emmetio/html-transform/lib/addons
* @type {Object}
*/
addons: null,
/**
* Additional options for syntax formatter
* @see @emmetio/markup-formatters
* @type {Object}
*/
format: null
};
/**
* Expands given abbreviation into string, formatted according to provided
* syntax and options
* @param {String|Node} abbr Abbreviation string or parsed abbreviation tree
* @param {String|Object} [options] Parsing and formatting options (object) or
* abbreviation syntax (string)
* @return {String}
*/
function expand$2(abbr, options) {
options = createOptions(options);
return isStylesheet(options.syntax)
? expand$1(abbr, options)
: expand(abbr, options);
}
/**
* Parses given abbreviation into AST tree. This tree can be later formatted to
* string with `expand` function
* @param {String} abbr Abbreviation to parse
* @param {String|Object} [options] Parsing and formatting options (object) or
* abbreviation syntax (string)
* @return {Node}
*/
function parse$5(abbr, options) {
options = createOptions(options);
return isStylesheet(options.syntax)
? parse$4(abbr, options)
: parse$3(abbr, options);
}
/**
* Creates snippets registry for given syntax and additional `snippets`
* @param {String} syntax Snippets syntax, used for retreiving predefined snippets
* @param {SnippetsRegistry|Object|Object[]} [snippets] Additional snippets
* @return {SnippetsRegistry}
*/
function createSnippetsRegistry(syntax, snippets) {
return snippets instanceof SnippetsRegistry
? snippets
: snippetsRegistryFactory(isStylesheet(syntax) ? 'css' : syntax, snippets);
}
function createOptions(options) {
if (typeof options === 'string') {
options = { syntax: options };
}
options = Object.assign({}, defaultOptions$6, options);
options.format = Object.assign({field: options.field}, options.format);
options.profile = createProfile(options);
options.variables = Object.assign({}, defaultVariables, options.variables);
options.snippets = createSnippetsRegistry(isStylesheet(options.syntax) ? 'css' : options.syntax, options.snippets);
return options;
}
/**
* Check if given syntax belongs to stylesheet markup.
* Emmet uses different abbreviation flavours: one is a default markup syntax,
* used for HTML, Slim, Pug etc, the other one is used for stylesheets and
* allows embedded values in abbreviation name
* @param {String} syntax
* @return {Boolean}
*/
function isStylesheet(syntax) {
return stylesheetSyntaxes.has(syntax);
}
/**
* Creates output profile from given options
* @param {Object} options
* @return {Profile}
*/
function createProfile(options) {
return options.profile instanceof Profile
? options.profile
: new Profile(options.profile);
}
exports.expand = expand$2;
exports.parse = parse$5;
exports.createSnippetsRegistry = createSnippetsRegistry;
exports.createOptions = createOptions;
exports.isStylesheet = isStylesheet;
exports.createProfile = createProfile;
Object.defineProperty(exports, '__esModule', { value: true });
})));