(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 `
` instead of * `
` * @type {Boolean} */ compactBooleanAttributes: false, /** * A set of boolean attributes * @type {Set} */ booleanAttributes: ['contenteditable', 'seamless', 'async', 'autofocus', 'autoplay', 'checked', 'controls', 'defer', 'disabled', 'formnovalidate', 'hidden', 'ismap', 'loop', 'multiple', 'muted', 'novalidate', 'readonly', 'required', 'reversed', 'selected', 'typemustmatch'], /** * Style of self-closing tags: * 'html' –
* 'xml' –
* 'xhtml' –
* @type {String} */ selfClosingStyle: 'html', /** * A set of inline-level elements * @type {Set} */ inlineElements: ['a', 'abbr', 'acronym', 'applet', 'b', 'basefont', 'bdo', 'big', 'br', 'button', 'cite', 'code', 'del', 'dfn', 'em', 'font', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'map', 'object', 'q', 's', 'samp', 'select', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'textarea', 'tt', 'u', 'var'] }; /** * Creates output profile for given options (@see defaults) * @param {defaults} options */ class Profile { constructor(options) { this.options = Object.assign({}, defaultOptions, options); this.quoteChar = this.options.attributeQuotes === 'single' ? '\'' : '"'; } /** * Returns value of given option name * @param {String} name * @return {*} */ get(name) { return this.options[name]; } /** * Quote given string according to profile * @param {String} str String to quote * @return {String} */ quote(str) { return `${this.quoteChar}${str != null ? str : ''}${this.quoteChar}`; } /** * Output given tag name accoding to options * @param {String} name * @return {String} */ name(name) { return strcase(name, this.options.tagCase); } /** * Outputs attribute name accoding to current settings * @param {String} Attribute name * @return {String} */ attribute(attr) { return strcase(attr, this.options.attributeCase); } /** * Check if given attribute is boolean * @param {Attribute} attr * @return {Boolean} */ isBooleanAttribute(attr) { return attr.options.boolean || this.get('booleanAttributes').indexOf((attr.name || '').toLowerCase()) !== -1; } /** * Returns a token for self-closing tag, depending on current options * @return {String} */ selfClose() { switch (this.options.selfClosingStyle) { case 'xhtml': return ' /'; case 'xml': return '/'; default: return ''; } } /** * Returns indent for given level * @param {Number} level Indentation level * @return {String} */ indent(level) { level = level || 0; let output = ''; while (level--) { output += this.options.indent; } return output; } /** * Check if given tag name belongs to inline-level element * @param {Node|String} node Parsed node or tag name * @return {Boolean} */ isInline(node) { if (typeof node === 'string') { return this.get('inlineElements').indexOf(node.toLowerCase()) !== -1; } // inline node is a node either with inline-level name or text-only node return node.name != null ? this.isInline(node.name) : node.isTextOnly; } /** * Outputs formatted field for given params * @param {Number} index Field index * @param {String} [placeholder] Field placeholder, can be empty * @return {String} */ field(index, placeholder) { return this.options.field(index, placeholder); } } function strcase(string, type) { if (type) { string = type === 'upper' ? string.toUpperCase() : string.toLowerCase(); } return string; } class Snippet { constructor(key, value) { this.key = key; this.value = value; } } class SnippetsStorage { constructor(data) { this._string = new Map(); this._regexp = new Map(); this._disabled = false; this.load(data); } get disabled() { return this._disabled; } /** * Disables current store. A disabled store always returns `undefined` * on `get()` method */ disable() { this._disabled = true; } /** * Enables current store. */ enable() { this._disabled = false; } /** * Registers a new snippet item * @param {String|Regexp} key * @param {String|Function} value */ set(key, value) { if (typeof key === 'string') { key.split('|').forEach(k => this._string.set(k, new Snippet(k, value))); } else if (key instanceof RegExp) { this._regexp.set(key, new Snippet(key, value)); } else { throw new Error('Unknow snippet key: ' + key); } return this; } /** * Returns a snippet matching given key. It first tries to find snippet * exact match in a string key map, then tries to match one with regexp key * @param {String} key * @return {Snippet} */ get(key) { if (this.disabled) { return undefined; } if (this._string.has(key)) { return this._string.get(key); } const keys = Array.from(this._regexp.keys()); for (let i = 0, il = keys.length; i < il; i++) { if (keys[i].test(key)) { return this._regexp.get(keys[i]); } } } /** * Batch load of snippets data * @param {Object|Map} data */ load(data) { this.reset(); if (data instanceof Map) { data.forEach((value, key) => this.set(key, value)); } else if (data && typeof data === 'object') { Object.keys(data).forEach(key => this.set(key, data[key])); } } /** * Clears all stored snippets */ reset() { this._string.clear(); this._regexp.clear(); } /** * Returns all available snippets from given store */ values() { if (this.disabled) { return []; } const string = Array.from(this._string.values()); const regexp = Array.from(this._regexp.values()); return string.concat(regexp); } } /** * A snippets registry. Contains snippets, separated by store and sorted by * priority: a store with higher priority takes precedence when resolving snippet * for given key */ class SnippetsRegistry { /** * Creates snippets registry, filled with given `data` * @param {Object|Array} data Registry snippets. If array is given, adds items * from array in order of precedence, registers global snippets otherwise */ constructor(data) { this._registry = []; if (Array.isArray(data)) { data.forEach((snippets, level) => this.add(level, snippets)); } else if (typeof data === 'object') { this.add(data); } } /** * Return store for given level * @param {Number} level * @return {SnippetsStorage} */ get(level) { for (let i = 0; i < this._registry.length; i++) { const item = this._registry[i]; if (item.level === level) { return item.store; } } } /** * Adds new store for given level * @param {Number} [level] Store level (priority). Store with higher level * takes precedence when resolving snippets * @param {Object} [snippets] A snippets data for new store * @return {SnipetsStorage} */ add(level, snippets) { if (level != null && typeof level === 'object') { snippets = level; level = 0; } const store = new SnippetsStorage(snippets); // remove previous store from same level this.remove(level); this._registry.push({level, store}); this._registry.sort((a, b) => b.level - a.level); return store; } /** * Remove registry with given level or store * @param {Number|SnippetsStorage} data Either level or snippets store */ remove(data) { this._registry = this._registry .filter(item => item.level !== data && item.store !== data); } /** * Returns snippet from registry that matches given name * @param {String} name * @return {Snippet} */ resolve(name) { for (let i = 0; i < this._registry.length; i++) { const snippet = this._registry[i].store.get(name); if (snippet) { return snippet; } } } /** * Returns all available snippets from current registry. Snippets with the * same key are resolved by their storage priority. * @param {Object} options * @param {Object} options.type Return snippets only of given type: 'string' * or 'regexp'. Returns all snippets if not defined * @return {Array} */ all(options) { options = options || {}; const result = new Map(); const fillResult = snippet => { const type = snippet.key instanceof RegExp ? 'regexp' : 'string'; if ((!options.type || options.type === type) && !result.has(snippet.key)) { result.set(snippet.key, snippet); } }; this._registry.forEach(item => { item.store.values().forEach(fillResult); }); return Array.from(result.values()); } /** * Removes all stores from registry */ clear() { this._registry.length = 0; } } /** * Methods for consuming quoted values */ const SINGLE_QUOTE = 39; // ' const DOUBLE_QUOTE = 34; // " const defaultOptions$1 = { escape: 92, // \ character throws: false }; /** * Consumes 'single' or "double"-quoted string from given string, if possible * @param {StreamReader} stream * @param {Number} options.escape A character code of quote-escape symbol * @param {Boolean} options.throws Throw error if quotes string can’t be properly consumed * @return {Boolean} `true` if quoted string was consumed. The contents * of quoted string will be availabe as `stream.current()` */ var eatQuoted = function(stream, options) { options = options ? Object.assign({}, defaultOptions$1, options) : defaultOptions$1; const start = stream.pos; const quote = stream.peek(); if (stream.eat(isQuote)) { while (!stream.eof()) { switch (stream.next()) { case quote: stream.start = start; return true; case options.escape: stream.next(); break; } } // If we’re here then stream wasn’t properly consumed. // Revert stream and decide what to do stream.pos = start; if (options.throws) { throw stream.error('Unable to consume quoted string'); } } return false; }; function isQuote(code) { return code === SINGLE_QUOTE || code === DOUBLE_QUOTE; } /** * Check if given code is a number * @param {Number} code * @return {Boolean} */ function isNumber(code) { return code > 47 && code < 58; } /** * Check if given character code is alpha code (letter through A to Z) * @param {Number} code * @param {Number} [from] * @param {Number} [to] * @return {Boolean} */ function isAlpha(code, from, to) { from = from || 65; // A to = to || 90; // Z code &= ~32; // quick hack to convert any char code to uppercase char code return code >= from && code <= to; } /** * Check if given character code is alpha-numeric (letter through A to Z or number) * @param {Number} code * @return {Boolean} */ function isAlphaNumeric(code) { return isNumber(code) || isAlpha(code); } function isWhiteSpace(code) { return code === 32 /* space */ || code === 9 /* tab */ || code === 160; /* non-breaking space */ } /** * Check if given character code is a space * @param {Number} code * @return {Boolean} */ function isSpace(code) { return isWhiteSpace(code) || code === 10 /* LF */ || code === 13; /* CR */ } /** * Attribute descriptor of parsed abbreviation node * @param {String} name Attribute name * @param {String} value Attribute value * @param {Object} options Additional custom attribute options * @param {Boolean} options.boolean Attribute is boolean (e.g. name equals value) * @param {Boolean} options.implied Attribute is implied (e.g. must be outputted * only if contains non-null value) */ class Attribute { constructor(name, value, options) { this.name = name; this.value = value != null ? value : null; this.options = options || {}; } /** * Create a copy of current attribute * @return {Attribute} */ clone() { return new Attribute(this.name, this.value, Object.assign({}, this.options)); } /** * A string representation of current node */ valueOf() { return `${this.name}="${this.value}"`; } } /** * A parsed abbreviation AST node. Nodes build up an abbreviation AST tree */ class Node { /** * Creates a new node * @param {String} [name] Node name * @param {Array} [attributes] Array of attributes to add */ constructor(name, attributes) { // own properties this.name = name || null; this.value = null; this.repeat = null; this.selfClosing = false; this.children = []; /** @type {Node} Pointer to parent node */ this.parent = null; /** @type {Node} Pointer to next sibling */ this.next = null; /** @type {Node} Pointer to previous sibling */ this.previous = null; this._attributes = []; if (Array.isArray(attributes)) { attributes.forEach(attr => this.setAttribute(attr)); } } /** * Array of current node attributes * @return {Attribute[]} Array of attributes */ get attributes() { return this._attributes; } /** * A shorthand to retreive node attributes as map * @return {Object} */ get attributesMap() { return this.attributes.reduce((out, attr) => { out[attr.name] = attr.options.boolean ? attr.name : attr.value; return out; }, {}); } /** * Check if current node is a grouping one, e.g. has no actual representation * and is used for grouping subsequent nodes only * @return {Boolean} */ get isGroup() { return !this.name && !this.value && !this._attributes.length; } /** * Check if given node is a text-only node, e.g. contains only value * @return {Boolean} */ get isTextOnly() { return !this.name && !!this.value && !this._attributes.length; } /** * Returns first child node * @return {Node} */ get firstChild() { return this.children[0]; } /** * Returns last child of current node * @return {Node} */ get lastChild() { return this.children[this.children.length - 1]; } /** * Return index of current node in its parent child list * @return {Number} Returns -1 if current node is a root one */ get childIndex() { return this.parent ? this.parent.children.indexOf(this) : -1; } /** * Returns next sibling of current node * @return {Node} */ get nextSibling() { return this.next; } /** * Returns previous sibling of current node * @return {Node} */ get previousSibling() { return this.previous; } /** * Returns array of unique class names in current node * @return {String[]} */ get classList() { const attr = this.getAttribute('class'); return attr && attr.value ? attr.value.split(/\s+/g).filter(uniqueClass) : []; } /** * Convenient alias to create a new node instance * @param {String} [name] Node name * @param {Object} [attributes] Attributes hash * @return {Node} */ create(name, attributes) { return new Node(name, attributes); } /** * Sets given attribute for current node * @param {String|Object|Attribute} name Attribute name or attribute object * @param {String} [value] Attribute value */ setAttribute(name, value) { const attr = createAttribute(name, value); const curAttr = this.getAttribute(name); if (curAttr) { this.replaceAttribute(curAttr, attr); } else { this._attributes.push(attr); } } /** * Check if attribute with given name exists in node * @param {String} name * @return {Boolean} */ hasAttribute(name) { return !!this.getAttribute(name); } /** * Returns attribute object by given name * @param {String} name * @return {Attribute} */ getAttribute(name) { if (typeof name === 'object') { name = name.name; } for (var i = 0; i < this._attributes.length; i++) { const attr = this._attributes[i]; if (attr.name === name) { return attr; } } } /** * Replaces attribute with new instance * @param {String|Attribute} curAttribute Current attribute name or instance * to replace * @param {String|Object|Attribute} newName New attribute name or attribute object * @param {String} [newValue] New attribute value */ replaceAttribute(curAttribute, newName, newValue) { if (typeof curAttribute === 'string') { curAttribute = this.getAttribute(curAttribute); } const ix = this._attributes.indexOf(curAttribute); if (ix !== -1) { this._attributes.splice(ix, 1, createAttribute(newName, newValue)); } } /** * Removes attribute with given name * @param {String|Attribute} attr Atrtibute name or instance */ removeAttribute(attr) { if (typeof attr === 'string') { attr = this.getAttribute(attr); } const ix = this._attributes.indexOf(attr); if (ix !== -1) { this._attributes.splice(ix, 1); } } /** * Removes all attributes from current node */ clearAttributes() { this._attributes.length = 0; } /** * Adds given class name to class attribute * @param {String} token Class name token */ addClass(token) { token = normalize(token); if (!this.hasAttribute('class')) { this.setAttribute('class', token); } else if (token && !this.hasClass(token)) { this.setAttribute('class', this.classList.concat(token).join(' ')); } } /** * Check if current node contains given class name * @param {String} token Class name token * @return {Boolean} */ hasClass(token) { return this.classList.indexOf(normalize(token)) !== -1; } /** * Removes given class name from class attribute * @param {String} token Class name token */ removeClass(token) { token = normalize(token); if (this.hasClass(token)) { this.setAttribute('class', this.classList.filter(name => name !== token).join(' ')); } } /** * Appends child to current node * @param {Node} node */ appendChild(node) { this.insertAt(node, this.children.length); } /** * Inserts given `newNode` before `refNode` child node * @param {Node} newNode * @param {Node} refNode */ insertBefore(newNode, refNode) { this.insertAt(newNode, this.children.indexOf(refNode)); } /** * Insert given `node` at `pos` position of child list * @param {Node} node * @param {Number} pos */ insertAt(node, pos) { if (pos < 0 || pos > this.children.length) { throw new Error('Unable to insert node: position is out of child list range'); } const prev = this.children[pos - 1]; const next = this.children[pos]; node.remove(); node.parent = this; this.children.splice(pos, 0, node); if (prev) { node.previous = prev; prev.next = node; } if (next) { node.next = next; next.previous = node; } } /** * Removes given child from current node * @param {Node} node */ removeChild(node) { const ix = this.children.indexOf(node); if (ix !== -1) { this.children.splice(ix, 1); if (node.previous) { node.previous.next = node.next; } if (node.next) { node.next.previous = node.previous; } node.parent = node.next = node.previous = null; } } /** * Removes current node from its parent */ remove() { if (this.parent) { this.parent.removeChild(this); } } /** * Creates a detached copy of current node * @param {Boolean} deep Clone node contents as well * @return {Node} */ clone(deep) { const clone = new Node(this.name); clone.value = this.value; clone.selfClosing = this.selfClosing; if (this.repeat) { clone.repeat = Object.assign({}, this.repeat); } this._attributes.forEach(attr => clone.setAttribute(attr.clone())); if (deep) { this.children.forEach(child => clone.appendChild(child.clone(true))); } return clone; } /** * Walks on each descendant node and invokes given `fn` function on it. * The function receives two arguments: the node itself and its depth level * from current node. If function returns `false`, it stops walking * @param {Function} fn */ walk(fn, _level) { _level = _level || 0; let ctx = this.firstChild; while (ctx) { // in case if context node will be detached during `fn` call const next = ctx.next; if (fn(ctx, _level) === false || ctx.walk(fn, _level + 1) === false) { return false; } ctx = next; } } /** * A helper method for transformation chaining: runs given `fn` function on * current node and returns the same node */ use(fn) { const args = [this]; for (var i = 1; i < arguments.length; i++) { args.push(arguments[i]); } fn.apply(null, args); return this; } toString() { const attrs = this.attributes.map(attr => { attr = this.getAttribute(attr.name); const opt = attr.options; let out = `${opt && opt.implied ? '!' : ''}${attr.name || ''}`; if (opt && opt.boolean) { out += '.'; } else if (attr.value != null) { out += `="${attr.value}"`; } return out; }); let out = `${this.name || ''}`; if (attrs.length) { out += `[${attrs.join(' ')}]`; } if (this.value != null) { out += `{${this.value}}`; } if (this.selfClosing) { out += '/'; } if (this.repeat) { out += `*${this.repeat.count ? this.repeat.count : ''}`; if (this.repeat.value != null) { out += `@${this.repeat.value}`; } } return out; } } /** * Attribute factory * @param {String|Attribute|Object} name Attribute name or attribute descriptor * @param {*} value Attribute value * @return {Attribute} */ function createAttribute(name, value) { if (name instanceof Attribute) { return name; } if (typeof name === 'string') { return new Attribute(name, value); } if (name && typeof name === 'object') { return new Attribute(name.name, name.value, name.options); } } /** * @param {String} str * @return {String} */ function normalize(str) { return String(str).trim(); } function uniqueClass(item, i, arr) { return item && arr.indexOf(item) === i; } /** * A streaming, character code-based string reader */ class StreamReader { constructor(string, start, end) { if (end == null && typeof string === 'string') { end = string.length; } this.string = string; this.pos = this.start = start || 0; this.end = end; } /** * Returns true only if the stream is at the end of the file. * @returns {Boolean} */ eof() { return this.pos >= this.end; } /** * Creates a new stream instance which is limited to given `start` and `end` * range. E.g. its `eof()` method will look at `end` property, not actual * stream end * @param {Point} start * @param {Point} end * @return {StreamReader} */ limit(start, end) { return new this.constructor(this.string, start, end); } /** * Returns the next character code in the stream without advancing it. * Will return NaN at the end of the file. * @returns {Number} */ peek() { return this.string.charCodeAt(this.pos); } /** * Returns the next character in the stream and advances it. * Also returns undefined when no more characters are available. * @returns {Number} */ next() { if (this.pos < this.string.length) { return this.string.charCodeAt(this.pos++); } } /** * `match` can be a character code or a function that takes a character code * and returns a boolean. If the next character in the stream 'matches' * the given argument, it is consumed and returned. * Otherwise, `false` is returned. * @param {Number|Function} match * @returns {Boolean} */ eat(match) { const ch = this.peek(); const ok = typeof match === 'function' ? match(ch) : ch === match; if (ok) { this.next(); } return ok; } /** * Repeatedly calls eat with the given argument, until it * fails. Returns true if any characters were eaten. * @param {Object} match * @returns {Boolean} */ eatWhile(match) { const start = this.pos; while (!this.eof() && this.eat(match)) {} return this.pos !== start; } /** * Backs up the stream n characters. Backing it up further than the * start of the current token will cause things to break, so be careful. * @param {Number} n */ backUp(n) { this.pos -= (n || 1); } /** * Get the string between the start of the current token and the * current stream position. * @returns {String} */ current() { return this.substring(this.start, this.pos); } /** * Returns substring for given range * @param {Number} start * @param {Number} [end] * @return {String} */ substring(start, end) { return this.string.slice(start, end); } /** * Creates error object with current stream state * @param {String} message * @return {Error} */ error(message) { const err = new Error(`${message} at char ${this.pos + 1}`); err.originalMessage = message; err.pos = this.pos; err.string = this.string; return err; } } const ASTERISK = 42; // * /** * Consumes node repeat token from current stream position and returns its * parsed value * @param {StringReader} stream * @return {Object} */ function consumeRepeat(stream) { if (stream.eat(ASTERISK)) { stream.start = stream.pos; // XXX think about extending repeat syntax with through numbering return { count: stream.eatWhile(isNumber) ? +stream.current() : null }; } } const opt = { throws: true }; /** * Consumes quoted literal from current stream position and returns it’s inner, * unquoted, value * @param {StringReader} stream * @return {String} Returns `null` if unable to consume quoted value from current * position */ function consumeQuoted(stream) { if (eatQuoted(stream, opt)) { return stream.current().slice(1, -1); } } const TEXT_START = 123; // { const TEXT_END = 125; // } const ESCAPE = 92; // \ character /** * Consumes text node `{...}` from stream * @param {StreamReader} stream * @return {String} Returns consumed text value (without surrounding braces) or * `null` if there’s no text at starting position */ function consumeText(stream) { // NB using own implementation instead of `eatPair()` from @emmetio/stream-reader-utils // to disable quoted value consuming const start = stream.pos; if (stream.eat(TEXT_START)) { let stack = 1, ch; let result = ''; let offset = stream.pos; while (!stream.eof()) { ch = stream.next(); if (ch === TEXT_START) { stack++; } else if (ch === TEXT_END) { stack--; if (!stack) { stream.start = start; return result + stream.substring(offset, stream.pos - 1); } } else if (ch === ESCAPE) { ch = stream.next(); if (ch === TEXT_START || ch === TEXT_END) { result += stream.substring(offset, stream.pos - 2) + String.fromCharCode(ch); offset = stream.pos; } } } // If we’re here then paired character can’t be consumed stream.pos = start; throw stream.error(`Unable to find closing ${String.fromCharCode(TEXT_END)} for text start`); } return null; } const EXCL = 33; // . const DOT = 46; // . const EQUALS = 61; // = const ATTR_OPEN = 91; // [ const ATTR_CLOSE = 93; // ] const reAttributeName = /^\!?[\w\-:\$@]+\.?$|^\!?\[[\w\-:\$@]+\]\.?$/; /** * Consumes attributes defined in square braces from given stream. * Example: * [attr col=3 title="Quoted string" selected. support={react}] * @param {StringReader} stream * @returns {Array} Array of consumed attributes */ function consumeAttributes(stream) { if (!stream.eat(ATTR_OPEN)) { return null; } const result = []; let token, attr; while (!stream.eof()) { stream.eatWhile(isWhiteSpace); if (stream.eat(ATTR_CLOSE)) { return result; // End of attribute set } else if ((token = consumeQuoted(stream)) != null) { // Consumed quoted value: anonymous attribute result.push({ name: null, value: token }); } else if (eatUnquoted(stream)) { // Consumed next word: could be either attribute name or unquoted default value token = stream.current(); // In angular attribute names can be surrounded by [] if (token[0] === '[' && stream.peek() === ATTR_CLOSE) { stream.next(); token = stream.current(); } if (!reAttributeName.test(token)) { // anonymous attribute result.push({ name: null, value: token }); } else { // Looks like a regular attribute attr = parseAttributeName(token); result.push(attr); if (stream.eat(EQUALS)) { // Explicitly defined value. Could be a word, a quoted string // or React-like expression if ((token = consumeQuoted(stream)) != null) { attr.value = token; } else if ((token = consumeText(stream)) != null) { attr.value = token; attr.options = { before: '{', after: '}' }; } else if (eatUnquoted(stream)) { attr.value = stream.current(); } } } } else { throw stream.error('Expected attribute name'); } } throw stream.error('Expected closing "]" brace'); } function parseAttributeName(name) { const options = {}; // If a first character in attribute name is `!` — it’s an implied // default attribute if (name.charCodeAt(0) === EXCL) { name = name.slice(1); options.implied = true; } // Check for last character: if it’s a `.`, user wants boolean attribute if (name.charCodeAt(name.length - 1) === DOT) { name = name.slice(0, name.length - 1); options.boolean = true; } const attr = { name }; if (Object.keys(options).length) { attr.options = options; } return attr; } /** * Eats token that can be an unquoted value from given stream * @param {StreamReader} stream * @return {Boolean} */ function eatUnquoted(stream) { const start = stream.pos; if (stream.eatWhile(isUnquoted)) { stream.start = start; return true; } } function isUnquoted(code) { return !isSpace(code) && !isQuote(code) && code !== ATTR_CLOSE && code !== EQUALS; } const HASH = 35; // # const DOT$1 = 46; // . const SLASH = 47; // / /** * Consumes a single element node from current abbreviation stream * @param {StringReader} stream * @return {Node} */ function consumeElement(stream) { // consume element name, if provided const start = stream.pos; const node = new Node(eatName(stream)); let next; while (!stream.eof()) { if (stream.eat(DOT$1)) { node.addClass(eatName(stream)); } else if (stream.eat(HASH)) { node.setAttribute('id', eatName(stream)); } else if (stream.eat(SLASH)) { // A self-closing indicator must be at the end of non-grouping node if (node.isGroup) { stream.backUp(1); throw stream.error('Unexpected self-closing indicator'); } node.selfClosing = true; if (next = consumeRepeat(stream)) { node.repeat = next; } break; } else if (next = consumeAttributes(stream)) { for (let i = 0, il = next.length; i < il; i++) { node.setAttribute(next[i]); } } else if ((next = consumeText(stream)) !== null) { node.value = next; } else if (next = consumeRepeat(stream)) { node.repeat = next; } else { break; } } if (start === stream.pos) { throw stream.error(`Unable to consume abbreviation node, unexpected ${stream.peek()}`); } return node; } function eatName(stream) { stream.start = stream.pos; stream.eatWhile(isName); return stream.current(); } function isName(code) { return isAlphaNumeric(code) || code === 45 /* - */ || code === 58 /* : */ || code === 36 /* $ */ || code === 64 /* @ */ || code === 33 /* ! */ || code === 95 /* _ */ || code === 37 /* % */; } const GROUP_START = 40; // ( const GROUP_END = 41; // ) const OP_SIBLING = 43; // + const OP_CHILD = 62; // > const OP_CLIMB = 94; // ^ /** * Parses given string into a node tree * @param {String} str Abbreviation to parse * @return {Node} */ function parse(str) { const stream = new StreamReader(str.trim()); const root = new Node(); let ctx = root, groupStack = [], ch; while (!stream.eof()) { ch = stream.peek(); if (ch === GROUP_START) { // start of group // The grouping node should be detached to properly handle // out-of-bounds `^` operator. Node will be attached right on group end const node = new Node(); groupStack.push([node, ctx, stream.pos]); ctx = node; stream.next(); continue; } else if (ch === GROUP_END) { // end of group const lastGroup = groupStack.pop(); if (!lastGroup) { throw stream.error('Unexpected ")" group end'); } const node = lastGroup[0]; ctx = lastGroup[1]; stream.next(); // a group can have a repeater if (node.repeat = consumeRepeat(stream)) { ctx.appendChild(node); } else { // move all children of group into parent node while (node.firstChild) { ctx.appendChild(node.firstChild); } } // for convenience, groups can be joined with optional `+` operator stream.eat(OP_SIBLING); continue; } const node = consumeElement(stream); ctx.appendChild(node); if (stream.eof()) { break; } switch (stream.peek()) { case OP_SIBLING: stream.next(); continue; case OP_CHILD: stream.next(); ctx = node; continue; case OP_CLIMB: // it’s perfectly valid to have multiple `^` operators while (stream.eat(OP_CLIMB)) { ctx = ctx.parent || ctx; } continue; } } if (groupStack.length) { stream.pos = groupStack.pop()[2]; throw stream.error('Expected group close'); } return root; } /** * Parses given abbreviation and un-rolls it into a full tree: recursively * replaces repeated elements with actual nodes * @param {String} abbr * @return {Node} */ function index(abbr) { const tree = parse(abbr); tree.walk(unroll); return tree; } function unroll(node) { if (!node.repeat || !node.repeat.count) { return; } const parent = node.parent; let ix = parent.children.indexOf(node); for (let i = 0; i < node.repeat.count; i++) { const clone = node.clone(true); clone.repeat.value = i + 1; clone.walk(unroll); if (clone.isGroup) { while (clone.children.length > 0) { clone.firstChild.repeat = clone.repeat; parent.insertAt(clone.firstChild, ix++); } } else { parent.insertAt(clone, ix++); } } node.parent.removeChild(node); } /** * For every node in given `tree`, finds matching snippet from `registry` and * resolves it into a parsed abbreviation. Resolved node is then updated or * replaced with matched abbreviation tree. * * A HTML registry basically contains aliases to another Emmet abbreviations, * e.g. a predefined set of name, attribues and so on, possibly a complex * abbreviation with multiple elements. So we have to get snippet, parse it * and recursively resolve it. * * @param {Node} tree Parsed Emmet abbreviation * @param {SnippetsRegistry} registry Registry with all available snippets * @return {Node} Updated tree */ var index$1 = function(tree, registry) { tree.walk(node => resolveNode(node, registry)); return tree; }; function resolveNode(node, registry) { const stack = new Set(); const resolve = node => { const snippet = registry.resolve(node.name); // A snippet in stack means circular reference. // It can be either a user error or a perfectly valid snippet like // "img": "img[src alt]/", e.g. an element with predefined shape. // In any case, simply stop parsing and keep element as is if (!snippet || stack.has(snippet)) { return; } // In case if matched snippet is a function, pass control into it if (typeof snippet.value === 'function') { return snippet.value(node, registry, resolve); } const tree = index(snippet.value); stack.add(snippet); tree.walk(resolve); stack.delete(snippet); // move current node contents into new tree const childTarget = findDeepestNode(tree); merge(childTarget, node); while (tree.firstChild) { node.parent.insertBefore(tree.firstChild, node); } childTarget.parent.insertBefore(node, childTarget); childTarget.remove(); }; resolve(node); } /** * Adds data from first node into second node and returns it * @param {Node} from * @param {Node} to * @return {Node} */ function merge(from, to) { to.name = from.name; if (from.selfClosing) { to.selfClosing = true; } if (from.value != null) { to.value = from.value; } if (from.repeat) { to.repeat = Object.assign({}, from.repeat); } return mergeAttributes(from, to); } /** * Transfer attributes from first element to second one and preserve first * element’s attributes order * @param {Node} from * @param {Node} to * @return {Node} */ function mergeAttributes(from, to) { mergeClassNames(from, to); // It’s important to preserve attributes order: ones in `from` have higher // pripority than in `to`. Collect attributes in map in order they should // appear in `to` const attrMap = new Map(); let attrs = from.attributes; for (let i = 0; i < attrs.length; i++) { attrMap.set(attrs[i].name, attrs[i].clone()); } attrs = to.attributes.slice(); for (let i = 0, attr, a; i < attrs.length; i++) { attr = attrs[i]; if (attrMap.has(attr.name)) { a = attrMap.get(attr.name); a.value = attr.value; // If user explicitly wrote attribute in abbreviation, it’s no longer // implied and should be outputted even if value is empty if (a.options.implied) { a.options.implied = false; } } else { attrMap.set(attr.name, attr); } to.removeAttribute(attr); } const newAttrs = Array.from(attrMap.values()); for (let i = 0; i < newAttrs.length; i++) { to.setAttribute(newAttrs[i]); } return to; } /** * Adds class names from first node to second one * @param {Node} from * @param {Node} to * @return {Node} */ function mergeClassNames(from, to) { const classNames = from.classList; for (let i = 0; i < classNames.length; i++) { to.addClass(classNames[i]); } return to; } /** * Finds node which is the deepest for in current node or node iteself. * @param {Node} node * @return {Node} */ function findDeepestNode(node) { while (node.children.length) { node = node.children[node.children.length - 1]; } return node; } const inlineElements = new Set('a,abbr,acronym,applet,b,basefont,bdo,big,br,button,cite,code,del,dfn,em,font,i,iframe,img,input,ins,kbd,label,map,object,q,s,samp,select,small,span,strike,strong,sub,sup,textarea,tt,u,var'.split(',')); const elementMap = { p: 'span', ul: 'li', ol: 'li', table: 'tr', tr: 'td', tbody: 'tr', thead: 'tr', tfoot: 'tr', colgroup: 'col', select: 'option', optgroup: 'option', audio: 'source', video: 'source', object: 'param', map: 'area' }; /** * Returns best child node name for given parent node name * @param {String} parentName Name of parent node * @return {String} */ function resolveImplicitName(parentName) { parentName = (parentName || '').toLowerCase(); return elementMap[parentName] || (inlineElements.has(parentName) ? 'span' : 'div'); } /** * Adds missing tag names for given tree depending on node’s parent name */ var implicitTags = function(tree) { tree.walk(node => { // resolve only nameless nodes without content if (node.name == null && node.attributes.length) { node.name = resolveImplicitName(node.parent.name); } }); return tree; }; /** * Locates all occurances of given `token` which are not escaped (e.g. are not * preceded with `\`) given in `str` * @param {String} str * @return {Array} Array of token ranges */ function findUnescapedTokens(str, token) { const result = new Set(); const tlen = token.length; // 1. Find all occurances of tokens let pos = 0; while ((pos = str.indexOf(token, pos)) !== -1) { result.add(pos); pos += tlen; } if (result.size) { // 2. Remove ones that escaped let pos = 0; const len = str.length; while (pos < len) { if (str[pos++] === '\\') { result.delete(pos++); } } } return Array.from(result).map(ix => range(ix, tlen)); } /** * Replaces `ranges`, generated by `range()` function, with given `value` in `str` * @param {String} str Where to replace ranges * @param {Array} ranges Ranes, created by `range()` function * @param {String|Function} value Replacement value. If it’s a function, it * will take a range value as argument and should return a new string * @return {String} */ function replaceRanges(str, ranges, value) { // should walk from the end of array to keep ranges valid after replacement for (let i = ranges.length - 1; i >= 0; i--) { const r = ranges[i]; let offset = 0; let offsetLength = 0; if (str.substr(r[0] + r[1], 1) === '@'){ const matches = str.substr(r[0] + r[1] + 1).match(/^(\d+)/); if (matches) { offsetLength = matches[1].length + 1; offset = parseInt(matches[1]) - 1; } } str = str.substring(0, r[0]) + (typeof value === 'function' ? value(str.substr(r[0], r[1]), offset) : value) + str.substring(r[0] + r[1] + offsetLength); } return str; } function range(start, length) { return [start, length]; } const numberingToken = '$'; /** * Numbering of expanded abbreviation: finds all nodes with `$` in value * or attributes and replaces its occurances with repeater value */ var applyNumbering = function(tree) { tree.walk(applyNumbering$1); return tree; }; /** * Applies numbering for given node: replaces occurances of numbering token * in node’s name, content and attributes * @param {Node} node * @return {Node} */ function applyNumbering$1(node) { const repeater = findRepeater(node); if (repeater && repeater.value != null) { // NB replace numbering in nodes with explicit repeater only: // it solves issues with abbreviations like `xsl:if[test=$foo]` where // `$foo` is preferred output const value = repeater.value; node.name = replaceNumbering(node.name, value); node.value = replaceNumbering(node.value, value); node.attributes.forEach(attr => { const copy = node.getAttribute(attr.name).clone(); copy.name = replaceNumbering(attr.name, value); copy.value = replaceNumbering(attr.value, value); node.replaceAttribute(attr.name, copy); }); } return node; } /** * Returns repeater object for given node * @param {Node} node * @return {Object} */ function findRepeater(node) { while (node) { if (node.repeat) { return node.repeat; } node = node.parent; } } /** * Replaces numbering in given string * @param {String} str * @param {Number} value * @return {String} */ function replaceNumbering(str, value) { // replace numbering in strings only: skip explicit wrappers that could // contain unescaped numbering tokens if (typeof str === 'string') { const ranges = getNumberingRanges(str); return replaceNumberingRanges(str, ranges, value); } return str; } /** * Returns numbering ranges, e.g. ranges of `$` occurances, in given string. * Multiple adjacent ranges are combined * @param {String} str * @return {Array} */ function getNumberingRanges(str) { return findUnescapedTokens(str || '', numberingToken) .reduce((out, range$$1) => { // skip ranges that actually belongs to output placeholder or tabstops if (!/[#{]/.test(str[range$$1[0] + 1] || '')) { const lastRange = out[out.length - 1]; if (lastRange && lastRange[0] + lastRange[1] === range$$1[0]) { lastRange[1] += range$$1[1]; } else { out.push(range$$1); } } return out; }, []); } /** * @param {String} str * @param {Array} ranges * @param {Number} value * @return {String} */ function replaceNumberingRanges(str, ranges, value) { const replaced = replaceRanges(str, ranges, (token, offset) => { let _value = String(value + offset); // pad values for multiple numbering tokens, e.g. 3 for $$$ becomes 003 while (_value.length < token.length) { _value = '0' + _value; } return _value; }); // unescape screened numbering tokens return unescapeString(replaced); } /** * Unescapes characters, screened with `\`, in given string * @param {String} str * @return {String} */ function unescapeString(str) { let i = 0, result = ''; const len = str.length; while (i < len) { const ch = str[i++]; result += (ch === '\\') ? (str[i++] || '') : ch; } return result; } /** Placeholder for inserted content */ const placeholder = '$#'; /** Placeholder for caret */ const caret = '|'; const reUrl = /^((?:https?|ftp|file):\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/; const reEmail = /^([a-z0-9_\.-]+)@([\da-z\.-]+)\.([a-z\.]{2,6})$/; const reProto = /^([a-z]+:)?\/\//i; /** * Inserts content into node with implicit repeat count: this node is then * duplicated for each content item and content itself is inserted either into * deepest child or instead of a special token. * * This method uses two distinct steps: `prepare()` and `insert()` since most * likely these steps will be used separately to properly insert content * with unescaped `$` numbering markers. * * @param {Node} tree Parsed abbreviation * @param {String[]} content Array of content items to insert * @return {Node} */ /** * Finds nodes with implicit repeat and creates `amount` copies of it in tree * @param {Node} tree * @param {Number} amount * @return {Node} */ function prepare(tree, amount) { amount = amount || 1; tree.walk(node => { if (node.repeat && node.repeat.count === null) { for (let i = 0; i < amount; i++) { const clone = node.clone(true); clone.repeat.implicit = true; clone.repeat.count = amount; clone.repeat.value = i + 1; clone.repeat.index = i; node.parent.insertBefore(clone, node); } node.remove(); } }); return tree; } /** * Inserts content into implicitly repeated nodes, created by `prepare()` method * @param {Node} tree * @param {String[]} content * @return {Node} */ function insert(tree, content) { if (Array.isArray(content) && content.length) { let updated = false; tree.walk(node => { if (node.repeat && node.repeat.implicit) { updated = true; insertContent(node, content[node.repeat.index]); } }); if (!updated) { // no node with implicit repeat was found, insert content as // deepest child setNodeContent(findDeepestNode$1(tree), content.join('\n')); } } return tree; } /** * Inserts `content` into given `node`: either replaces output placeholders * or inserts it into deepest child node * @param {Node} node * @param {String} content * @return {Node} */ function insertContent(node, content) { let inserted = insertContentIntoPlaceholder(node, content); node.walk(child => inserted |= insertContentIntoPlaceholder(child, content)); if (!inserted) { // no placeholders were found in node, insert content into deepest child setNodeContent(findDeepestNode$1(node), content); } return node; } /** * Inserts given `content` into placeholders for given `node`. Placeholders * might be available in attribute values and node content * @param {Node} node * @param {String} content * @return {Boolean} Returns `true` if placeholders were found and replaced in node */ function insertContentIntoPlaceholder(node, content) { const state = {replaced: false}; node.value = replacePlaceholder(node.value, content, state); node.attributes.forEach(attr => { if (attr.value) { node.setAttribute(attr.name, replacePlaceholder(attr.value, content, state)); } }); return state.replaced; } /** * Replaces all placeholder occurances in given `str` with `value` * @param {String} str * @param {String} value * @param {Object} [_state] If provided, set `replaced` property of given * object to `true` if placeholder was found and replaced * @return {String} */ function replacePlaceholder(str, value, _state) { if (typeof str === 'string') { const ranges = findUnescapedTokens(str, placeholder); if (ranges.length) { if (_state) { _state.replaced = true; } str = replaceRanges(str, ranges, value); } } return str; } /** * Finds node which is the deepest for in current node or node iteself. * @param {Node} node * @return {Node} */ function findDeepestNode$1(node) { while (node.children.length) { node = node.children[node.children.length - 1]; } return node; } /** * Updates content of given node * @param {Node} node * @param {String} content */ function setNodeContent(node, content) { // find caret position and replace it with content, if possible if (node.value) { const ranges = findUnescapedTokens(node.value, caret); if (ranges.length) { node.value = replaceRanges(node.value, ranges, content); return; } } if (node.name.toLowerCase() === 'a' || node.hasAttribute('href')) { // special case: inserting content into `` tag if (reUrl.test(content)) { node.setAttribute('href', (reProto.test(content) ? '' : 'http://') + content); } else if (reEmail.test(content)) { node.setAttribute('href', 'mailto:' + content); } } node.value = content; } const defaultOptions$2 = { element: '__', modifier: '_' }; const reElement = /^(-+)([a-z0-9]+[a-z0-9-]*)/i; const reModifier = /^(_+)([a-z0-9]+[a-z0-9-]*)/i; const blockCandidates1 = className => /^[a-z]\-/i.test(className); const blockCandidates2 = className => /^[a-z]/i.test(className); /** * BEM transformer: updates class names written as `-element` and * `_modifier` into full class names as described in BEM specs. Also adds missing * class names: fir example, if node contains `.block_modifier` class, ensures * that element contains `.block` class as well */ var bem = function(tree, options) { options = Object.assign({}, defaultOptions$2, options); tree.walk(node => expandClassNames(node, options)); const lookup = createBlockLookup(tree); tree.walk(node => expandShortNotation(node, lookup, options)); return tree; }; /** * Expands existing class names in BEM notation in given `node`. * For example, if node contains `b__el_mod` class name, this method ensures * that element contains `b__el` class as well * @param {Node} node * @param {Object} options * @return {Set} */ function expandClassNames(node, options) { const classNames = node.classList.reduce((out, cl) => { // remove all modifiers and element prefixes from class name to get a base element name const ix = cl.indexOf('_'); if (ix > 0 && !cl.startsWith('-')) { out.add(cl.slice(0, ix)); out.add(cl.slice(ix)); return out; } return out.add(cl); }, new Set()); if (classNames.size) { node.setAttribute('class', Array.from(classNames).join(' ')); } } /** * Expands short BEM notation, e.g. `-element` and `_modifier` * @param {Node} node Parsed Emmet abbreviation node * @param {Map} lookup BEM block name lookup * @param {Object} options */ function expandShortNotation(node, lookup, options) { const classNames = node.classList.reduce((out, cl) => { let prefix, m; const originalClass = cl; // parse element definition (could be only one) if (m = cl.match(reElement)) { prefix = getBlockName(node, lookup, m[1]) + options.element + m[2]; out.add(prefix); cl = cl.slice(m[0].length); } // parse modifiers definitions (may contain multiple) while (m = cl.match(reModifier)) { if (!prefix) { prefix = getBlockName(node, lookup, m[1]); out.add(prefix); } out.add(`${prefix}${options.modifier}${m[2]}`); cl = cl.slice(m[0].length); } if (cl === originalClass) { // class name wasn’t modified: it’s not a BEM-specific class, // add it as-is into output out.add(originalClass); } return out; }, new Set()); const arrClassNames = Array.from(classNames).filter(Boolean); if (arrClassNames.length) { node.setAttribute('class', arrClassNames.join(' ')); } } /** * Creates block name lookup for each node in given tree, e.g. finds block * name explicitly for each node * @param {Node} tree * @return {Map} */ function createBlockLookup(tree) { const lookup = new Map(); tree.walk(node => { const classNames = node.classList; if (classNames.length) { // guess best block name from class or use parent’s block name lookup.set(node, find(classNames, blockCandidates1) || find(classNames, blockCandidates2) || lookup.get(node.parent) ); } }); return lookup; } /** * Returns block name for given `node` by `prefix`, which tells the depth of * of parent node lookup * @param {Node} node * @param {Map} lookup * @param {String} prefix * @return {String} */ function getBlockName(node, lookup, prefix) { let depth = prefix.length > 1 ? prefix.length : 0; // NB don’t walk up to root node, stay at first root child in case of // too deep prefix while (node.parent && node.parent.parent && depth--) { node = node.parent; } return lookup.get(node) || ''; } function find(arr, filter) { return arr.filter(filter)[0]; } /** * JSX transformer: replaces `class` and `for` attributes with `className` and * `htmlFor` attributes respectively */ var jsx = function(tree) { tree.walk(node => { replace(node, 'class', 'className'); replace(node, 'for', 'htmlFor'); }); return tree; }; function replace(node, oldName, newName) { let attr = node.getAttribute(oldName); if (attr) { attr.name = newName; } } const reSupporterNames = /^xsl:(variable|with\-param)$/i; /** * XSL transformer: removes `select` attributes from certain nodes that contain * children */ var xsl = function(tree) { tree.walk(node => { if (reSupporterNames.test(node.name || '') && (node.children.length || node.value)) { node.removeAttribute('select'); } }); return tree; }; const supportedAddons = { bem, jsx, xsl }; /** * Runs additional transforms on given tree. * These transforms may introduce side-effects and unexpected result * so they are not applied by default, authors must specify which addons * in `addons` argument as `{addonName: addonOptions}` * @param {Node} tree Parsed Emmet abbreviation * @param {Object} addons Add-ons to apply and their options */ var addons = function(tree, addons) { Object.keys(addons || {}).forEach(key => { if (key in supportedAddons) { const addonOpt = typeof addons[key] === 'object' ? addons[key] : null; tree = tree.use(supportedAddons[key], addonOpt); } }); return tree; }; /** * Applies basic HTML-specific transformations for given parsed abbreviation: * – resolve implied tag names * – insert repeated content * – resolve node numbering */ var index$2 = function(tree, content, appliedAddons) { if (typeof content === 'string') { content = [content]; } else if (content && typeof content === 'object' && !Array.isArray(content)) { appliedAddons = content; content = null; } return tree .use(implicitTags) .use(prepare, Array.isArray(content) ? content.length : null) .use(applyNumbering) .use(insert, content) .use(addons, appliedAddons); }; /** * Replaces all unescaped ${variable} occurances in given parsed abbreviation * `tree` with values provided in `variables` hash. Precede `$` with `\` to * escape it and skip replacement * @param {Node} tree Parsed abbreviation tree * @param {Object} variables Variables values * @return {Node} */ function replaceVariables(tree, variables) { variables = variables || {}; tree.walk(node => replaceInNode(node, variables)); return tree; } function replaceInNode(node, variables) { // Replace variables in attributes. const attrs = node.attributes; for (let i = 0, il = attrs.length; i < il; i++) { const attr = attrs[i]; if (typeof attr.value === 'string') { node.setAttribute(attr.name, replaceInString(attr.value, variables)); } } if (node.value != null) { node.value = replaceInString(node.value, variables); } return node; } /** * Replaces all unescaped `${variable}` occurances in given string with values * from `variables` object * @param {String} string * @param {Object} variables * @return {String} */ function replaceInString(string, variables) { const model = createModel(string); let offset = 0; let output = ''; for (let i = 0, il = model.variables.length; i < il; i++) { const v = model.variables[i]; let value = v.name in variables ? variables[v.name] : v.name; if (typeof value === 'function') { value = value(model.string, v, offset + v.location); } output += model.string.slice(offset, v.location) + value; offset = v.location + v.length; } return output + model.string.slice(offset); } /** * Creates variable model from given string. The model contains a `string` with * all escaped variable tokens written without escape symbol and `variables` * property with all unescaped variables and their ranges * @param {String} string * @return {Object} */ function createModel(string) { const reVariable = /\$\{([a-z][\w\-]*)\}/ig; const escapeCharCode = 92; // `\` symbol const variables = []; // We have to replace unescaped (e.g. not preceded with `\`) tokens. // Instead of writing a stream parser, we’ll cut some edges here: // 1. Find all tokens // 2. Walk string char-by-char and resolve only tokens that are not escaped const tokens = new Map(); let m; while (m = reVariable.exec(string)) { tokens.set(m.index, m); } if (tokens.size) { let start = 0, pos = 0, len = string.length; let output = ''; while (pos < len) { if (string.charCodeAt(pos) === escapeCharCode && tokens.has(pos + 1)) { // Found escape symbol that escapes variable: we should // omit this symbol in output string and skip variable const token = tokens.get(pos + 1); output += string.slice(start, pos) + token[0]; start = pos = token.index + token[0].length; tokens.delete(pos + 1); continue; } pos++; } string = output + string.slice(start); // Not using `.map()` here to reduce memory allocations const validMatches = Array.from(tokens.values()); for (let i = 0, il = validMatches.length; i < il; i++) { const token = validMatches[i]; variables.push({ name: token[1], location: token.index, length: token[0].length }); } } return {string, variables}; } const DOLLAR = 36; // $ const COLON = 58; // : const ESCAPE$1 = 92; // \ const OPEN_BRACE = 123; // { const CLOSE_BRACE = 125; // } /** * Finds fields in given string and returns object with field-less string * and array of fileds found * @param {String} string * @return {Object} */ function parse$2(string) { const stream = new StreamReader(string); const fields = []; let cleanString = '', offset = 0, pos = 0; let code, field; while (!stream.eof()) { code = stream.peek(); pos = stream.pos; if (code === ESCAPE$1) { stream.next(); stream.next(); } else if (field = consumeField(stream, cleanString.length + pos - offset)) { fields.push(field); cleanString += stream.string.slice(offset, pos) + field.placeholder; offset = stream.pos; } else { stream.next(); } } return new FieldString(cleanString + stream.string.slice(offset), fields); } /** * Marks given `string` with `fields`: wraps each field range with * `${index:placeholder}` (by default) or any other token produced by `token` * function, if provided * @param {String} string String to mark * @param {Array} fields Array of field descriptor. A field descriptor is a * `{index, location, length}` array. It is important that fields in array * must be ordered by their location in string: some fields my refer the same * location so they must appear in order that user expects. * @param {Function} [token] Function that generates field token. This function * received two arguments: `index` and `placeholder` and should return string * @return {String} String with marked fields */ function mark(string, fields, token) { token = token || createToken; // order fields by their location and appearence // NB field ranges should not overlap! (not supported yet) const ordered = fields .map((field, order) => ({order, field, end: field.location + field.length})) .sort((a, b) => (a.end - b.end) || (a.order - b.order)); // mark ranges in string let offset = 0; const result = ordered.map(item => { const placeholder = string.substr(item.field.location, item.field.length); const prefix = string.slice(offset, item.field.location); offset = item.end; return prefix + token(item.field.index, placeholder); }); return result.join('') + string.slice(offset); } /** * Creates field token for string * @param {Number} index Field index * @param {String} placeholder Field placeholder, could be empty string * @return {String} */ function createToken(index, placeholder) { return placeholder ? `\${${index}:${placeholder}}` : `\${${index}}`; } /** * Consumes field from current stream position: it can be an `$index` or * or `${index}` or `${index:placeholder}` * @param {StreamReader} stream * @param {Number} location Field location in *clean* string * @return {Object} Object with `index` and `placeholder` properties if * fieald was successfully consumed, `null` otherwise */ function consumeField(stream, location) { const start = stream.pos; if (stream.eat(DOLLAR)) { // Possible start of field let index = consumeIndex(stream); let placeholder = ''; // consumed $index placeholder if (index != null) { return new Field(index, placeholder, location); } if (stream.eat(OPEN_BRACE)) { index = consumeIndex(stream); if (index != null) { if (stream.eat(COLON)) { placeholder = consumePlaceholder(stream); } if (stream.eat(CLOSE_BRACE)) { return new Field(index, placeholder, location); } } } } // If we reached here then there’s no valid field here, revert // back to starting position stream.pos = start; } /** * Consumes a placeholder: value right after `:` in field. Could be empty * @param {StreamReader} stream * @return {String} */ function consumePlaceholder(stream) { let code; const stack = []; stream.start = stream.pos; while (!stream.eof()) { code = stream.peek(); if (code === OPEN_BRACE) { stack.push(stream.pos); } else if (code === CLOSE_BRACE) { if (!stack.length) { break; } stack.pop(); } stream.next(); } if (stack.length) { throw stream.error('Unable to find matching "}" for curly brace at ' + stack.pop()); } return stream.current(); } /** * Consumes integer from current stream position * @param {StreamReader} stream * @return {Number} */ function consumeIndex(stream) { stream.start = stream.pos; if (stream.eatWhile(isNumber)) { return Number(stream.current()); } } class Field { constructor(index, placeholder, location) { this.index = index; this.placeholder = placeholder; this.location = location; this.length = this.placeholder.length; } } class FieldString { /** * @param {String} string * @param {Field[]} fields */ constructor(string, fields) { this.string = string; this.fields = fields; } mark(token) { return mark(this.string, this.fields, token); } toString() { return string; } } const defaultFieldsRenderer = text => text; /** * Output node is an object containing generated output for given Emmet * abbreviation node. Output node can be passed to various processors that * may shape-up final node output. The final output is simply a concatenation * of `.open`, `.text` and `.close` properties and its `.before*` and `.after*` * satellites * @param {Node} node Parsed Emmet abbreviation node * @param {Function} fieldsRenderer A function for rendering fielded text (text with * tabstops) for current node. @see ./render.js for details */ class OutputNode { constructor(node, fieldsRenderer, options) { if (typeof fieldsRenderer === 'object') { options = fieldsRenderer; fieldsRenderer = null; } this.node = node; this._fieldsRenderer = fieldsRenderer || defaultFieldsRenderer; this.open = null; this.beforeOpen = ''; this.afterOpen = ''; this.close = null; this.beforeClose = ''; this.afterClose = ''; this.text = null; this.beforeText = ''; this.afterText = ''; this.indent = ''; this.newline = ''; if (options) { Object.assign(this, options); } } clone() { return new this.constructor(this.node, this); } /** * Properly indents given multiline text * @param {String} text */ indentText(text) { const lines = splitByLines(text); if (lines.length === 1) { // no newlines, nothing to indent return text; } // No newline and no indent means no formatting at all: // in this case we should replace newlines with spaces const nl = (!this.newline && !this.indent) ? ' ' : this.newline; return lines.map((line, i) => i ? this.indent + line : line).join(nl); } /** * Renders given text with fields * @param {String} text * @return {String} */ renderFields(text) { return this._fieldsRenderer(text); } toString(children) { const open = this._wrap(this.open, this.beforeOpen, this.afterOpen); const close = this._wrap(this.close, this.beforeClose, this.afterClose); const text = this._wrap(this.text, this.beforeText, this.afterText); return open + text + (children != null ? children : '') + close; } _wrap(str, before, after) { before = before != null ? before : ''; after = after != null ? after : ''; // automatically trim whitespace for non-empty wraps if (str != null) { str = before ? str.replace(/^\s+/, '') : str; str = after ? str.replace(/\s+$/, '') : str; return before + this.indentText(str) + after; } return ''; } } /** * Splits given text by lines * @param {String} text * @return {String[]} */ function splitByLines(text) { return (text || '').split(/\r\n|\r|\n/g); } /** * Default output of field (tabstop) * @param {Number} index Field index * @param {String} placeholder Field placeholder, can be null * @return {String} */ const defaultField = (index, placeholder) => (placeholder || ''); /** * Renders given parsed abbreviation `tree` via `formatter` function. * @param {Node} tree Parsed Emmet abbreviation * @param {Function} [field] Optional function to format field/tabstop (@see `defaultField`) * @param {Function} formatter Output formatter function. It takes an output node— * a special wrapper for parsed node that holds formatting and output properties— * and updates its output properties to shape-up node’s output. * Function arguments: * – `outNode`: OutputNode * – `renderFields`: a helper function that parses fields/tabstops from given * text and replaces them with `field` function output. * It also takes care about field indicies and ensures that the same indicies * from different nodes won’t collide */ function render(tree, field, formatter) { if (typeof formatter === 'undefined') { formatter = field; field = null; } field = field || defaultField; // Each node may contain fields like `${1:placeholder}`. // Since most modern editors will link all fields with the same // index, we have to ensure that different nodes has their own indicies. // We’ll use this `fieldState` object to globally increment field indices // during output const fieldState = { index: 1 }; const fieldsRenderer = text => text == null ? field(fieldState.index++) : getFieldsModel(text, fieldState).mark(field); return run(tree.children, formatter, fieldsRenderer); } function run(nodes, formatter, fieldsRenderer) { return nodes.map(node => { const outNode = formatter(new OutputNode(node, fieldsRenderer)); return outNode ? outNode.toString(run(node.children, formatter, fieldsRenderer)) : ''; }).join(''); } /** * Returns fields (tab-stops) model with properly updated indices that won’t * collide with fields in other nodes of foprmatted tree * @param {String|Object} text Text to get fields model from or model itself * @param {Object} fieldState Abbreviation tree-wide field state reference * @return {Object} Field model */ function getFieldsModel(text, fieldState) { const model = typeof text === 'object' ? text : parse$2(text); let largestIndex = -1; model.fields.forEach(field => { field.index += fieldState.index; if (field.index > largestIndex) { largestIndex = field.index; } }); if (largestIndex !== -1) { fieldState.index = largestIndex + 1; } return model; } const TOKEN = /^(.*?)([A-Z_]+)(.*?)$/; const TOKEN_OPEN = 91; // [ const TOKEN_CLOSE = 93; // ] /** * A basic templating engine. * Takes every `[TOKEN]` from given string and replaces it with * `TOKEN` value from given `data` attribute. The token itself may contain * various characters between `[`, token name and `]`. Contents of `[...]` will * be outputted only if `TOKEN` value is not empty. Also, only `TOKEN` name will * be replaced with actual value, all other characters will remain as is. * * Example: * ``` * template('[]', {NAME: 'foo'}) -> "" * template('[]', {}) -> "" * ``` */ function template(str, data) { if (str == null) { return str; } // NB since token may contain inner `[` and `]`, we can’t just use regexp // for replacement, should manually parse string instead const stack = []; const replacer = (str, left, token, right) => data[token] != null ? left + data[token] + right : ''; let output = ''; let offset = 0, i = 0; let code, lastPos; while (i < str.length) { code = str.charCodeAt(i); if (code === TOKEN_OPEN) { stack.push(i); } else if (code === TOKEN_CLOSE) { lastPos = stack.pop(); if (!stack.length) { output += str.slice(offset, lastPos) + str.slice(lastPos + 1, i).replace(TOKEN, replacer); offset = i + 1; } } i++; } return output + str.slice(offset); } /** * Various utility methods used by formatters */ /** * Splits given text by lines * @param {String} text * @return {String[]} */ function splitByLines$1(text) { return (text || '').split(/\r\n|\r|\n/g); } /** * Check if given node is a first child in its parent * @param {Node} node * @return {Boolean} */ function isFirstChild(node) { return node.parent.firstChild === node; } /** * Check if given node is a last child in its parent node * @param {Node} node * @return {Boolean} */ /** * Check if given node is a root node * @param {Node} node * @return {Boolean} */ function isRoot(node) { return node && !node.parent; } /** * Check if given node is a pseudo-snippet: a text-only node with explicitly * defined children * @param {Node} node * @return {Boolean} */ function isPseudoSnippet(node) { return node.isTextOnly && !!node.children.length; } /** * Handles pseudo-snippet node. * A pseudo-snippet is a text-only node with explicitly defined children. * For such case, we have to figure out if pseudo-snippet contains fields * (tab-stops) in node value and “split” it: make contents before field with * lowest index node’s “open” part and contents after lowest index — “close” * part. With this trick a final output will look like node’s children * are nested inside node value * @param {OutputNode} outNode * @return {Boolean} Returns “true” if given node is a pseudo-snippets, * `false` otherwise */ function handlePseudoSnippet(outNode) { const node = outNode.node; // original abbreviaiton node if (isPseudoSnippet(node)) { const fieldsModel = parse$2(node.value); const field = findLowestIndexField(fieldsModel); if (field) { const parts = splitFieldsModel(fieldsModel, field); outNode.open = outNode.renderFields(parts[0]); outNode.close = outNode.renderFields(parts[1]); } else { outNode.text = outNode.renderFields(fieldsModel); } return true; } return false; } /** * Finds field with lowest index in given text * @param {Object} model * @return {Object} */ function findLowestIndexField(model) { return model.fields.reduce((result, field) => !result || field.index < result.index ? field : result , null); } /** * Splits given fields model in two parts by given field * @param {Object} model * @param {Object} field * @return {Array} Two-items array */ function splitFieldsModel(model, field) { const ix = model.fields.indexOf(field); const left = new model.constructor( model.string.slice(0, field.location), model.fields.slice(0, ix) ); const right = new model.constructor( model.string.slice(field.location + field.length), model.fields.slice(ix + 1) ); return [left, right]; } const commentOptions = { // enable node commenting enabled: false, // attributes that should trigger node commenting on specific node, // if commenting is enabled trigger: ['id', 'class'], // comment before opening tag before: '', // comment after closing tag after: '\n' }; /** * Renders given parsed Emmet abbreviation as HTML, formatted according to * `profile` options * @param {Node} tree Parsed Emmet abbreviation * @param {Profile} profile Output profile * @param {Object} [options] Additional formatter options * @return {String} */ function html(tree, profile, options) { options = Object.assign({}, options); options.comment = Object.assign({}, commentOptions, options.comment); return render(tree, options.field, outNode => { outNode = setFormatting(outNode, profile); if (!handlePseudoSnippet(outNode)) { const node = outNode.node; if (node.name) { const name = profile.name(node.name); const attrs = formatAttributes(outNode, profile); outNode.open = `<${name}${attrs}${node.selfClosing ? profile.selfClose() : ''}>`; if (!node.selfClosing) { outNode.close = ``; } commentNode(outNode, options.comment); } // Do not generate fields for nodes with empty value and children // or if node is self-closed if (node.value || (!node.children.length && !node.selfClosing) ) { outNode.text = outNode.renderFields(node.value); } } return outNode; }); } /** * Updates formatting properties for given output node * @param {OutputNode} outNode Output wrapper of farsed abbreviation node * @param {Profile} profile Output profile * @return {OutputNode} */ function setFormatting(outNode, profile) { const node = outNode.node; if (shouldFormatNode(node, profile)) { outNode.indent = profile.indent(getIndentLevel(node, profile)); outNode.newline = '\n'; const prefix = outNode.newline + outNode.indent; // do not format the very first node in output if (!isRoot(node.parent) || !isFirstChild(node)) { outNode.beforeOpen = prefix; if (node.isTextOnly) { outNode.beforeText = prefix; } } if (hasInnerFormatting(node, profile)) { if (!node.isTextOnly) { outNode.beforeText = prefix + profile.indent(1); } outNode.beforeClose = prefix; } } return outNode; } /** * Check if given node should be formatted * @param {Node} node * @param {Profile} profile * @return {Boolean} */ function shouldFormatNode(node, profile) { if (!profile.get('format')) { return false; } if (node.parent.isTextOnly && node.parent.children.length === 1 && parse$2(node.parent.value).fields.length) { // Edge case: do not format the only child of text-only node, // but only if parent contains fields return false; } return isInline(node, profile) ? shouldFormatInline(node, profile) : true; } /** * Check if given inline node should be formatted as well, e.g. it contains * enough adjacent siblings that should force formatting * @param {Node} node * @param {Profile} profile * @return {Boolean} */ function shouldFormatInline(node, profile) { if (!isInline(node, profile)) { return false; } if (isPseudoSnippet(node)) { return true; } // check if inline node is the next sibling of block-level node if (node.childIndex === 0) { // first node in parent: format if it’s followed by a block-level element let next = node; while (next = next.nextSibling) { if (!isInline(next, profile)) { return true; } } } else if (!isInline(node.previousSibling, profile)) { // node is right after block-level element return true; } if (profile.get('inlineBreak')) { // check for adjacent inline elements before and after current element let adjacentInline = 1; let before = node, after = node; while (isInlineElement((before = before.previousSibling), profile)) { adjacentInline++; } while (isInlineElement((after = after.nextSibling), profile)) { adjacentInline++; } if (adjacentInline >= profile.get('inlineBreak')) { return true; } } // Another edge case: inline node contains node that should receive foramtting for (let i = 0, il = node.children.length; i < il; i++) { if (shouldFormatNode(node.children[i], profile)) { return true; } } return false; } /** * Check if given node contains inner formatting, e.g. any of its children should * be formatted * @param {Node} node * @param {Profile} profile * @return {Boolean} */ function hasInnerFormatting(node, profile) { // check if node if forced for inner formatting const nodeName = (node.name || '').toLowerCase(); if (profile.get('formatForce').indexOf(nodeName) !== -1) { return true; } // check if any of children should receive formatting // NB don’t use `childrent.some()` to reduce memory allocations for (let i = 0; i < node.children.length; i++) { if (shouldFormatNode(node.children[i], profile)) { return true; } } return false; } /** * Outputs attributes of given abbreviation node as HTML attributes * @param {OutputNode} outNode * @param {Profile} profile * @return {String} */ function formatAttributes(outNode, profile) { const node = outNode.node; return node.attributes.map(attr => { if (attr.options.implied && attr.value == null) { return null; } const attrName = profile.attribute(attr.name); let attrValue = null; // handle boolean attributes if (attr.options.boolean || profile.get('booleanAttributes').indexOf(attrName.toLowerCase()) !== -1) { if (profile.get('compactBooleanAttributes') && attr.value == null) { return ` ${attrName}`; } else if (attr.value == null) { attrValue = attrName; } } if (attrValue == null) { attrValue = outNode.renderFields(attr.value); } // For https://github.com/Microsoft/vscode/issues/63703 // https://github.com/emmetio/markup-formatters/pull/2/files return attr.options.before && attr.options.after ? ` ${attrName}=${attr.options.before+attrValue+attr.options.after}` : ` ${attrName}=${profile.quote(attrValue)}`; }).join(''); } /** * Check if given node is inline-level * @param {Node} node * @param {Profile} profile * @return {Boolean} */ function isInline(node, profile) { return (node && node.isTextOnly) || isInlineElement(node, profile); } /** * Check if given node is inline-level element, e.g. element with explicitly * defined node name * @param {Node} node * @param {Profile} profile * @return {Boolean} */ function isInlineElement(node, profile) { return node && profile.isInline(node); } /** * Computes indent level for given node * @param {Node} node * @param {Profile} profile * @param {Number} level * @return {Number} */ function getIndentLevel(node, profile) { // Increase indent level IF NOT: // * parent is text-only node // * there’s a parent node with a name that is explicitly set to decrease level const skip = profile.get('formatSkip') || []; let level = node.parent.isTextOnly ? -2 : -1; let ctx = node; while (ctx = ctx.parent) { if (skip.indexOf( (ctx.name || '').toLowerCase() ) === -1) { level++; } } return level < 0 ? 0 : level; } /** * Comments given output node, if required * @param {OutputNode} outNode * @param {Object} options */ function commentNode(outNode, options) { const node = outNode.node; if (!options.enabled || !options.trigger || !node.name) { return; } const attrs = outNode.node.attributes.reduce((out, attr) => { if (attr.name && attr.value != null) { out[attr.name.toUpperCase().replace(/-/g, '_')] = attr.value; } return out; }, {}); // add comment only if attribute trigger is present for (let i = 0, il = options.trigger.length; i < il; i++) { if (options.trigger[i].toUpperCase() in attrs) { outNode.open = template(options.before, attrs) + outNode.open; if (outNode.close) { outNode.close += template(options.after, attrs); } break; } } } /** * Common utility methods for indent-based syntaxes (Slim, Pug, etc.) */ const reId = /^id$/i; const reClass = /^class$/i; const defaultAttrOptions = { primary: attrs => attrs.join(''), secondary: attrs => attrs.map(attr => attr.isBoolean ? attr.name : `${attr.name}=${attr.value}`).join(', ') }; const defaultNodeOptions = { open: null, close: null, omitName: /^div$/i, attributes: defaultAttrOptions }; function indentFormat(outNode, profile, options) { options = Object.assign({}, defaultNodeOptions, options); const node = outNode.node; outNode.indent = profile.indent(getIndentLevel$1(node, profile)); outNode.newline = '\n'; // Do not format the very first node in output if (!isRoot(node.parent) || !isFirstChild(node)) { outNode.beforeOpen = outNode.newline + outNode.indent; } if (node.name) { const data = Object.assign({ NAME: profile.name(node.name), SELF_CLOSE: node.selfClosing ? options.selfClose : null }, getAttributes(outNode, profile, options.attributes)); // omit tag name if node has primary attributes if (options.omitName && options.omitName.test(data.NAME) && data.PRIMARY_ATTRS) { data.NAME = null; } if (options.open != null) { outNode.open = template(options.open, data); } if (options.close != null) { outNode.close = template(options.close, data); } } return outNode; } /** * Formats attributes of given node into a string. * @param {OutputNode} node Output node wrapper * @param {Profile} profile Output profile * @param {Object} options Additional formatting options * @return {String} */ function getAttributes(outNode, profile, options) { options = Object.assign({}, defaultAttrOptions, options); const primary = [], secondary = []; const node = outNode.node; node.attributes.forEach(attr => { if (attr.options.implied && attr.value == null) { return null; } const name = profile.attribute(attr.name); const value = outNode.renderFields(attr.value); if (reId.test(name)) { value && primary.push(`#${value}`); } else if (reClass.test(name)) { value && primary.push(`.${value.replace(/\s+/g, '.')}`); } else { const isBoolean = attr.value == null && (attr.options.boolean || profile.get('booleanAttributes').indexOf(name.toLowerCase()) !== -1); secondary.push({ name, value, isBoolean }); } }); return { PRIMARY_ATTRS: options.primary(primary) || null, SECONDARY_ATTRS: options.secondary(secondary) || null }; } /** * Computes indent level for given node * @param {Node} node * @param {Profile} profile * @param {Number} level * @return {Number} */ function getIndentLevel$1(node, profile) { let level = node.parent.isTextOnly ? -2 : -1; let ctx = node; while (ctx = ctx.parent) { level++; } return level < 0 ? 0 : level; } const reNl = /\n|\r/; /** * Renders given parsed Emmet abbreviation as HAML, formatted according to * `profile` options * @param {Node} tree Parsed Emmet abbreviation * @param {Profile} profile Output profile * @param {Object} [options] Additional formatter options * @return {String} */ function haml(tree, profile, options) { options = options || {}; const nodeOptions = { open: '[%NAME][PRIMARY_ATTRS][(SECONDARY_ATTRS)][SELF_CLOSE]', selfClose: '/', attributes: { secondary(attrs) { return attrs.map(attr => attr.isBoolean ? `${attr.name}${profile.get('compactBooleanAttributes') ? '' : '=true'}` : `${attr.name}=${profile.quote(attr.value)}` ).join(' '); } } }; return render(tree, options.field, outNode => { outNode = indentFormat(outNode, profile, nodeOptions); outNode = updateFormatting(outNode, profile); if (!handlePseudoSnippet(outNode)) { const node = outNode.node; // Do not generate fields for nodes with empty value and children // or if node is self-closed if (node.value || (!node.children.length && !node.selfClosing) ) { outNode.text = outNode.renderFields(formatNodeValue(node, profile)); } } return outNode; }); } /** * Updates formatting properties for given output node * NB Unlike HTML, HAML is indent-based format so some formatting options from * `profile` will not take effect, otherwise output will be broken * @param {OutputNode} outNode Output wrapper of parsed abbreviation node * @param {Profile} profile Output profile * @return {OutputNode} */ function updateFormatting(outNode, profile) { const node = outNode.node; if (!node.isTextOnly && node.value) { // node with text: put a space before single-line text outNode.beforeText = reNl.test(node.value) ? outNode.newline + outNode.indent + profile.indent(1) : ' '; } return outNode; } /** * Formats value of given node: for multiline text we should add a ` |` suffix * at the end of each line. Also ensure that text is perfectly aligned. * @param {Node} node * @param {Profile} profile * @return {String|null} */ function formatNodeValue(node, profile) { if (node.value != null && reNl.test(node.value)) { const lines = splitByLines$1(node.value); const indent = profile.indent(1); const maxLength = lines.reduce((prev, line) => Math.max(prev, line.length), 0); return lines.map((line, i) => `${i ? indent : ''}${pad(line, maxLength)} |`).join('\n'); } return node.value; } function pad(text, len) { while (text.length < len) { text += ' '; } return text; } const reNl$1 = /\n|\r/; const secondaryAttrs = { none: '[ SECONDARY_ATTRS]', round: '[(SECONDARY_ATTRS)]', curly: '[{SECONDARY_ATTRS}]', square: '[[SECONDARY_ATTRS]' }; /** * Renders given parsed Emmet abbreviation as Slim, formatted according to * `profile` options * @param {Node} tree Parsed Emmet abbreviation * @param {Profile} profile Output profile * @param {Object} [options] Additional formatter options * @return {String} */ function slim(tree, profile, options) { options = options || {}; const SECONDARY_ATTRS = options.attributeWrap && secondaryAttrs[options.attributeWrap] || secondaryAttrs.none; const booleanAttr = SECONDARY_ATTRS === secondaryAttrs.none ? attr => `${attr.name}=true` : attr => attr.name; const nodeOptions = { open: `[NAME][PRIMARY_ATTRS]${SECONDARY_ATTRS}[SELF_CLOSE]`, selfClose: '/', attributes: { secondary(attrs) { return attrs.map(attr => attr.isBoolean ? booleanAttr(attr) : `${attr.name}=${profile.quote(attr.value)}` ).join(' '); } } }; return render(tree, options.field, (outNode, renderFields) => { outNode = indentFormat(outNode, profile, nodeOptions); outNode = updateFormatting$1(outNode, profile); if (!handlePseudoSnippet(outNode)) { const node = outNode.node; // Do not generate fields for nodes with empty value and children // or if node is self-closed if (node.value || (!node.children.length && !node.selfClosing) ) { outNode.text = outNode.renderFields(formatNodeValue$1(node, profile)); } } return outNode; }); } /** * Updates formatting properties for given output node * NB Unlike HTML, Slim is indent-based format so some formatting options from * `profile` will not take effect, otherwise output will be broken * @param {OutputNode} outNode Output wrapper of farsed abbreviation node * @param {Profile} profile Output profile * @return {OutputNode} */ function updateFormatting$1(outNode, profile) { const node = outNode.node; const parent = node.parent; // Edge case: a single inline-level child inside node without text: // allow it to be inlined if (profile.get('inlineBreak') === 0 && isInline$1(node, profile) && !isRoot(parent) && parent.value == null && parent.children.length === 1) { outNode.beforeOpen = ': '; } if (!node.isTextOnly && node.value) { // node with text: put a space before single-line text outNode.beforeText = reNl$1.test(node.value) ? outNode.newline + outNode.indent + profile.indent(1) : ' '; } return outNode; } /** * Formats value of given node: for multiline text we should precede each * line with `| ` with one-level deep indent * @param {Node} node * @param {Profile} profile * @return {String|null} */ function formatNodeValue$1(node, profile) { if (node.value != null && reNl$1.test(node.value)) { const indent = profile.indent(1); return splitByLines$1(node.value).map((line, i) => `${indent}${i ? ' ' : '|'} ${line}`).join('\n'); } return node.value; } /** * Check if given node is inline-level * @param {Node} node * @param {Profile} profile * @return {Boolean} */ function isInline$1(node, profile) { return node && (node.isTextOnly || profile.isInline(node)); } const reNl$2 = /\n|\r/; /** * Renders given parsed Emmet abbreviation as Pug, formatted according to * `profile` options * @param {Node} tree Parsed Emmet abbreviation * @param {Profile} profile Output profile * @param {Object} [options] Additional formatter options * @return {String} */ function pug(tree, profile, options) { options = options || {}; const nodeOptions = { open: '[NAME][PRIMARY_ATTRS][(SECONDARY_ATTRS)]', attributes: { secondary(attrs) { return attrs.map(attr => attr.isBoolean ? attr.name : `${attr.name}=${profile.quote(attr.value)}`).join(', '); } } }; return render(tree, options.field, outNode => { outNode = indentFormat(outNode, profile, nodeOptions); outNode = updateFormatting$2(outNode, profile); if (!handlePseudoSnippet(outNode)) { const node = outNode.node; // Do not generate fields for nodes with empty value and children // or if node is self-closed if (node.value || (!node.children.length && !node.selfClosing) ) { outNode.text = outNode.renderFields(formatNodeValue$2(node, profile)); } } return outNode; }); } /** * Updates formatting properties for given output node * NB Unlike HTML, Pug is indent-based format so some formatting options from * `profile` will not take effect, otherwise output will be broken * @param {OutputNode} outNode Output wrapper of parsed abbreviation node * @param {Profile} profile Output profile * @return {OutputNode} */ function updateFormatting$2(outNode, profile) { const node = outNode.node; if (!node.isTextOnly && node.value) { // node with text: put a space before single-line text outNode.beforeText = reNl$2.test(node.value) ? outNode.newline + outNode.indent + profile.indent(1) : ' '; } return outNode; } /** * Formats value of given node: for multiline text we should precede each * line with `| ` with one-level deep indent * @param {Node} node * @param {Profile} profile * @return {String|null} */ function formatNodeValue$2(node, profile) { if (node.value != null && reNl$2.test(node.value)) { const indent = profile.indent(1); return splitByLines$1(node.value).map(line => `${indent}| ${line}`).join('\n'); } return node.value; } const supportedSyntaxed = { html, haml, slim, pug }; /** * Outputs given parsed abbreviation in specified syntax * @param {Node} tree Parsed abbreviation tree * @param {Profile} profile Output profile * @param {String} [syntax] Output syntax. If not given, `html` syntax is used * @param {Function} options.field A function to output field/tabstop for * host editor. This function takes two arguments: `index` and `placeholder` and * should return a string that represents tabstop in host editor. By default * only a placeholder is returned * @example * { * field(index, placeholder) { * // return field in TextMate-style, e.g. ${1} or ${2:foo} * return `\${${index}${placeholder ? ':' + placeholder : ''}}`; * } * } * @return {String} */ var index$3 = function(tree, profile, syntax, options) { if (typeof syntax === 'object') { options = syntax; syntax = null; } if (!supports(syntax)) { // fallback to HTML if given syntax is not supported syntax = 'html'; } return supportedSyntaxed[syntax](tree, profile, options); }; /** * Check if given syntax is supported * @param {String} syntax * @return {Boolean} */ function supports(syntax) { return !!syntax && syntax in supportedSyntaxed; } /** * Expands given abbreviation into code * @param {String|Node} abbr Abbreviation to parse or already parsed abbreviation * @param {Object} options * @return {String} */ function expand(abbr, options) { options = options || {}; if (typeof abbr === 'string') { abbr = parse$3(abbr, options); } return index$3(abbr, options.profile, options.syntax, options.format); } /** * Parses given Emmet abbreviation into a final abbreviation tree with all * required transformations applied * @param {String} Abbreviation to parse * @param {Object} options * @return {Node} */ function parse$3(abbr, options) { return index(abbr) .use(index$1, options.snippets) .use(replaceVariables, options.variables) .use(index$2, options.text, options.addons); } /** * A wrapper for holding CSS value */ class CSSValue { constructor() { this.type = 'css-value'; this.value = []; } get size() { return this.value.length; } add(value) { this.value.push(value); } has(value) { return this.value.indexOf(value) !== -1; } toString() { return this.value.join(' '); } } const HASH$1 = 35; // # const DOT$2 = 46; // . /** * Consumes a color token from given string * @param {StreamReader} stream * @return {Color} Returns consumend color object, `undefined` otherwise */ var consumeColor = function(stream) { // supported color variations: // #abc → #aabbccc // #0 → #000000 // #fff.5 → rgba(255, 255, 255, 0.5) // #t → transparent if (stream.peek() === HASH$1) { stream.start = stream.pos; stream.next(); stream.eat(116) /* t */ || stream.eatWhile(isHex); const base = stream.current(); // a hex color can be followed by `.num` alpha value stream.start = stream.pos; if (stream.eat(DOT$2) && !stream.eatWhile(isNumber)) { throw stream.error('Unexpected character for alpha value of color'); } return new Color(base, stream.current()); } }; class Color { constructor(value, alpha) { this.type = 'color'; this.raw = value; this.alpha = Number(alpha != null && alpha !== '' ? alpha : 1); value = value.slice(1); // remove # let r = 0, g = 0, b = 0; if (value === 't') { this.alpha = 0; } else { switch (value.length) { case 0: break; case 1: r = g = b = value + value; break; case 2: r = g = b = value; break; case 3: r = value[0] + value[0]; g = value[1] + value[1]; b = value[2] + value[2]; break; default: value += value; r = value.slice(0, 2); g = value.slice(2, 4); b = value.slice(4, 6); } } this.r = parseInt(r, 16); this.g = parseInt(g, 16); this.b = parseInt(b, 16); } /** * Output current color as hex value * @param {Boolean} shor Produce short value (e.g. #fff instead of #ffffff), if possible * @return {String} */ toHex(short) { const fn = (short && isShortHex(this.r) && isShortHex(this.g) && isShortHex(this.b)) ? toShortHex : toHex; return '#' + fn(this.r) + fn(this.g) + fn(this.b); } /** * Output current color as `rgba?(...)` CSS color * @return {String} */ toRGB() { const values = [this.r, this.g, this.b]; if (this.alpha !== 1) { values.push(this.alpha.toFixed(8).replace(/\.?0+$/, '')); } return `${values.length === 3 ? 'rgb' : 'rgba'}(${values.join(', ')})`; } toString(short) { if (!this.r && !this.g && !this.b && !this.alpha) { return 'transparent'; } return this.alpha === 1 ? this.toHex(short) : this.toRGB(); } } /** * Check if given code is a hex value (/0-9a-f/) * @param {Number} code * @return {Boolean} */ function isHex(code) { return isNumber(code) || isAlpha(code, 65, 70); // A-F } function isShortHex(hex) { return !(hex % 17); } function toShortHex(num) { return (num >> 4).toString(16); } function toHex(num) { return pad$1(num.toString(16), 2); } function pad$1(value, len) { while (value.length < len) { value = '0' + value; } return value; } /** * @param {Number} code * @return {Boolean} */ function isAlphaNumericWord(code) { return isNumber(code) || isAlphaWord(code); } /** * @param {Number} code * @return {Boolean} */ function isAlphaWord(code) { return code === 95 /* _ */ || isAlpha(code); } const PERCENT = 37; // % const DOT$1$1 = 46; // . const DASH$1 = 45; // - /** * Consumes numeric CSS value (number with optional unit) from current stream, * if possible * @param {StreamReader} stream * @return {NumericValue} */ var consumeNumericValue = function(stream) { stream.start = stream.pos; if (eatNumber(stream)) { const num = stream.current(); stream.start = stream.pos; // eat unit, which can be a % or alpha word stream.eat(PERCENT) || stream.eatWhile(isAlphaWord); return new NumericValue(num, stream.current()); } }; /** * A numeric CSS value with optional unit */ class NumericValue { constructor(value, unit) { this.type = 'numeric'; this.value = Number(value); this.unit = unit || ''; } toString() { return `${this.value}${this.unit}`; } } /** * Eats number value from given stream * @param {StreamReader} stream * @return {Boolean} Returns `true` if number was consumed */ function eatNumber(stream) { const start = stream.pos; const negative = stream.eat(DASH$1); let hadDot = false, consumed = false, code; while (!stream.eof()) { code = stream.peek(); // either a second dot or not a number: stop parsing if (code === DOT$1$1 ? hadDot : !isNumber(code)) { break; } consumed = true; if (code === DOT$1$1) { hadDot = true; } stream.next(); } if (negative && !consumed) { // edge case: consumed dash only, bail out stream.pos = start; } return start !== stream.pos; } const DOLLAR$1 = 36; // $ const DASH$2 = 45; // - const AT$1 = 64; // @ /** * Consumes a keyword: either a variable (a word that starts with $ or @) or CSS * keyword or shorthand * @param {StreamReader} stream * @param {Boolean} [short] Use short notation for consuming value. * The difference between “short” and “full” notation is that first one uses * alpha characters only and used for extracting keywords from abbreviation, * while “full” notation also supports numbers and dashes * @return {String} Consumed variable */ var consumeKeyword = function(stream, short) { stream.start = stream.pos; if (stream.eat(DOLLAR$1) || stream.eat(AT$1)) { // SCSS or LESS variable stream.eatWhile(isVariableName); } else if (short) { stream.eatWhile(isAlphaWord); } else { stream.eatWhile(isKeyword); } return stream.start !== stream.pos ? new Keyword(stream.current()) : null; }; class Keyword { constructor(value) { this.type = 'keyword'; this.value = value; } toString() { return this.value; } } function isKeyword(code) { return isAlphaNumericWord(code) || code === DASH$2; } function isVariableName(code) { return code === 45 /* - */ || isAlphaNumericWord(code); } const opt$1 = { throws: true }; /** * Consumes 'single' or "double"-quoted string from given string, if possible * @param {StreamReader} stream * @return {String} */ var consumeQuoted$1 = function(stream) { if (eatQuoted(stream, opt$1)) { return new QuotedString(stream.current()); } }; class QuotedString { constructor(value) { this.type = 'string'; this.value = value; } toString() { return this.value; } } const LBRACE = 40; // ( const RBRACE = 41; // ) const COMMA = 44; // , /** * Consumes arguments from given string. * Arguments are comma-separated list of CSS values inside round braces, e.g. * `(1, a2, 'a3')`. Nested lists and quoted strings are supported * @param {StreamReader} stream * @return {Array} Array of arguments, `null` if arguments cannot be consumed */ function consumeArgumentList(stream) { if (!stream.eat(LBRACE)) { // not an argument list return null; } let level = 1, code, arg; const argsList = []; while (!stream.eof()) { if (arg = consumeArgument(stream)) { argsList.push(arg); } else { // didn’t consumed argument, expect argument separator or end-of-arguments stream.eatWhile(isWhiteSpace); if (stream.eat(RBRACE)) { // end of arguments list break; } if (!stream.eat(COMMA)) { throw stream.error('Expected , or )'); } } } return argsList; } /** * Consumes a single argument. An argument is a `CSSValue`, e.g. it could be * a space-separated string of value * @param {StreamReader} stream * @return {CSSValue} */ function consumeArgument(stream) { const result = new CSSValue(); let value; while (!stream.eof()) { stream.eatWhile(isWhiteSpace); value = consumeNumericValue(stream) || consumeColor(stream) || consumeQuoted$1(stream) || consumeKeywordOrFunction(stream); if (!value) { break; } result.add(value); } return result.size ? result : null; } /** * Consumes either function call like `foo()` or keyword like `foo` * @param {StreamReader} stream * @return {Keyword|FunctionCall} */ function consumeKeywordOrFunction(stream) { const kw = consumeKeyword(stream); if (kw) { const args = consumeArgumentList(stream); return args ? new FunctionCall(kw.toString(), args) : kw; } } class FunctionCall { /** * @param {String} name Function name * @param {Array} args Function arguments */ constructor(name, args) { this.type = 'function'; this.name = name; this.args = args || []; } toString() { return `${this.name}(${this.args.join(', ')})`; } } const EXCL$1 = 33; // ! const DOLLAR$2 = 36; // $ const PLUS = 43; // + const DASH = 45; // - const COLON$1 = 58; // : const AT = 64; // @ /** * Parses given Emmet CSS abbreviation and returns it as parsed Node tree * @param {String} abbr * @return {Node} */ var index$4 = function(abbr) { const root = new Node(); const stream = new StreamReader(abbr); while (!stream.eof()) { let node = new Node(consumeIdent(stream)); node.value = consumeValue(stream); const args = consumeArgumentList(stream); if (args) { // technically, arguments in CSS are anonymous Emmet Node attributes, // but since Emmet can support only one anonymous, `null`-name // attribute (for good reasons), we’ll use argument index as name for (let i = 0; i < args.length; i++) { node.setAttribute(String(i), args[i]); } } // Consume `!important` modifier at the end of expression if (stream.eat(EXCL$1)) { node.value.add('!'); } root.appendChild(node); // CSS abbreviations cannot be nested, only listed if (!stream.eat(PLUS)) { break; } } if (!stream.eof()) { throw stream.error('Unexpected character'); } return root; }; /** * Consumes CSS property identifier from given stream * @param {StreamReader} stream * @return {String} */ function consumeIdent(stream) { stream.start = stream.pos; stream.eatWhile(isIdentPrefix); stream.eatWhile(isIdent); return stream.start !== stream.pos ? stream.current() : null; } /** * Consumes embedded value from Emmet CSS abbreviation stream * @param {StreamReader} stream * @return {CSSValue} */ function consumeValue(stream) { const values = new CSSValue(); let value; while (!stream.eof()) { // use colon as value separator stream.eat(COLON$1); if (value = consumeNumericValue(stream) || consumeColor(stream)) { // edge case: a dash after unit-less numeric value or color should // be treated as value separator, not negative sign if (!value.unit) { stream.eat(DASH); } } else { stream.eat(DASH); value = consumeKeyword(stream, true); } if (!value) { break; } values.add(value); } return values; } /** * @param {Number} code * @return {Boolean} */ function isIdent(code) { return isAlphaWord(code); } /** * @param {Number} code * @return {Boolean} */ function isIdentPrefix(code) { return code === AT || code === DOLLAR$2 || code === EXCL$1; } const DASH$3 = 45; // - /** * Calculates fuzzy match score of how close `abbr` matches given `string`. * @param {String} abbr Abbreviation to score * @param {String} string String to match * @param {Number} [fuzziness] Fuzzy factor * @return {Number} Match score */ var stringScore = function(abbr, string) { abbr = abbr.toLowerCase(); string = string.toLowerCase(); if (abbr === string) { return 1; } // a string MUST start with the same character as abbreviation if (!string || abbr.charCodeAt(0) !== string.charCodeAt(0)) { return 0; } const abbrLength = abbr.length; const stringLength = string.length; let i = 1, j = 1, score = stringLength; let ch1, ch2, found, acronym; while (i < abbrLength) { ch1 = abbr.charCodeAt(i); found = false; acronym = false; while (j < stringLength) { ch2 = string.charCodeAt(j); if (ch1 === ch2) { found = true; score += (stringLength - j) * (acronym ? 2 : 1); break; } // add acronym bonus for exactly next match after unmatched `-` acronym = ch2 === DASH$3; j++; } if (!found) { break; } i++; } return score && score * (i / abbrLength) / sum(stringLength); }; /** * Calculates sum of first `n` natural numbers, e.g. 1+2+3+...n * @param {Number} n * @return {Number} */ function sum(n) { return n * (n + 1) / 2; } const reProperty = /^([a-z\-]+)(?:\s*:\s*([^\n\r]+))?$/; const DASH$1$1 = 45; // - /** * Creates a special structure for resolving CSS properties from plain CSS * snippets. * Almost all CSS snippets are aliases for real CSS properties with available * value variants, optionally separated by `|`. Most values are keywords that * can be fuzzy-resolved as well. Some CSS properties are shorthands for other, * more specific properties, like `border` and `border-style`. For such cases * keywords from more specific properties should be available in shorthands too. * @param {Snippet[]} snippets * @return {CSSSnippet[]} */ var cssSnippets = function(snippets) { return nest( snippets.map(snippet => new CSSSnippet(snippet.key, snippet.value)) ); }; class CSSSnippet { constructor(key, value) { this.key = key; this.value = value; this.property = null; // detect if given snippet is a property const m = value && value.match(reProperty); if (m) { this.property = m[1]; this.value = m[2]; } this.dependencies = []; } addDependency(dep) { this.dependencies.push(dep); } get defaulValue() { return this.value != null ? splitValue(this.value)[0] : null; } /** * Returns list of unique keywords for current CSS snippet and its dependencies * @return {String[]} */ keywords() { const stack = []; const keywords = new Set(); let i = 0, item, candidates; if (this.property) { // scan valid CSS-properties only stack.push(this); } while (i < stack.length) { // NB Keep items in stack instead of push/pop to avoid possible // circular references item = stack[i++]; if (item.value) { candidates = splitValue(item.value).filter(isKeyword$1); // extract possible keywords from snippet value for (let j = 0; j < candidates.length; j++) { keywords.add(candidates[j].trim()); } // add dependencies into scan stack for (let j = 0, deps = item.dependencies; j < deps.length; j++) { if (stack.indexOf(deps[j]) === -1) { stack.push(deps[j]); } } } } return Array.from(keywords); } } /** * Nests more specific CSS properties into shorthand ones, e.g. * background-position-x -> background-position -> background * @param {CSSSnippet[]} snippets * @return {CSSSnippet[]} */ function nest(snippets) { snippets = snippets.sort(snippetsSort); const stack = []; // For sorted list of CSS properties, create dependency graph where each // shorthand property contains its more specific one, e.g. // backgound -> background-position -> background-position-x for (let i = 0, cur, prev; i < snippets.length; i++) { cur = snippets[i]; if (!cur.property) { // not a CSS property, skip it continue; } // Check if current property belongs to one from parent stack. // Since `snippets` array is sorted, items are perfectly aligned // from shorthands to more specific variants while (stack.length) { prev = stack[stack.length - 1]; if (cur.property.indexOf(prev.property) === 0 && cur.property.charCodeAt(prev.property.length) === DASH$1$1) { prev.addDependency(cur); stack.push(cur); break; } stack.pop(); } if (!stack.length) { stack.push(cur); } } return snippets; } /** * A sorting function for array of snippets * @param {CSSSnippet} a * @param {CSSSnippet} b * @return {Number} */ function snippetsSort(a, b) { if (a.key === b.key) { return 0; } return a.key < b.key ? -1 : 1; } /** * Check if given string is a keyword candidate * @param {String} str * @return {Boolean} */ function isKeyword$1(str) { return /^\s*[\w\-]+/.test(str); } function splitValue(value) { return String(value).split('|'); } const globalKeywords = ['auto', 'inherit', 'unset']; const unitlessProperties = [ 'z-index', 'line-height', 'opacity', 'font-weight', 'zoom', 'flex', 'flex-grow', 'flex-shrink' ]; const defaultOptions$3 = { intUnit: 'px', floatUnit: 'em', unitAliases: { e :'em', p: '%', x: 'ex', r: 'rem' }, fuzzySearchMinScore: 0 }; /** * For every node in given `tree`, finds matching snippet from `registry` and * updates node with snippet data. * * This resolver uses fuzzy matching for searching matched snippets and their * keyword values. */ var index$5 = function(tree, registry, options) { const snippets = convertToCSSSnippets(registry); options = { intUnit: (options && options.intUnit) || defaultOptions$3.intUnit, floatUnit: (options && options.floatUnit) || defaultOptions$3.floatUnit, unitAliases: Object.assign({}, defaultOptions$3.unitAliases, options && options.unitAliases), fuzzySearchMinScore: (options && options.fuzzySearchMinScore) || defaultOptions$3.fuzzySearchMinScore }; tree.walk(node => resolveNode$1(node, snippets, options)); return tree; }; function convertToCSSSnippets(registry) { return cssSnippets(registry.all({type: 'string'})) } /** * Resolves given node: finds matched CSS snippets using fuzzy match and resolves * keyword aliases from node value * @param {Node} node * @param {CSSSnippet[]} snippets * @param {Object} options * @return {Node} */ function resolveNode$1(node, snippets, options) { const snippet = findBestMatch(node.name, snippets, 'key', options.fuzzySearchMinScore); if (!snippet) { // Edge case: `!important` snippet return node.name === '!' ? setNodeAsText(node, '!important') : node; } return snippet.property ? resolveAsProperty(node, snippet, options) : resolveAsSnippet(node, snippet); } /** * Resolves given parsed abbreviation node as CSS propery * @param {Node} node * @param {CSSSnippet} snippet * @param {Object} formatOptions * @return {Node} */ function resolveAsProperty(node, snippet, formatOptions) { const abbr = node.name; node.name = snippet.property; if (node.value && typeof node.value === 'object') { // resolve keyword shortcuts const keywords = snippet.keywords(); if (!node.value.size) { // no value defined, try to resolve unmatched part as a keyword alias let kw = findBestMatch(getUnmatchedPart(abbr, snippet.key), keywords); if (!kw) { // no matching value, try to get default one kw = snippet.defaulValue; if (kw && kw.indexOf('${') === -1) { // Quick and dirty test for existing field. If not, wrap // default value in a field kw = `\${1:${kw}}`; } } if (kw) { node.value.add(kw); } } else { // replace keyword aliases in current node value for (let i = 0, token; i < node.value.value.length; i++) { token = node.value.value[i]; if (token === '!') { token = `${!i ? '${1} ' : ''}!important`; } else if (isKeyword$2(token)) { token = findBestMatch(token.value, keywords) || findBestMatch(token.value, globalKeywords) || token; } else if (isNumericValue(token)) { token = resolveNumericValue(node.name, token, formatOptions); } node.value.value[i] = token; } } } return node; } /** * Resolves given parsed abbreviation node as a snippet: a plain code chunk * @param {Node} node * @param {CSSSnippet} snippet * @return {Node} */ function resolveAsSnippet(node, snippet) { return setNodeAsText(node, snippet.value); } /** * Sets given parsed abbreviation node as a text snippet * @param {Node} node * @param {String} text * @return {Node} */ function setNodeAsText(node, text) { node.name = null; node.value = text; return node; } /** * Finds best matching item from `items` array * @param {String} abbr Abbreviation to match * @param {Array} items List of items for match * @param {String} [key] If `items` is a list of objects, use `key` as object * property to test against * @param {Number} fuzzySearchMinScore The minimum score the best matched item should have to be a valid match. * @return {*} */ function findBestMatch(abbr, items, key, fuzzySearchMinScore) { if (!abbr) { return null; } let matchedItem = null; let maxScore = 0; fuzzySearchMinScore = fuzzySearchMinScore || 0; for (let i = 0, item; i < items.length; i++) { item = items[i]; const score = stringScore(abbr, getScoringPart(item, key)); if (score === 1) { // direct hit, no need to look further return item; } if (score && score >= maxScore) { maxScore = score; matchedItem = item; } } return maxScore >= fuzzySearchMinScore ? matchedItem : null; } function getScoringPart(item, key) { const value = item && typeof item === 'object' ? item[key] : item; const m = (value || '').match(/^[\w-@]+/); return m ? m[0] : value; } /** * Returns a part of `abbr` that wasn’t directly matched agains `string`. * For example, if abbreviation `poas` is matched against `position`, the unmatched part will be `as` * since `a` wasn’t found in string stream * @param {String} abbr * @param {String} string * @return {String} */ function getUnmatchedPart(abbr, string) { for (let i = 0, lastPos = 0; i < abbr.length; i++) { lastPos = string.indexOf(abbr[i], lastPos); if (lastPos === -1) { return abbr.slice(i); } lastPos++; } return ''; } /** * Check if given CSS value token is a keyword * @param {*} token * @return {Boolean} */ function isKeyword$2(token) { return tokenTypeOf(token, 'keyword'); } /** * Check if given CSS value token is a numeric value * @param {*} token * @return {Boolean} */ function isNumericValue(token) { return tokenTypeOf(token, 'numeric'); } function tokenTypeOf(token, type) { return token && typeof token === 'object' && token.type === type; } /** * Resolves numeric value for given CSS property * @param {String} property CSS property name * @param {NumericValue} token CSS numeric value token * @param {Object} formatOptions Formatting options for units * @return {NumericValue} */ function resolveNumericValue(property, token, formatOptions) { if (token.unit) { token.unit = formatOptions.unitAliases[token.unit] || token.unit; } else if (token.value !== 0 && unitlessProperties.indexOf(property) === -1) { // use `px` for integers, `em` for floats // NB: num|0 is a quick alternative to Math.round(0) token.unit = token.value === (token.value|0) ? formatOptions.intUnit : formatOptions.floatUnit; } return token; } const defaultOptions$4 = { shortHex: true, format: { between: ': ', after: ';' } }; /** * Renders given parsed Emmet CSS abbreviation as CSS-like * stylesheet, formatted according to `profile` options * @param {Node} tree Parsed Emmet abbreviation * @param {Profile} profile Output profile * @param {Object} [options] Additional formatter options * @return {String} */ function css(tree, profile, options) { options = Object.assign({}, defaultOptions$4, options); return render(tree, options.field, outNode => { const node = outNode.node; let value = String(node.value || ''); if (node.attributes.length) { const fieldValues = node.attributes.map(attr => stringifyAttribute(attr, options)); value = injectFields(value, fieldValues); } outNode.open = node.name && profile.name(node.name); outNode.afterOpen = options.format.between; outNode.text = outNode.renderFields(value || null); if (outNode.open && (!outNode.text || !outNode.text.endsWith(';'))) { outNode.afterText = options.format.after; } if (profile.get('format')) { outNode.newline = '\n'; if (tree.lastChild !== node) { outNode.afterText += outNode.newline; } } return outNode; }); } /** * Injects given field values at each field of given string * @param {String} string * @param {String[]} attributes * @return {FieldString} */ function injectFields(string, values) { const fieldsModel = parse$2(string); const fieldsAmount = fieldsModel.fields.length; if (fieldsAmount) { values = values.slice(); if (values.length > fieldsAmount) { // More values that output fields: collapse rest values into // a single token values = values.slice(0, fieldsAmount - 1) .concat(values.slice(fieldsAmount - 1).join(', ')); } while (values.length) { const value = values.shift(); const field = fieldsModel.fields.shift(); const delta = value.length - field.length; fieldsModel.string = fieldsModel.string.slice(0, field.location) + value + fieldsModel.string.slice(field.location + field.length); // Update location of the rest fields in string for (let i = 0, il = fieldsModel.fields.length; i < il; i++) { fieldsModel.fields[i].location += delta; } } } return fieldsModel; } function stringifyAttribute(attr, options) { if (attr.value && typeof attr.value === 'object' && attr.value.type === 'css-value') { return attr.value.value .map(token => { if (token && typeof token === 'object') { return token.type === 'color' ? token.toString(options.shortHex) : token.toString(); } return String(token); }) .join(' '); } return attr.value != null ? String(attr.value) : ''; } const syntaxFormat = { css: { between: ': ', after: ';' }, scss: 'css', less: 'css', sass: { between: ': ', after: '' }, stylus: { between: ' ', after: '' } }; /** * Outputs given parsed abbreviation in specified stylesheet syntax * @param {Node} tree Parsed abbreviation tree * @param {Profile} profile Output profile * @param {String} [syntax] Output syntax. If not given, `css` syntax is used * @param {Function} options.field A function to output field/tabstop for * host editor. This function takes two arguments: `index` and `placeholder` and * should return a string that represents tabstop in host editor. By default * only a placeholder is returned * @example * { * field(index, placeholder) { * // return field in TextMate-style, e.g. ${1} or ${2:foo} * return `\${${index}${placeholder ? ':' + placeholder : ''}}`; * } * } * @return {String} */ var index$6 = function(tree, profile, syntax, options) { if (typeof syntax === 'object') { options = syntax; syntax = null; } if (!supports$1(syntax)) { // fallback to CSS if given syntax is not supported syntax = 'css'; } options = Object.assign({}, options, { format: getFormat(syntax, options) }); // CSS abbreviations doesn’t support nesting so simply // output root node children return css(tree, profile, options); }; /** * Check if given syntax is supported * @param {String} syntax * @return {Boolean} */ function supports$1(syntax) { return !!syntax && syntax in syntaxFormat; } /** * Returns formatter object for given syntax * @param {String} syntax * @param {Object} [options] * @return {Object} Formatter object as defined in `syntaxFormat` */ function getFormat(syntax, options) { let format = syntaxFormat[syntax]; if (typeof format === 'string') { format = syntaxFormat[format]; } return Object.assign({}, format, options && options.stylesheet); } /** * Expands given abbreviation into code * @param {String|Node} abbr Abbreviation to parse or already parsed abbreviation * @param {Object} options * @return {String} */ function expand$1(abbr, options) { options = options || {}; if (typeof abbr === 'string') { abbr = parse$4(abbr, options); } return index$6(abbr, options.profile, options.syntax, options.format); } /** * Parses given Emmet abbreviation into a final abbreviation tree with all * required transformations applied * @param {String|Node} abbr Abbreviation to parse or already parsed abbreviation * @param {Object} options * @return {Node} */ function parse$4(abbr, options) { if (typeof abbr === 'string') { abbr = index$4(abbr); } return abbr.use(index$5, options.snippets, options.format ? options.format.stylesheet : {}); } var html$1 = { "a": "a[href]", "a:blank": "a[href='http://${0}' target='_blank' rel='noopener noreferrer']", "a:link": "a[href='http://${0}']", "a:mail": "a[href='mailto:${0}']", "a:tel": "a[href='tel:+${0}']", "abbr": "abbr[title]", "acr|acronym": "acronym[title]", "base": "base[href]/", "basefont": "basefont/", "br": "br/", "frame": "frame/", "hr": "hr/", "bdo": "bdo[dir]", "bdo:r": "bdo[dir=rtl]", "bdo:l": "bdo[dir=ltr]", "col": "col/", "link": "link[rel=stylesheet href]/", "link:css": "link[href='${1:style}.css']", "link:print": "link[href='${1:print}.css' media=print]", "link:favicon": "link[rel='shortcut icon' type=image/x-icon href='${1:favicon.ico}']", "link:mf|link:manifest": "link[rel='manifest' href='${1:manifest.json}']", "link:touch": "link[rel=apple-touch-icon href='${1:favicon.png}']", "link:rss": "link[rel=alternate type=application/rss+xml title=RSS href='${1:rss.xml}']", "link:atom": "link[rel=alternate type=application/atom+xml title=Atom href='${1:atom.xml}']", "link:im|link:import": "link[rel=import href='${1:component}.html']", "meta": "meta/", "meta:utf": "meta[http-equiv=Content-Type content='text/html;charset=UTF-8']", "meta:vp": "meta[name=viewport content='width=${1:device-width}, initial-scale=${2:1.0}']", "meta:compat": "meta[http-equiv=X-UA-Compatible content='${1:IE=7}']", "meta:edge": "meta:compat[content='${1:ie=edge}']", "meta:redirect": "meta[http-equiv=refresh content='0; url=${1:http://example.com}']", "meta:kw": "meta[name=keywords content]", "meta:desc": "meta[name=description content]", "style": "style", "script": "script", "script:src": "script[src]", "img": "img[src alt]/", "img:s|img:srcset": "img[srcset src alt]", "img:z|img:sizes": "img[sizes srcset src alt]", "picture": "picture", "src|source": "source/", "src:sc|source:src": "source[src type]", "src:s|source:srcset": "source[srcset]", "src:t|source:type": "source[srcset type='${1:image/}']", "src:z|source:sizes": "source[sizes srcset]", "src:m|source:media": "source[media='(${1:min-width: })' srcset]", "src:mt|source:media:type": "source:media[type='${2:image/}']", "src:mz|source:media:sizes": "source:media[sizes srcset]", "src:zt|source:sizes:type": "source[sizes srcset type='${1:image/}']", "iframe": "iframe[src frameborder=0]", "embed": "embed[src type]/", "object": "object[data type]", "param": "param[name value]/", "map": "map[name]", "area": "area[shape coords href alt]/", "area:d": "area[shape=default]", "area:c": "area[shape=circle]", "area:r": "area[shape=rect]", "area:p": "area[shape=poly]", "form": "form[action]", "form:get": "form[method=get]", "form:post": "form[method=post]", "label": "label[for]", "input": "input[type=${1:text}]/", "inp": "input[name=${1} id=${1}]", "input:h|input:hidden": "input[type=hidden name]", "input:t|input:text": "inp[type=text]", "input:search": "inp[type=search]", "input:email": "inp[type=email]", "input:url": "inp[type=url]", "input:p|input:password": "inp[type=password]", "input:datetime": "inp[type=datetime]", "input:date": "inp[type=date]", "input:datetime-local": "inp[type=datetime-local]", "input:month": "inp[type=month]", "input:week": "inp[type=week]", "input:time": "inp[type=time]", "input:tel": "inp[type=tel]", "input:number": "inp[type=number]", "input:color": "inp[type=color]", "input:c|input:checkbox": "inp[type=checkbox]", "input:r|input:radio": "inp[type=radio]", "input:range": "inp[type=range]", "input:f|input:file": "inp[type=file]", "input:s|input:submit": "input[type=submit value]", "input:i|input:image": "input[type=image src alt]", "input:b|input:button": "input[type=button value]", "input:reset": "input:button[type=reset]", "isindex": "isindex/", "select": "select[name=${1} id=${1}]", "select:d|select:disabled": "select[disabled.]", "opt|option": "option[value]", "textarea": "textarea[name=${1} id=${1} cols=${2:30} rows=${3:10}]", "marquee": "marquee[behavior direction]", "menu:c|menu:context": "menu[type=context]", "menu:t|menu:toolbar": "menu[type=toolbar]", "video": "video[src]", "audio": "audio[src]", "html:xml": "html[xmlns=http://www.w3.org/1999/xhtml]", "keygen": "keygen/", "command": "command/", "btn:s|button:s|button:submit" : "button[type=submit]", "btn:r|button:r|button:reset" : "button[type=reset]", "btn:d|button:d|button:disabled" : "button[disabled.]", "fst:d|fset:d|fieldset:d|fieldset:disabled" : "fieldset[disabled.]", "bq": "blockquote", "fig": "figure", "figc": "figcaption", "pic": "picture", "ifr": "iframe", "emb": "embed", "obj": "object", "cap": "caption", "colg": "colgroup", "fst": "fieldset", "btn": "button", "optg": "optgroup", "tarea": "textarea", "leg": "legend", "sect": "section", "art": "article", "hdr": "header", "ftr": "footer", "adr": "address", "dlg": "dialog", "str": "strong", "prog": "progress", "mn": "main", "tem": "template", "fset": "fieldset", "datag": "datagrid", "datal": "datalist", "kg": "keygen", "out": "output", "det": "details", "cmd": "command", "ri:d|ri:dpr": "img:s", "ri:v|ri:viewport": "img:z", "ri:a|ri:art": "pic>src:m+img", "ri:t|ri:type": "pic>src:t+img", "!!!": "{}", "doc": "html[lang=${lang}]>(head>meta[charset=${charset}]+meta:vp+title{${1:Document}})+body", "!|html:5": "!!!+doc", "c": "{}", "cc:ie": "{}", "cc:noie": "{${0}}" }; var css$1 = { "@f": "@font-face {\n\tfont-family: ${1};\n\tsrc: url(${1});\n}", "@ff": "@font-face {\n\tfont-family: '${1:FontName}';\n\tsrc: url('${2:FileName}.eot');\n\tsrc: url('${2:FileName}.eot?#iefix') format('embedded-opentype'),\n\t\t url('${2:FileName}.woff') format('woff'),\n\t\t url('${2:FileName}.ttf') format('truetype'),\n\t\t url('${2:FileName}.svg#${1:FontName}') format('svg');\n\tfont-style: ${3:normal};\n\tfont-weight: ${4:normal};\n}", "@i|@import": "@import url(${0});", "@kf": "@keyframes ${1:identifier} {\n\t${2}\n}", "@m|@media": "@media ${1:screen} {\n\t${0}\n}", "ac": "align-content:start|end|flex-start|flex-end|center|space-between|space-around|stretch|space-evenly", "ai": "align-items:start|end|flex-start|flex-end|center|baseline|stretch", "anim": "animation:${1:name} ${2:duration} ${3:timing-function} ${4:delay} ${5:iteration-count} ${6:direction} ${7:fill-mode}", "animdel": "animation-delay:time", "animdir": "animation-direction:normal|reverse|alternate|alternate-reverse", "animdur": "animation-duration:${1:0}s", "animfm": "animation-fill-mode:both|forwards|backwards", "animic": "animation-iteration-count:1|infinite", "animn": "animation-name", "animps": "animation-play-state:running|paused", "animtf": "animation-timing-function:linear|ease|ease-in|ease-out|ease-in-out|cubic-bezier(${1:0.1}, ${2:0.7}, ${3:1.0}, ${3:0.1})", "ap": "appearance:none", "as": "align-self:start|end|auto|flex-start|flex-end|center|baseline|stretch", "b": "bottom", "bd": "border:${1:1px} ${2:solid} ${3:#000}", "bdb": "border-bottom:${1:1px} ${2:solid} ${3:#000}", "bdbc": "border-bottom-color:${1:#000}", "bdbi": "border-bottom-image:url(${0})", "bdbk": "border-break:close", "bdbli": "border-bottom-left-image:url(${0})|continue", "bdblrs": "border-bottom-left-radius", "bdbri": "border-bottom-right-image:url(${0})|continue", "bdbrrs": "border-bottom-right-radius", "bdbs": "border-bottom-style", "bdbw": "border-bottom-width", "bdc": "border-color:${1:#000}", "bdci": "border-corner-image:url(${0})|continue", "bdcl": "border-collapse:collapse|separate", "bdf": "border-fit:repeat|clip|scale|stretch|overwrite|overflow|space", "bdi": "border-image:url(${0})", "bdl": "border-left:${1:1px} ${2:solid} ${3:#000}", "bdlc": "border-left-color:${1:#000}", "bdlen": "border-length", "bdli": "border-left-image:url(${0})", "bdls": "border-left-style", "bdlw": "border-left-width", "bdr": "border-right:${1:1px} ${2:solid} ${3:#000}", "bdrc": "border-right-color:${1:#000}", "bdri": "border-right-image:url(${0})", "bdrs": "border-radius", "bdrst": "border-right-style", "bdrw": "border-right-width", "bds": "border-style:none|hidden|dotted|dashed|solid|double|dot-dash|dot-dot-dash|wave|groove|ridge|inset|outset", "bdsp": "border-spacing", "bdt": "border-top:${1:1px} ${2:solid} ${3:#000}", "bdtc": "border-top-color:${1:#000}", "bdti": "border-top-image:url(${0})", "bdtli": "border-top-left-image:url(${0})|continue", "bdtlrs": "border-top-left-radius", "bdtri": "border-top-right-image:url(${0})|continue", "bdtrrs": "border-top-right-radius", "bdts": "border-top-style", "bdtw": "border-top-width", "bdw": "border-width", "bfv": "backface-visibility:hidden|visible", "bg": "background:${1:#000}", "bga": "background-attachment:fixed|scroll", "bgbk": "background-break:bounding-box|each-box|continuous", "bgc": "background-color:#${1:fff}", "bgcp": "background-clip:padding-box|border-box|content-box|no-clip", "bgi": "background-image:url(${0})", "bgo": "background-origin:padding-box|border-box|content-box", "bgp": "background-position:${1:0} ${2:0}", "bgpx": "background-position-x", "bgpy": "background-position-y", "bgr": "background-repeat:no-repeat|repeat-x|repeat-y|space|round", "bgsz": "background-size:contain|cover", "bxsh": "box-shadow:${1:inset }${2:hoff} ${3:voff} ${4:blur} ${5:#000}|none", "bxsz": "box-sizing:border-box|content-box|border-box", "c": "color:${1:#000}", "cl": "clear:both|left|right|none", "cm": "/* ${0} */", "cnt": "content:'${0}'|normal|open-quote|no-open-quote|close-quote|no-close-quote|attr(${0})|counter(${0})|counters(${0})", "coi": "counter-increment", "colm": "columns", "colmc": "column-count", "colmf": "column-fill", "colmg": "column-gap", "colmr": "column-rule", "colmrc": "column-rule-color", "colmrs": "column-rule-style", "colmrw": "column-rule-width", "colms": "column-span", "colmw": "column-width", "cor": "counter-reset", "cp": "clip:auto|rect(${1:top} ${2:right} ${3:bottom} ${4:left})", "cps": "caption-side:top|bottom", "cur": "cursor:pointer|auto|default|crosshair|hand|help|move|pointer|text", "d": "display:grid|inline-grid|subgrid|block|none|flex|inline-flex|inline|inline-block|list-item|run-in|compact|table|inline-table|table-caption|table-column|table-column-group|table-header-group|table-footer-group|table-row|table-row-group|table-cell|ruby|ruby-base|ruby-base-group|ruby-text|ruby-text-group", "ec": "empty-cells:show|hide", "f": "font:${1:1em} ${2:sans-serif}", "fd": "font-display:auto|block|swap|fallback|optional", "fef": "font-effect:none|engrave|emboss|outline", "fem": "font-emphasize", "femp": "font-emphasize-position:before|after", "fems": "font-emphasize-style:none|accent|dot|circle|disc", "ff": "font-family:serif|sans-serif|cursive|fantasy|monospace", "fft": "font-family:\"Times New Roman\", Times, Baskerville, Georgia, serif", "ffa": "font-family:Arial, \"Helvetica Neue\", Helvetica, sans-serif", "ffv": "font-family:Verdana, Geneva, sans-serif", "fl": "float:left|right|none", "fs": "font-style:italic|normal|oblique", "fsm": "font-smoothing:antialiased|subpixel-antialiased|none", "fst": "font-stretch:normal|ultra-condensed|extra-condensed|condensed|semi-condensed|semi-expanded|expanded|extra-expanded|ultra-expanded", "fv": "font-variant:normal|small-caps", "fvs": "font-variation-settings:normal|inherit|initial|unset", "fw": "font-weight:normal|bold|bolder|lighter", "fx": "flex", "fxb": "flex-basis:fill|max-content|min-content|fit-content|content", "fxd": "flex-direction:row|row-reverse|column|column-reverse", "fxf": "flex-flow", "fxg": "flex-grow", "fxsh": "flex-shrink", "fxw": "flex-wrap:nowrap|wrap|wrap-reverse", "fsz": "font-size", "fsza": "font-size-adjust", "gtc": "grid-template-columns:repeat()|minmax()", "gtr": "grid-template-rows:repeat()|minmax()", "gta": "grid-template-areas", "gt": "grid-template", "gg": "grid-gap", "gcg": "grid-column-gap", "grg": "grid-row-gap", "gac": "grid-auto-columns:auto|minmax()", "gar": "grid-auto-rows:auto|minmax()", "gaf": "grid-auto-flow:row|column|dense|inherit|initial|unset", "gd": "grid", "gc": "grid-column", "gcs": "grid-column-start", "gce": "grid-column-end", "gr": "grid-row", "grs": "grid-row-start", "gre": "grid-row-end", "ga": "grid-area", "h": "height", "jc": "justify-content:start|end|stretch|flex-start|flex-end|center|space-between|space-around|space-evenly", "ji": "justify-items:start|end|center|stretch", "js": "justify-self:start|end|center|stretch", "l": "left", "lg": "background-image:linear-gradient(${1})", "lh": "line-height", "lis": "list-style", "lisi": "list-style-image", "lisp": "list-style-position:inside|outside", "list": "list-style-type:disc|circle|square|decimal|decimal-leading-zero|lower-roman|upper-roman", "lts": "letter-spacing:normal", "m": "margin", "mah": "max-height", "mar": "max-resolution", "maw": "max-width", "mb": "margin-bottom", "mih": "min-height", "mir": "min-resolution", "miw": "min-width", "ml": "margin-left", "mr": "margin-right", "mt": "margin-top", "ol": "outline", "olc": "outline-color:${1:#000}|invert", "olo": "outline-offset", "ols": "outline-style:none|dotted|dashed|solid|double|groove|ridge|inset|outset", "olw": "outline-width|thin|medium|thick", "op": "opacity", "ord": "order", "ori": "orientation:landscape|portrait", "orp": "orphans", "ov": "overflow:hidden|visible|hidden|scroll|auto", "ovs": "overflow-style:scrollbar|auto|scrollbar|panner|move|marquee", "ovx": "overflow-x:hidden|visible|hidden|scroll|auto", "ovy": "overflow-y:hidden|visible|hidden|scroll|auto", "p": "padding", "pb": "padding-bottom", "pgba": "page-break-after:auto|always|left|right", "pgbb": "page-break-before:auto|always|left|right", "pgbi": "page-break-inside:auto|avoid", "pl": "padding-left", "pos": "position:relative|absolute|relative|fixed|static", "pr": "padding-right", "pt": "padding-top", "q": "quotes", "qen": "quotes:'\\201C' '\\201D' '\\2018' '\\2019'", "qru": "quotes:'\\00AB' '\\00BB' '\\201E' '\\201C'", "r": "right", "rsz": "resize:none|both|horizontal|vertical", "t": "top", "ta": "text-align:left|center|right|justify", "tal": "text-align-last:left|center|right", "tbl": "table-layout:fixed", "td": "text-decoration:none|underline|overline|line-through", "te": "text-emphasis:none|accent|dot|circle|disc|before|after", "th": "text-height:auto|font-size|text-size|max-size", "ti": "text-indent", "tj": "text-justify:auto|inter-word|inter-ideograph|inter-cluster|distribute|kashida|tibetan", "to": "text-outline:${1:0} ${2:0} ${3:#000}", "tov": "text-overflow:ellipsis|clip", "tr": "text-replace", "trf": "transform:${1}|skewX(${1:angle})|skewY(${1:angle})|scale(${1:x}, ${2:y})|scaleX(${1:x})|scaleY(${1:y})|scaleZ(${1:z})|scale3d(${1:x}, ${2:y}, ${3:z})|rotate(${1:angle})|rotateX(${1:angle})|rotateY(${1:angle})|rotateZ(${1:angle})|translate(${1:x}, ${2:y})|translateX(${1:x})|translateY(${1:y})|translateZ(${1:z})|translate3d(${1:tx}, ${2:ty}, ${3:tz})", "trfo": "transform-origin", "trfs": "transform-style:preserve-3d", "trs": "transition:${1:prop} ${2:time}", "trsde": "transition-delay:${1:time}", "trsdu": "transition-duration:${1:time}", "trsp": "transition-property:${1:prop}", "trstf": "transition-timing-function:${1:fn}", "tsh": "text-shadow:${1:hoff} ${2:voff} ${3:blur} ${4:#000}", "tt": "text-transform:uppercase|lowercase|capitalize|none", "tw": "text-wrap:none|normal|unrestricted|suppress", "us": "user-select:none", "v": "visibility:hidden|visible|collapse", "va": "vertical-align:top|super|text-top|middle|baseline|bottom|text-bottom|sub", "w": "width", "whs": "white-space:nowrap|pre|pre-wrap|pre-line|normal", "whsc": "white-space-collapse:normal|keep-all|loose|break-strict|break-all", "wid": "widows", "wm": "writing-mode:lr-tb|lr-tb|lr-bt|rl-tb|rl-bt|tb-rl|tb-lr|bt-lr|bt-rl", "wob": "word-break:normal|keep-all|break-all", "wos": "word-spacing", "wow": "word-wrap:none|unrestricted|suppress|break-word|normal", "z": "z-index", "zom": "zoom:1" }; var xsl$1 = { "tm|tmatch": "xsl:template[match mode]", "tn|tname": "xsl:template[name]", "call": "xsl:call-template[name]", "ap": "xsl:apply-templates[select mode]", "api": "xsl:apply-imports", "imp": "xsl:import[href]", "inc": "xsl:include[href]", "ch": "xsl:choose", "wh|xsl:when": "xsl:when[test]", "ot": "xsl:otherwise", "if": "xsl:if[test]", "par": "xsl:param[name]", "pare": "xsl:param[name select]", "var": "xsl:variable[name]", "vare": "xsl:variable[name select]", "wp": "xsl:with-param[name select]", "key": "xsl:key[name match use]", "elem": "xsl:element[name]", "attr": "xsl:attribute[name]", "attrs": "xsl:attribute-set[name]", "cp": "xsl:copy[select]", "co": "xsl:copy-of[select]", "val": "xsl:value-of[select]", "for|each": "xsl:for-each[select]", "tex": "xsl:text", "com": "xsl:comment", "msg": "xsl:message[terminate=no]", "fall": "xsl:fallback", "num": "xsl:number[value]", "nam": "namespace-alias[stylesheet-prefix result-prefix]", "pres": "xsl:preserve-space[elements]", "strip": "xsl:strip-space[elements]", "proc": "xsl:processing-instruction[name]", "sort": "xsl:sort[select order]", "choose": "xsl:choose>xsl:when+xsl:otherwise", "xsl": "!!!+xsl:stylesheet[version=1.0 xmlns:xsl=http://www.w3.org/1999/XSL/Transform]>{\n|}", "!!!": "{}" }; var index$7 = { html: html$1, css: css$1, xsl: xsl$1 }; var latin = { "common": ["lorem", "ipsum", "dolor", "sit", "amet", "consectetur", "adipisicing", "elit"], "words": ["exercitationem", "perferendis", "perspiciatis", "laborum", "eveniet", "sunt", "iure", "nam", "nobis", "eum", "cum", "officiis", "excepturi", "odio", "consectetur", "quasi", "aut", "quisquam", "vel", "eligendi", "itaque", "non", "odit", "tempore", "quaerat", "dignissimos", "facilis", "neque", "nihil", "expedita", "vitae", "vero", "ipsum", "nisi", "animi", "cumque", "pariatur", "velit", "modi", "natus", "iusto", "eaque", "sequi", "illo", "sed", "ex", "et", "voluptatibus", "tempora", "veritatis", "ratione", "assumenda", "incidunt", "nostrum", "placeat", "aliquid", "fuga", "provident", "praesentium", "rem", "necessitatibus", "suscipit", "adipisci", "quidem", "possimus", "voluptas", "debitis", "sint", "accusantium", "unde", "sapiente", "voluptate", "qui", "aspernatur", "laudantium", "soluta", "amet", "quo", "aliquam", "saepe", "culpa", "libero", "ipsa", "dicta", "reiciendis", "nesciunt", "doloribus", "autem", "impedit", "minima", "maiores", "repudiandae", "ipsam", "obcaecati", "ullam", "enim", "totam", "delectus", "ducimus", "quis", "voluptates", "dolores", "molestiae", "harum", "dolorem", "quia", "voluptatem", "molestias", "magni", "distinctio", "omnis", "illum", "dolorum", "voluptatum", "ea", "quas", "quam", "corporis", "quae", "blanditiis", "atque", "deserunt", "laboriosam", "earum", "consequuntur", "hic", "cupiditate", "quibusdam", "accusamus", "ut", "rerum", "error", "minus", "eius", "ab", "ad", "nemo", "fugit", "officia", "at", "in", "id", "quos", "reprehenderit", "numquam", "iste", "fugiat", "sit", "inventore", "beatae", "repellendus", "magnam", "recusandae", "quod", "explicabo", "doloremque", "aperiam", "consequatur", "asperiores", "commodi", "optio", "dolor", "labore", "temporibus", "repellat", "veniam", "architecto", "est", "esse", "mollitia", "nulla", "a", "similique", "eos", "alias", "dolore", "tenetur", "deleniti", "porro", "facere", "maxime", "corrupti"] }; var ru = { "common": ["далеко-далеко", "за", "словесными", "горами", "в стране", "гласных", "и согласных", "живут", "рыбные", "тексты"], "words": ["вдали", "от всех", "они", "буквенных", "домах", "на берегу", "семантика", "большого", "языкового", "океана", "маленький", "ручеек", "даль", "журчит", "по всей", "обеспечивает", "ее","всеми", "необходимыми", "правилами", "эта", "парадигматическая", "страна", "которой", "жаренные", "предложения", "залетают", "прямо", "рот", "даже", "всемогущая", "пунктуация", "не", "имеет", "власти", "над", "рыбными", "текстами", "ведущими", "безорфографичный", "образ", "жизни", "однажды", "одна", "маленькая", "строчка","рыбного", "текста", "имени", "lorem", "ipsum", "решила", "выйти", "большой", "мир", "грамматики", "великий", "оксмокс", "предупреждал", "о", "злых", "запятых", "диких", "знаках", "вопроса", "коварных", "точках", "запятой", "но", "текст", "дал", "сбить", "себя", "толку", "он", "собрал", "семь", "своих", "заглавных", "букв", "подпоясал", "инициал", "за", "пояс", "пустился", "дорогу", "взобравшись", "первую", "вершину", "курсивных", "гор", "бросил", "последний", "взгляд", "назад", "силуэт", "своего", "родного", "города", "буквоград", "заголовок", "деревни", "алфавит", "подзаголовок", "своего", "переулка", "грустный", "реторический", "вопрос", "скатился", "его", "щеке", "продолжил", "свой", "путь", "дороге", "встретил", "рукопись", "она", "предупредила", "моей", "все", "переписывается", "несколько", "раз", "единственное", "что", "меня", "осталось", "это", "приставка", "возвращайся", "ты", "лучше", "свою", "безопасную", "страну", "послушавшись", "рукописи", "наш", "продолжил", "свой", "путь", "вскоре", "ему", "повстречался", "коварный", "составитель", "рекламных", "текстов", "напоивший", "языком", "речью", "заманивший", "свое", "агентство", "которое", "использовало", "снова", "снова", "своих", "проектах", "если", "переписали", "то", "живет", "там", "до", "сих", "пор"] }; var sp = { "common": ["mujer", "uno", "dolor", "más", "de", "poder", "mismo", "si"], "words": ["ejercicio", "preferencia", "perspicacia", "laboral", "paño", "suntuoso", "molde", "namibia", "planeador", "mirar", "demás", "oficinista", "excepción", "odio", "consecuencia", "casi", "auto", "chicharra", "velo", "elixir", "ataque", "no", "odio", "temporal", "cuórum", "dignísimo", "facilismo", "letra", "nihilista", "expedición", "alma", "alveolar", "aparte", "león", "animal", "como", "paria", "belleza", "modo", "natividad", "justo", "ataque", "séquito", "pillo", "sed", "ex", "y", "voluminoso", "temporalidad", "verdades", "racional", "asunción", "incidente", "marejada", "placenta", "amanecer", "fuga", "previsor", "presentación", "lejos", "necesariamente", "sospechoso", "adiposidad", "quindío", "pócima", "voluble", "débito", "sintió", "accesorio", "falda", "sapiencia", "volutas", "queso", "permacultura", "laudo", "soluciones", "entero", "pan", "litro", "tonelada", "culpa", "libertario", "mosca", "dictado", "reincidente", "nascimiento", "dolor", "escolar", "impedimento", "mínima", "mayores", "repugnante", "dulce", "obcecado", "montaña", "enigma", "total", "deletéreo", "décima", "cábala", "fotografía", "dolores", "molesto", "olvido", "paciencia", "resiliencia", "voluntad", "molestias", "magnífico", "distinción", "ovni", "marejada", "cerro", "torre", "y", "abogada", "manantial", "corporal", "agua", "crepúsculo", "ataque", "desierto", "laboriosamente", "angustia", "afortunado", "alma", "encefalograma", "materialidad", "cosas", "o", "renuncia", "error", "menos", "conejo", "abadía", "analfabeto", "remo", "fugacidad", "oficio", "en", "almácigo", "vos", "pan", "represión", "números", "triste", "refugiado", "trote", "inventor", "corchea", "repelente", "magma", "recusado", "patrón", "explícito", "paloma", "síndrome", "inmune", "autoinmune", "comodidad", "ley", "vietnamita", "demonio", "tasmania", "repeler", "apéndice", "arquitecto", "columna", "yugo", "computador", "mula", "a", "propósito", "fantasía", "alias", "rayo", "tenedor", "deleznable", "ventana", "cara", "anemia", "corrupto"] }; const langs = { latin, ru, sp }; const defaultOptions$5 = { wordCount: 30, skipCommon: false, lang: 'latin' }; /** * Replaces given parsed Emmet abbreviation node with nodes filled with * Lorem Ipsum stub text. * @param {Node} node * @return {Node} */ var index$8 = function(node, options) { options = Object.assign({}, defaultOptions$5, options); const dict = langs[options.lang] || langs.latin; const startWithCommon = !options.skipCommon && !isRepeating(node); if (!node.repeat && !isRoot$1(node.parent)) { // non-repeating element, insert text stub as a content of parent node // and remove current one node.parent.value = paragraph(dict, options.wordCount, startWithCommon); node.remove(); } else { // Replace named node with generated content node.value = paragraph(dict, options.wordCount, startWithCommon); node.name = node.parent.name ? resolveImplicitName(node.parent.name) : null; } return node; }; function isRoot$1(node) { return !node.parent; } /** * Returns random integer between from and to values * @param {Number} from * @param {Number} to * @returns {Number} */ function rand(from, to) { return Math.floor(Math.random() * (to - from) + from); } /** * @param {Array} arr * @param {Number} count * @returns {Array} */ function sample(arr, count) { const len = arr.length; const iterations = Math.min(len, count); const result = new Set(); while (result.size < iterations) { result.add(arr[rand(0, len)]); } return Array.from(result); } function choice(val) { return val[rand(0, val.length - 1)]; } function sentence(words, end) { if (words.length) { words = [capitalize(words[0])].concat(words.slice(1)); } return words.join(' ') + (end || choice('?!...')); // more dots than question marks } function capitalize(word) { return word[0].toUpperCase() + word.slice(1); } /** * Insert commas at randomly selected words. This function modifies values * inside words array * @param {Array} words */ function insertCommas(words) { if (words.length < 2) { return words; } words = words.slice(); const len = words.length; const hasComma = /,$/; let totalCommas = 0; if (len > 3 && len <= 6) { totalCommas = rand(0, 1); } else if (len > 6 && len <= 12) { totalCommas = rand(0, 2); } else { totalCommas = rand(1, 4); } for (let i = 0, pos; i < totalCommas; i++) { pos = rand(0, len - 2); if (!hasComma.test(words[pos])) { words[pos] += ','; } } return words; } /** * Generate a paragraph of "Lorem ipsum" text * @param {Object} dict Words dictionary (see `lang/*.json`) * @param {Number} wordCount Words count in paragraph * @param {Boolean} startWithCommon Should paragraph start with common * "lorem ipsum" sentence. * @returns {String} */ function paragraph(dict, wordCount, startWithCommon) { const result = []; let totalWords = 0; let words; if (startWithCommon && dict.common) { words = dict.common.slice(0, wordCount); totalWords += words.length; result.push(sentence(insertCommas(words), '.')); } while (totalWords < wordCount) { words = sample(dict.words, Math.min(rand(2, 30), wordCount - totalWords)); totalWords += words.length; result.push(sentence(insertCommas(words))); } return result.join(' '); } /** * Check if given node is in repeating context, e.g. node itself or one of its * parent is repeated * @param {Node} node * @return {Boolean} */ function isRepeating(node) { while (node.parent) { if (node.repeat && node.repeat.value && node.repeat.value > 1) { return true; } node = node.parent; } return false; } const reLorem = /^lorem([a-z]*)(\d*)$/i; /** * Constructs a snippets registry, filled with snippets, for given options * @param {String} syntax Abbreviation syntax * @param {Object|Object[]} snippets Additional snippets * @return {SnippetsRegistry} */ function snippetsRegistryFactory(syntax, snippets) { const registrySnippets = [index$7[syntax] || index$7.html]; if (Array.isArray(snippets)) { snippets.forEach(item => { // if array item is a string, treat it as a reference to globally // defined snippets registrySnippets.push(typeof item === 'string' ? index$7[item] : item); }); } else if (typeof snippets === 'object') { registrySnippets.push(snippets); } const registry = new SnippetsRegistry(registrySnippets.filter(Boolean)); // for non-stylesheet syntaxes add Lorem Ipsum generator if (syntax !== 'css') { registry.get(0).set(reLorem, loremGenerator); } return registry; } function loremGenerator(node) { const options = {}; const m = node.name.match(reLorem); if (m[1]) { options.lang = m[1]; } if (m[2]) { options.wordCount = +m[2]; } return index$8(node, options); } /** * Default variables used in snippets to insert common values into predefined snippets * @type {Object} */ const defaultVariables = { lang: 'en', locale: 'en-US', charset: 'UTF-8' }; /** * A list of syntaxes that should use Emmet CSS abbreviations: * a variations of default abbreivation that holds values right in abbreviation name * @type {Set} */ const stylesheetSyntaxes = new Set(['css', 'sass', 'scss', 'less', 'stylus', 'sss']); const defaultOptions$6 = { /** * Abbreviation output syntax * @type {String} */ syntax: 'html', /** * Field/tabstop generator for editor. Most editors support TextMate-style * fields: ${0} or ${1:item}. So for TextMate-style fields this function * will look like this: * @example * (index, placeholder) => `\${${index}${placeholder ? ':' + placeholder : ''}}` * * @param {Number} index Placeholder index. Fields with the same indices * should be linked * @param {String} [placeholder] Field placeholder * @return {String} */ field: (index, placeholder) => placeholder || '', /** * Insert given text string(s) into expanded abbreviation * If array of strings is given, the implicitly repeated element (e.g. `li*`) * will be repeated by the amount of items in array * @type {String|String[]} */ text: null, /** * Either predefined output profile or options for output profile. Used for * abbreviation output * @type {Profile|Object} */ profile: null, /** * Custom variables for variable resolver * @see @emmetio/variable-resolver * @type {Object} */ variables: {}, /** * Custom predefined snippets for abbreviation. The expanded abbreviation * will try to match given snippets that may contain custom elements, * predefined attributes etc. * May also contain array of items: either snippets (Object) or references * to default syntax snippets (String; the key in default snippets hash) * @see @emmetio/snippets * @type {Object|SnippetsRegistry} */ snippets: {}, /** * Hash of additional transformations that should be applied to expanded * abbreviation, like BEM or JSX. Since these transformations introduce * side-effect, they are disabled by default and should be enabled by * providing a transform name as a key and transform options as value: * @example * { * bem: {element: '--'}, * jsx: true // no options, just enable transform * } * @see @emmetio/html-transform/lib/addons * @type {Object} */ addons: null, /** * Additional options for syntax formatter * @see @emmetio/markup-formatters * @type {Object} */ format: null }; /** * Expands given abbreviation into string, formatted according to provided * syntax and options * @param {String|Node} abbr Abbreviation string or parsed abbreviation tree * @param {String|Object} [options] Parsing and formatting options (object) or * abbreviation syntax (string) * @return {String} */ function expand$2(abbr, options) { options = createOptions(options); return isStylesheet(options.syntax) ? expand$1(abbr, options) : expand(abbr, options); } /** * Parses given abbreviation into AST tree. This tree can be later formatted to * string with `expand` function * @param {String} abbr Abbreviation to parse * @param {String|Object} [options] Parsing and formatting options (object) or * abbreviation syntax (string) * @return {Node} */ function parse$5(abbr, options) { options = createOptions(options); return isStylesheet(options.syntax) ? parse$4(abbr, options) : parse$3(abbr, options); } /** * Creates snippets registry for given syntax and additional `snippets` * @param {String} syntax Snippets syntax, used for retreiving predefined snippets * @param {SnippetsRegistry|Object|Object[]} [snippets] Additional snippets * @return {SnippetsRegistry} */ function createSnippetsRegistry(syntax, snippets) { return snippets instanceof SnippetsRegistry ? snippets : snippetsRegistryFactory(isStylesheet(syntax) ? 'css' : syntax, snippets); } function createOptions(options) { if (typeof options === 'string') { options = { syntax: options }; } options = Object.assign({}, defaultOptions$6, options); options.format = Object.assign({field: options.field}, options.format); options.profile = createProfile(options); options.variables = Object.assign({}, defaultVariables, options.variables); options.snippets = createSnippetsRegistry(isStylesheet(options.syntax) ? 'css' : options.syntax, options.snippets); return options; } /** * Check if given syntax belongs to stylesheet markup. * Emmet uses different abbreviation flavours: one is a default markup syntax, * used for HTML, Slim, Pug etc, the other one is used for stylesheets and * allows embedded values in abbreviation name * @param {String} syntax * @return {Boolean} */ function isStylesheet(syntax) { return stylesheetSyntaxes.has(syntax); } /** * Creates output profile from given options * @param {Object} options * @return {Profile} */ function createProfile(options) { return options.profile instanceof Profile ? options.profile : new Profile(options.profile); } exports.expand = expand$2; exports.parse = parse$5; exports.createSnippetsRegistry = createSnippetsRegistry; exports.createOptions = createOptions; exports.isStylesheet = isStylesheet; exports.createProfile = createProfile; Object.defineProperty(exports, '__esModule', { value: true }); })));