|
- (function (global, factory) {
- typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
- typeof define === 'function' && define.amd ? define(['exports'], factory) :
- (factory((global.emmet = {})));
- }(this, (function (exports) { 'use strict';
-
- var defaultOptions = {
- /**
- * String for one-level indentation
- * @type {String}
- */
- indent: '\t',
-
- /**
- * Tag case: 'lower', 'upper' or '' (keep as-is)
- * @type {String}
- */
- tagCase: '',
-
- /**
- * Attribute name case: 'lower', 'upper' or '' (keep as-is)
- * @type {String}
- */
- attributeCase: '',
-
- /**
- * Attribute value quotes: 'single' or 'double'
- * @type {String}
- */
- attributeQuotes: 'double',
-
- /**
- * Enable output formatting (indentation and line breaks)
- * @type {Boolean}
- */
- format: true,
-
- /**
- * A list of tag names that should not get inner indentation
- * @type {Set}
- */
- formatSkip: ['html'],
-
- /**
- * A list of tag names that should *always* get inner indentation.
- * @type {Set}
- */
- formatForce: ['body'],
-
- /**
- * How many inline sibling elements should force line break for each tag.
- * Set to 0 to output all inline elements without formatting.
- * Set to 1 to output all inline elements with formatting (same as block-level).
- * @type {Number}
- */
- inlineBreak: 3,
-
- /**
- * Produce compact notation of boolean attribues: attributes where name equals value.
- * With this option enabled, output `<div contenteditable>` instead of
- * `<div contenteditable="contenteditable">`
- * @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' – <br>
- * 'xml' – <br/>
- * 'xhtml' – <br />
- * @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 <code>undefined</code> 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 <code>eat</code> with the given argument, until it
- * fails. Returns <code>true</code> 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 `<a>` 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>]', {NAME: 'foo'}) -> "<foo>"
- * template('[<NAME>]', {}) -> ""
- * ```
- */
- 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<!-- /[#ID][.CLASS] -->'
- };
-
- /**
- * 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",
-
- "!!!": "{<!DOCTYPE html>}",
- "doc": "html[lang=${lang}]>(head>meta[charset=${charset}]+meta:vp+title{${1:Document}})+body",
- "!|html:5": "!!!+doc",
-
- "c": "{<!-- ${0} -->}",
- "cc:ie": "{<!--[if IE]>${0}<![endif]-->}",
- "cc:noie": "{<!--[if !IE]><!-->${0}<!--<![endif]-->}"
- };
-
- var 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|}",
- "!!!": "{<?xml version=\"1.0\" encoding=\"UTF-8\"?>}"
- };
-
- 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 <code>from</code> and <code>to</code> 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 <code>words</code> 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 });
-
- })));
|