text: import resolveImplicitTag from '@emmetio/implicit-tag';
  
  /**
   * 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 = resolveImplicitTag(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;
          let descendingOrder = false;
  
          if (str.substr(r[0] + r[1], 1) === '@'){
              if (str.substr(r[0] + r[1] + 1, 1) === '-') {
                  descendingOrder = true;
              } 
              const matches = str.substr(r[0] + r[1] + 1 + Number(descendingOrder)).match(/^(\d+)/);
              if (matches) {
                  offsetLength = matches[1].length + 1 + Number(descendingOrder);
                  offset = parseInt(matches[1]) - 1;
              } else {
                  offsetLength = 2;
              }
          }
  
  		str = str.substring(0, r[0])
  			+ (typeof value === 'function' ? value(str.substr(r[0], r[1]), offset, descendingOrder) : 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;
          const count = repeater.count;
  
          node.name = replaceNumbering(node.name, value, count);
          node.value = replaceNumbering(node.value, value, count);
          node.attributes.forEach(attr => {
              const copy = node.getAttribute(attr.name).clone();
              copy.name = replaceNumbering(attr.name, value, count);
              copy.value = replaceNumbering(attr.value, value, count);
              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, count) {
      // 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, count);
      }
  
      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, count) {
      const replaced = replaceRanges(str, ranges, (token, offset, descendingOrder) => {
      let _value = descendingOrder ? String(offset + count - value + 1) : 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(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(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(node) {
  	while (node.children.length) {
  		node = node.children[node.children.length - 1];
  	}
  
  	return node;
  }
  
  /**
   * Updates content of given node
   * @param {Node} node
   * @param {String} content
   */
  function setNodeContent(node, content) {
  	// find caret position and replace it with content, if possible
  	if (node.value) {
  		const ranges = findUnescapedTokens(node.value, caret);
  		if (ranges.length) {
  			node.value = replaceRanges(node.value, ranges, content);
  			return;
  		}
  	}
  
  	if (node.name.toLowerCase() === 'a' || node.hasAttribute('href')) {
  		// special case: inserting content into `<a>` tag
  		if (reUrl.test(content)) {
  			node.setAttribute('href', (reProto.test(content) ? '' : 'http://') + content);
  		} else if (reEmail.test(content)) {
  			node.setAttribute('href', 'mailto:' + content);
  		}
  	}
  
  	node.value = content;
  }
  
  const defaultOptions = {
  	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, 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 = 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);
  };
  
  export default index;
  