diff --git a/client/editor.html b/client/editor.html
index 1bb10d98..c169d2db 100644
--- a/client/editor.html
+++ b/client/editor.html
@@ -6,6 +6,10 @@
emmet
object and will be available via
+ * emmet.require(name)
or emmet[name]
+ * @param {String} name
+ * @param {Function} factory
+ * @memberOf emmet
+ */
+ define: function(name, factory) {
+ // do not let redefine existing properties
+ if (!(name in modules)) {
+ modules[name] = _.isFunction(factory)
+ ? this.exec(factory)
+ : factory;
+ }
+ },
+
+ /**
+ * Returns reference to Emmet module
+ * @param {String} name Module name
+ */
+ require: r,
+
+ /**
+ * Helper method that just executes passed function but with all
+ * important arguments like 'require' and '_'
+ * @param {Function} fn
+ * @param {Object} context Execution context
+ */
+ exec: function(fn, context) {
+ return fn.call(context || global, _.bind(r, this), _, this);
+ },
+
+ /**
+ * The self-propagating extend function for classes.
+ * Took it from Backbone
+ * @param {Object} protoProps
+ * @param {Object} classProps
+ * @returns {Object}
+ */
+ extend: function(protoProps, classProps) {
+ var child = inherits(this, protoProps, classProps);
+ child.extend = this.extend;
+ // a hack required to WSH inherit `toString` method
+ if (protoProps.hasOwnProperty('toString'))
+ child.prototype.toString = protoProps.toString;
+ return child;
+ },
+
+ /**
+ * The essential function that expands Emmet abbreviation
+ * @param {String} abbr Abbreviation to parse
+ * @param {String} syntax Abbreviation's context syntax
+ * @param {String} profile Output profile (or its name)
+ * @param {Object} contextNode Contextual node where abbreviation is
+ * written
+ * @return {String}
+ */
+ expandAbbreviation: function(abbr, syntax, profile, contextNode) {
+ if (!abbr) return '';
+
+ syntax = syntax || defaultSyntax;
+// profile = profile || defaultProfile;
+
+ var filters = r('filters');
+ var parser = r('abbreviationParser');
+
+ profile = r('profile').get(profile, syntax);
+ r('tabStops').resetTabstopIndex();
+
+ var data = filters.extractFromAbbreviation(abbr);
+ var outputTree = parser.parse(data[0], {
+ syntax: syntax,
+ contextNode: contextNode
+ });
+
+ var filtersList = filters.composeList(syntax, profile, data[1]);
+ filters.apply(outputTree, filtersList, profile);
+ return outputTree.toString();
+ },
+
+ /**
+ * Returns default syntax name used in abbreviation engine
+ * @returns {String}
+ */
+ defaultSyntax: function() {
+ return defaultSyntax;
+ },
+
+ /**
+ * Returns default profile name used in abbreviation engine
+ * @returns {String}
+ */
+ defaultProfile: function() {
+ return defaultProfile;
+ },
+
+ /**
+ * Log message into console if it exists
+ */
+ log: function() {
+ if (global.console && global.console.log)
+ global.console.log.apply(global.console, arguments);
+ },
+
+ /**
+ * Setups function that should synchronously load undefined modules
+ * @param {Function} fn
+ */
+ setModuleLoader: function(fn) {
+ moduleLoader = fn;
+ }
+ };
+})(this);
+
+// export core for Node.JS
+if (typeof exports !== 'undefined') {
+ if (typeof module !== 'undefined' && module.exports) {
+ exports = module.exports = emmet;
+ }
+ exports.emmet = emmet;
+}/**
+ * Emmet abbreviation parser.
+ * Takes string abbreviation and recursively parses it into a tree. The parsed
+ * tree can be transformed into a string representation with
+ * toString()
method. Note that string representation is defined
+ * by custom processors (called filters), not by abbreviation parser
+ * itself.
+ *
+ * This module can be extended with custom pre-/post-processors to shape-up
+ * final tree or its representation. Actually, many features of abbreviation
+ * engine are defined in other modules as tree processors
+ *
+ *
+ * @author Sergey Chikuyonok (serge.che@gmail.com)
+ * @link http://chikuyonok.ru
+ * @memberOf __abbreviationParser
+ * @constructor
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.define('abbreviationParser', function(require, _) {
+ var reValidName = /^[\w\-\$\:@\!%]+\+?$/i;
+ var reWord = /[\w\-:\$]/;
+
+ var pairs = {
+ '[': ']',
+ '(': ')',
+ '{': '}'
+ };
+
+ var spliceFn = Array.prototype.splice;
+
+ var preprocessors = [];
+ var postprocessors = [];
+ var outputProcessors = [];
+
+ /**
+ * @type AbbreviationNode
+ */
+ function AbbreviationNode(parent) {
+ /** @type AbbreviationNode */
+ this.parent = null;
+ this.children = [];
+ this._attributes = [];
+
+ /** @type String Raw abbreviation for current node */
+ this.abbreviation = '';
+ this.counter = 1;
+ this._name = null;
+ this._text = '';
+ this.repeatCount = 1;
+ this.hasImplicitRepeat = false;
+
+ /** Custom data dictionary */
+ this._data = {};
+
+ // output properties
+ this.start = '';
+ this.end = '';
+ this.content = '';
+ this.padding = '';
+ }
+
+ AbbreviationNode.prototype = {
+ /**
+ * Adds passed node as child or creates new child
+ * @param {AbbreviationNode} child
+ * @param {Number} position Index in children array where child should
+ * be inserted
+ * @return {AbbreviationNode}
+ */
+ addChild: function(child, position) {
+ child = child || new AbbreviationNode;
+ child.parent = this;
+
+ if (_.isUndefined(position)) {
+ this.children.push(child);
+ } else {
+ this.children.splice(position, 0, child);
+ }
+
+ return child;
+ },
+
+ /**
+ * Creates a deep copy of current node
+ * @returns {AbbreviationNode}
+ */
+ clone: function() {
+ var node = new AbbreviationNode();
+ var attrs = ['abbreviation', 'counter', '_name', '_text', 'repeatCount', 'hasImplicitRepeat', 'start', 'end', 'content', 'padding'];
+ _.each(attrs, function(a) {
+ node[a] = this[a];
+ }, this);
+
+ // clone attributes
+ node._attributes = _.map(this._attributes, function(attr) {
+ return _.clone(attr);
+ });
+
+ node._data = _.clone(this._data);
+
+ // clone children
+ node.children = _.map(this.children, function(child) {
+ child = child.clone();
+ child.parent = node;
+ return child;
+ });
+
+ return node;
+ },
+
+ /**
+ * Removes current node from parent‘s child list
+ * @returns {AbbreviationNode} Current node itself
+ */
+ remove: function() {
+ if (this.parent) {
+ this.parent.children = _.without(this.parent.children, this);
+ }
+
+ return this;
+ },
+
+ /**
+ * Replaces current node in parent‘s children list with passed nodes
+ * @param {AbbreviationNode} node Replacement node or array of nodes
+ */
+ replace: function() {
+ var parent = this.parent;
+ var ix = _.indexOf(parent.children, this);
+ var items = _.flatten(arguments);
+ spliceFn.apply(parent.children, [ix, 1].concat(items));
+
+ // update parent
+ _.each(items, function(item) {
+ item.parent = parent;
+ });
+ },
+
+ /**
+ * Recursively sets property
to value
of current
+ * node and its children
+ * @param {String} name Property to update
+ * @param {Object} value New property value
+ */
+ updateProperty: function(name, value) {
+ this[name] = value;
+ _.each(this.children, function(child) {
+ child.updateProperty(name, value);
+ });
+ },
+
+ /**
+ * Finds first child node that matches truth test for passed
+ * fn
function
+ * @param {Function} fn
+ * @returns {AbbreviationNode}
+ */
+ find: function(fn) {
+ return this.findAll(fn)[0];
+// if (!_.isFunction(fn)) {
+// var elemName = fn.toLowerCase();
+// fn = function(item) {return item.name().toLowerCase() == elemName;};
+// }
+//
+// var result = null;
+// _.find(this.children, function(child) {
+// if (fn(child)) {
+// return result = child;
+// }
+//
+// return result = child.find(fn);
+// });
+//
+// return result;
+ },
+
+ /**
+ * Finds all child nodes that matches truth test for passed
+ * fn
function
+ * @param {Function} fn
+ * @returns {Array}
+ */
+ findAll: function(fn) {
+ if (!_.isFunction(fn)) {
+ var elemName = fn.toLowerCase();
+ fn = function(item) {return item.name().toLowerCase() == elemName;};
+ }
+
+ var result = [];
+ _.each(this.children, function(child) {
+ if (fn(child))
+ result.push(child);
+
+ result = result.concat(child.findAll(fn));
+ });
+
+ return _.compact(result);
+ },
+
+ /**
+ * Sets/gets custom data
+ * @param {String} name
+ * @param {Object} value
+ * @returns {Object}
+ */
+ data: function(name, value) {
+ if (arguments.length == 2) {
+ this._data[name] = value;
+
+ if (name == 'resource' && require('elements').is(value, 'snippet')) {
+ // setting snippet as matched resource: update `content`
+ // property with snippet value
+ this.content = value.data;
+ if (this._text) {
+ this.content = require('abbreviationUtils')
+ .insertChildContent(value.data, this._text);
+ }
+ }
+ }
+
+ return this._data[name];
+ },
+
+ /**
+ * Returns name of current node
+ * @returns {String}
+ */
+ name: function() {
+ var res = this.matchedResource();
+ if (require('elements').is(res, 'element')) {
+ return res.name;
+ }
+
+ return this._name;
+ },
+
+ /**
+ * Returns list of attributes for current node
+ * @returns {Array}
+ */
+ attributeList: function() {
+ var attrs = [];
+
+ var res = this.matchedResource();
+ if (require('elements').is(res, 'element') && _.isArray(res.attributes)) {
+ attrs = attrs.concat(res.attributes);
+ }
+
+ return optimizeAttributes(attrs.concat(this._attributes));
+ },
+
+ /**
+ * Returns or sets attribute value
+ * @param {String} name Attribute name
+ * @param {String} value New attribute value
+ * @returns {String}
+ */
+ attribute: function(name, value) {
+ if (arguments.length == 2) {
+ // modifying attribute
+ var ix = _.indexOf(_.pluck(this._attributes, 'name'), name.toLowerCase());
+ if (~ix) {
+ this._attributes[ix].value = value;
+ } else {
+ this._attributes.push({
+ name: name,
+ value: value
+ });
+ }
+ }
+
+ return (_.find(this.attributeList(), function(attr) {
+ return attr.name == name;
+ }) || {}).value;
+ },
+
+ /**
+ * Returns reference to the matched element
, if any.
+ * See {@link elements} module for a list of available elements
+ * @returns {Object}
+ */
+ matchedResource: function() {
+ return this.data('resource');
+ },
+
+ /**
+ * Returns index of current node in parent‘s children list
+ * @returns {Number}
+ */
+ index: function() {
+ return this.parent ? _.indexOf(this.parent.children, this) : -1;
+ },
+
+ /**
+ * Sets how many times current element should be repeated
+ * @private
+ */
+ _setRepeat: function(count) {
+ if (count) {
+ this.repeatCount = parseInt(count, 10) || 1;
+ } else {
+ this.hasImplicitRepeat = true;
+ }
+ },
+
+ /**
+ * Sets abbreviation that belongs to current node
+ * @param {String} abbr
+ */
+ setAbbreviation: function(abbr) {
+ abbr = abbr || '';
+
+ var that = this;
+
+ // find multiplier
+ abbr = abbr.replace(/\*(\d+)?$/, function(str, repeatCount) {
+ that._setRepeat(repeatCount);
+ return '';
+ });
+
+ this.abbreviation = abbr;
+
+ var abbrText = extractText(abbr);
+ if (abbrText) {
+ abbr = abbrText.element;
+ this.content = this._text = abbrText.text;
+ }
+
+ var abbrAttrs = parseAttributes(abbr);
+ if (abbrAttrs) {
+ abbr = abbrAttrs.element;
+ this._attributes = abbrAttrs.attributes;
+ }
+
+ this._name = abbr;
+
+ // validate name
+ if (this._name && !reValidName.test(this._name)) {
+ throw 'Invalid abbreviation';
+ }
+ },
+
+ /**
+ * Returns string representation of current node
+ * @return {String}
+ */
+ toString: function() {
+ var utils = require('utils');
+
+ var start = this.start;
+ var end = this.end;
+ var content = this.content;
+
+ // apply output processors
+ var node = this;
+ _.each(outputProcessors, function(fn) {
+ start = fn(start, node, 'start');
+ content = fn(content, node, 'content');
+ end = fn(end, node, 'end');
+ });
+
+
+ var innerContent = _.map(this.children, function(child) {
+ return child.toString();
+ }).join('');
+
+ content = require('abbreviationUtils').insertChildContent(content, innerContent, {
+ keepVariable: false
+ });
+
+ return start + utils.padString(content, this.padding) + end;
+ },
+
+ /**
+ * Check if current node contains children with empty expr
+ * property
+ * @return {Boolean}
+ */
+ hasEmptyChildren: function() {
+ return !!_.find(this.children, function(child) {
+ return child.isEmpty();
+ });
+ },
+
+ /**
+ * Check if current node has implied name that should be resolved
+ * @returns {Boolean}
+ */
+ hasImplicitName: function() {
+ return !this._name && !this.isTextNode();
+ },
+
+ /**
+ * Indicates that current element is a grouping one, e.g. has no
+ * representation but serves as a container for other nodes
+ * @returns {Boolean}
+ */
+ isGroup: function() {
+ return !this.abbreviation;
+ },
+
+ /**
+ * Indicates empty node (i.e. without abbreviation). It may be a
+ * grouping node and should not be outputted
+ * @return {Boolean}
+ */
+ isEmpty: function() {
+ return !this.abbreviation && !this.children.length;
+ },
+
+ /**
+ * Indicates that current node should be repeated
+ * @returns {Boolean}
+ */
+ isRepeating: function() {
+ return this.repeatCount > 1 || this.hasImplicitRepeat;
+ },
+
+ /**
+ * Check if current node is a text-only node
+ * @return {Boolean}
+ */
+ isTextNode: function() {
+ return !this.name() && !this.attributeList().length;
+ },
+
+ /**
+ * Indicates whether this node may be used to build elements or snippets
+ * @returns {Boolean}
+ */
+ isElement: function() {
+ return !this.isEmpty() && !this.isTextNode();
+ },
+
+ /**
+ * Returns latest and deepest child of current tree
+ * @returns {AbbreviationNode}
+ */
+ deepestChild: function() {
+ if (!this.children.length)
+ return null;
+
+ var deepestChild = this;
+ while (deepestChild.children.length) {
+ deepestChild = _.last(deepestChild.children);
+ }
+
+ return deepestChild;
+ }
+ };
+
+ /**
+ * Returns stripped string: a string without first and last character.
+ * Used for “unquoting” strings
+ * @param {String} str
+ * @returns {String}
+ */
+ function stripped(str) {
+ return str.substring(1, str.length - 1);
+ }
+
+ function consumeQuotedValue(stream, quote) {
+ var ch;
+ while (ch = stream.next()) {
+ if (ch === quote)
+ return true;
+
+ if (ch == '\\')
+ continue;
+ }
+
+ return false;
+ }
+
+ /**
+ * Parses abbreviation into a tree
+ * @param {String} abbr
+ * @returns {AbbreviationNode}
+ */
+ function parseAbbreviation(abbr) {
+ abbr = require('utils').trim(abbr);
+
+ var root = new AbbreviationNode;
+ var context = root.addChild(), ch;
+
+ /** @type StringStream */
+ var stream = require('stringStream').create(abbr);
+ var loopProtector = 1000, multiplier;
+
+ while (!stream.eol() && --loopProtector > 0) {
+ ch = stream.peek();
+
+ switch (ch) {
+ case '(': // abbreviation group
+ stream.start = stream.pos;
+ if (stream.skipToPair('(', ')')) {
+ var inner = parseAbbreviation(stripped(stream.current()));
+ if (multiplier = stream.match(/^\*(\d+)?/, true)) {
+ context._setRepeat(multiplier[1]);
+ }
+
+ _.each(inner.children, function(child) {
+ context.addChild(child);
+ });
+ } else {
+ throw 'Invalid abbreviation: mo matching ")" found for character at ' + stream.pos;
+ }
+ break;
+
+ case '>': // child operator
+ context = context.addChild();
+ stream.next();
+ break;
+
+ case '+': // sibling operator
+ context = context.parent.addChild();
+ stream.next();
+ break;
+
+ case '^': // climb up operator
+ var parent = context.parent || context;
+ context = (parent.parent || parent).addChild();
+ stream.next();
+ break;
+
+ default: // consume abbreviation
+ stream.start = stream.pos;
+ stream.eatWhile(function(c) {
+ if (c == '[' || c == '{') {
+ if (stream.skipToPair(c, pairs[c])) {
+ stream.backUp(1);
+ return true;
+ }
+
+ throw 'Invalid abbreviation: mo matching "' + pairs[c] + '" found for character at ' + stream.pos;
+ }
+
+ if (c == '+') {
+ // let's see if this is an expando marker
+ stream.next();
+ var isMarker = stream.eol() || ~'+>^*'.indexOf(stream.peek());
+ stream.backUp(1);
+ return isMarker;
+ }
+
+ return c != '(' && isAllowedChar(c);
+ });
+
+ context.setAbbreviation(stream.current());
+ stream.start = stream.pos;
+ }
+ }
+
+ if (loopProtector < 1)
+ throw 'Endless loop detected';
+
+ return root;
+ }
+
+ /**
+ * Extract attributes and their values from attribute set:
+ * [attr col=3 title="Quoted string"]
+ * @param {String} attrSet
+ * @returns {Array}
+ */
+ function extractAttributes(attrSet, attrs) {
+ attrSet = require('utils').trim(attrSet);
+ var result = [];
+
+ /** @type StringStream */
+ var stream = require('stringStream').create(attrSet);
+ stream.eatSpace();
+
+ while (!stream.eol()) {
+ stream.start = stream.pos;
+ if (stream.eatWhile(reWord)) {
+ var attrName = stream.current();
+ var attrValue = '';
+ if (stream.peek() == '=') {
+ stream.next();
+ stream.start = stream.pos;
+ var quote = stream.peek();
+
+ if (quote == '"' || quote == "'") {
+ stream.next();
+ if (consumeQuotedValue(stream, quote)) {
+ attrValue = stream.current();
+ // strip quotes
+ attrValue = attrValue.substring(1, attrValue.length - 1);
+ } else {
+ throw 'Invalid attribute value';
+ }
+ } else if (stream.eatWhile(/[^\s\]]/)) {
+ attrValue = stream.current();
+ } else {
+ throw 'Invalid attribute value';
+ }
+ }
+
+ result.push({
+ name: attrName,
+ value: attrValue
+ });
+ stream.eatSpace();
+ } else {
+ break;
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Parses tag attributes extracted from abbreviation. If attributes found,
+ * returns object with element
and attributes
+ * properties
+ * @param {String} abbr
+ * @returns {Object} Returns null
if no attributes found in
+ * abbreviation
+ */
+ function parseAttributes(abbr) {
+ /*
+ * Example of incoming data:
+ * #header
+ * .some.data
+ * .some.data#header
+ * [attr]
+ * #item[attr=Hello other="World"].class
+ */
+ var result = [];
+ var attrMap = {'#': 'id', '.': 'class'};
+ var nameEnd = null;
+
+ /** @type StringStream */
+ var stream = require('stringStream').create(abbr);
+ while (!stream.eol()) {
+ switch (stream.peek()) {
+ case '#': // id
+ case '.': // class
+ if (nameEnd === null)
+ nameEnd = stream.pos;
+
+ var attrName = attrMap[stream.peek()];
+
+ stream.next();
+ stream.start = stream.pos;
+ stream.eatWhile(reWord);
+ result.push({
+ name: attrName,
+ value: stream.current()
+ });
+ break;
+ case '[': //begin attribute set
+ if (nameEnd === null)
+ nameEnd = stream.pos;
+
+ stream.start = stream.pos;
+ if (!stream.skipToPair('[', ']'))
+ throw 'Invalid attribute set definition';
+
+ result = result.concat(
+ extractAttributes(stripped(stream.current()))
+ );
+ break;
+ default:
+ stream.next();
+ }
+ }
+
+ if (!result.length)
+ return null;
+
+ return {
+ element: abbr.substring(0, nameEnd),
+ attributes: optimizeAttributes(result)
+ };
+ }
+
+ /**
+ * Optimize attribute set: remove duplicates and merge class attributes
+ * @param attrs
+ */
+ function optimizeAttributes(attrs) {
+ // clone all attributes to make sure that original objects are
+ // not modified
+ attrs = _.map(attrs, function(attr) {
+ return _.clone(attr);
+ });
+
+ var lookup = {};
+ return _.filter(attrs, function(attr) {
+ if (!(attr.name in lookup)) {
+ return lookup[attr.name] = attr;
+ }
+
+ var la = lookup[attr.name];
+
+ if (attr.name.toLowerCase() == 'class') {
+ la.value += (la.value.length ? ' ' : '') + attr.value;
+ } else {
+ la.value = attr.value;
+ }
+
+ return false;
+ });
+ }
+
+ /**
+ * Extract text data from abbreviation: if a{hello}
abbreviation
+ * is passed, returns object {element: 'a', text: 'hello'}
.
+ * If nothing found, returns null
+ * @param {String} abbr
+ *
+ */
+ function extractText(abbr) {
+ if (!~abbr.indexOf('{'))
+ return null;
+
+ /** @type StringStream */
+ var stream = require('stringStream').create(abbr);
+ while (!stream.eol()) {
+ switch (stream.peek()) {
+ case '[':
+ case '(':
+ stream.skipToPair(stream.peek(), pairs[stream.peek()]); break;
+
+ case '{':
+ stream.start = stream.pos;
+ stream.skipToPair('{', '}');
+ return {
+ element: abbr.substring(0, stream.start),
+ text: stripped(stream.current())
+ };
+
+ default:
+ stream.next();
+ }
+ }
+ }
+
+ /**
+ * “Un-rolls“ contents of current node: recursively replaces all repeating
+ * children with their repeated clones
+ * @param {AbbreviationNode} node
+ * @returns {AbbreviationNode}
+ */
+ function unroll(node) {
+ for (var i = node.children.length - 1, j, child; i >= 0; i--) {
+ child = node.children[i];
+
+ if (child.isRepeating()) {
+ j = child.repeatCount;
+ child.repeatCount = 1;
+ child.updateProperty('counter', 1);
+ while (--j > 0) {
+ child.parent.addChild(child.clone(), i + 1)
+ .updateProperty('counter', j + 1);
+ }
+ }
+ }
+
+ // to keep proper 'counter' property, we need to walk
+ // on children once again
+ _.each(node.children, unroll);
+
+ return node;
+ }
+
+ /**
+ * Optimizes tree node: replaces empty nodes with their children
+ * @param {AbbreviationNode} node
+ * @return {AbbreviationNode}
+ */
+ function squash(node) {
+ for (var i = node.children.length - 1; i >= 0; i--) {
+ /** @type AbbreviationNode */
+ var n = node.children[i];
+ if (n.isGroup()) {
+ n.replace(squash(n).children);
+ } else if (n.isEmpty()) {
+ n.remove();
+ }
+ }
+
+ _.each(node.children, squash);
+
+ return node;
+ }
+
+ function isAllowedChar(ch) {
+ var charCode = ch.charCodeAt(0);
+ var specialChars = '#.*:$-_!@|%';
+
+ return (charCode > 64 && charCode < 91) // uppercase letter
+ || (charCode > 96 && charCode < 123) // lowercase letter
+ || (charCode > 47 && charCode < 58) // number
+ || specialChars.indexOf(ch) != -1; // special character
+ }
+
+ // XXX add counter replacer function as output processor
+ outputProcessors.push(function(text, node) {
+ return require('utils').replaceCounter(text, node.counter);
+ });
+
+ return {
+ /**
+ * Parses abbreviation into tree with respect of groups,
+ * text nodes and attributes. Each node of the tree is a single
+ * abbreviation. Tree represents actual structure of the outputted
+ * result
+ * @memberOf abbreviationParser
+ * @param {String} abbr Abbreviation to parse
+ * @param {Object} options Additional options for parser and processors
+ *
+ * @return {AbbreviationNode}
+ */
+ parse: function(abbr, options) {
+ options = options || {};
+
+ var tree = parseAbbreviation(abbr);
+
+ if (options.contextNode) {
+ // add info about context node –
+ // a parent XHTML node in editor inside which abbreviation is
+ // expanded
+ tree._name = options.contextNode.name;
+ var attrLookup = {};
+ _.each(tree._attributes, function(attr) {
+ attrLookup[attr.name] = attr;
+ });
+
+ _.each(options.contextNode.attributes, function(attr) {
+ if (attr.name in attrLookup) {
+ attrLookup[attr.name].value = attr.value;
+ } else {
+ attr = _.clone(attr);
+ tree._attributes.push(attr);
+ attrLookup[attr.name] = attr;
+ }
+ });
+ }
+
+
+ // apply preprocessors
+ _.each(preprocessors, function(fn) {
+ fn(tree, options);
+ });
+
+ tree = squash(unroll(tree));
+
+ // apply postprocessors
+ _.each(postprocessors, function(fn) {
+ fn(tree, options);
+ });
+
+ return tree;
+ },
+
+ AbbreviationNode: AbbreviationNode,
+
+ /**
+ * Add new abbreviation preprocessor. Preprocessor is a function
+ * that applies to a parsed abbreviation tree right after it get parsed.
+ * The passed tree is in unoptimized state.
+ * @param {Function} fn Preprocessor function. This function receives
+ * two arguments: parsed abbreviation tree (AbbreviationNode
)
+ * and options
hash that was passed to parse
+ * method
+ */
+ addPreprocessor: function(fn) {
+ if (!_.include(preprocessors, fn))
+ preprocessors.push(fn);
+ },
+
+ /**
+ * Removes registered preprocessor
+ */
+ removeFilter: function(fn) {
+ preprocessor = _.without(preprocessors, fn);
+ },
+
+ /**
+ * Adds new abbreviation postprocessor. Postprocessor is a
+ * functinon that applies to optimized parsed abbreviation tree
+ * right before it returns from parse()
method
+ * @param {Function} fn Postprocessor function. This function receives
+ * two arguments: parsed abbreviation tree (AbbreviationNode
)
+ * and options
hash that was passed to parse
+ * method
+ */
+ addPostprocessor: function(fn) {
+ if (!_.include(postprocessors, fn))
+ postprocessors.push(fn);
+ },
+
+ /**
+ * Removes registered postprocessor function
+ */
+ removePostprocessor: function(fn) {
+ postprocessors = _.without(postprocessors, fn);
+ },
+
+ /**
+ * Registers output postprocessor. Output processor is a
+ * function that applies to output part (start
,
+ * end
and content
) when
+ * AbbreviationNode.toString()
method is called
+ */
+ addOutputProcessor: function(fn) {
+ if (!_.include(outputProcessors, fn))
+ outputProcessors.push(fn);
+ },
+
+ /**
+ * Removes registered output processor
+ */
+ removeOutputProcessor: function(fn) {
+ outputProcessors = _.without(outputProcessors, fn);
+ },
+
+ /**
+ * Check if passed symbol is valid symbol for abbreviation expression
+ * @param {String} ch
+ * @return {Boolean}
+ */
+ isAllowedChar: function(ch) {
+ ch = String(ch); // convert Java object to JS
+ return isAllowedChar(ch) || ~'>+^[](){}'.indexOf(ch);
+ }
+ };
+});/**
+ * Processor function that matches parsed AbbreviationNode
+ * against resources defined in resource
module
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _) {
+ /**
+ * Finds matched resources for child nodes of passed node
+ * element. A matched resource is a reference to snippets.json entry
+ * that describes output of parsed node
+ * @param {AbbreviationNode} node
+ * @param {String} syntax
+ */
+ function matchResources(node, syntax) {
+ var resources = require('resources');
+ var elements = require('elements');
+ var parser = require('abbreviationParser');
+
+ // do a shallow copy because the children list can be modified during
+ // resource matching
+ _.each(_.clone(node.children), /** @param {AbbreviationNode} child */ function(child) {
+ var r = resources.getMatchedResource(child, syntax);
+ if (_.isString(r)) {
+ child.data('resource', elements.create('snippet', r));
+ } else if (elements.is(r, 'reference')) {
+ // it’s a reference to another abbreviation:
+ // parse it and insert instead of current child
+ /** @type AbbreviationNode */
+ var subtree = parser.parse(r.data, {
+ syntax: syntax
+ });
+
+ // if context element should be repeated, check if we need to
+ // transfer repeated element to specific child node
+ if (child.repeatCount > 1) {
+ var repeatedChildren = subtree.findAll(function(node) {
+ return node.hasImplicitRepeat;
+ });
+
+ _.each(repeatedChildren, function(node) {
+ node.repeatCount = child.repeatCount;
+ node.hasImplicitRepeat = false;
+ });
+ }
+
+ // move child‘s children into the deepest child of new subtree
+ var deepestChild = subtree.deepestChild();
+ if (deepestChild) {
+ _.each(child.children, function(c) {
+ deepestChild.addChild(c);
+ });
+ }
+
+ // copy current attributes to children
+ _.each(subtree.children, function(node) {
+ _.each(child.attributeList(), function(attr) {
+ node.attribute(attr.name, attr.value);
+ });
+ });
+
+ child.replace(subtree.children);
+ } else {
+ child.data('resource', r);
+ }
+
+ matchResources(child, syntax);
+ });
+ }
+
+ // XXX register abbreviation filter that creates references to resources
+ // on abbreviation nodes
+ /**
+ * @param {AbbreviationNode} tree
+ */
+ require('abbreviationParser').addPreprocessor(function(tree, options) {
+ var syntax = options.syntax || emmet.defaultSyntax();
+ matchResources(tree, syntax);
+ });
+
+});/**
+ * Pasted content abbreviation processor. A pasted content is a content that
+ * should be inserted into implicitly repeated abbreviation nodes.
+ * This processor powers “Wrap With Abbreviation” action
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _) {
+ var parser = require('abbreviationParser');
+ var outputPlaceholder = '$#';
+
+ /**
+ * Locates output placeholders inside text
+ * @param {String} text
+ * @returns {Array} Array of ranges of output placeholder in text
+ */
+ function locateOutputPlaceholder(text) {
+ var range = require('range');
+ var result = [];
+
+ /** @type StringStream */
+ var stream = require('stringStream').create(text);
+
+ while (!stream.eol()) {
+ if (stream.peek() == '\\') {
+ stream.next();
+ } else {
+ stream.start = stream.pos;
+ if (stream.match(outputPlaceholder, true)) {
+ result.push(range.create(stream.start, outputPlaceholder));
+ continue;
+ }
+ }
+ stream.next();
+ }
+
+ return result;
+ }
+
+ /**
+ * Replaces output placeholders inside source
with
+ * value
+ * @param {String} source
+ * @param {String} value
+ * @returns {String}
+ */
+ function replaceOutputPlaceholders(source, value) {
+ var utils = require('utils');
+ var ranges = locateOutputPlaceholder(source);
+
+ ranges.reverse();
+ _.each(ranges, function(r) {
+ source = utils.replaceSubstring(source, value, r);
+ });
+
+ return source;
+ }
+
+ /**
+ * Check if parsed node contains output placeholder – a target where
+ * pasted content should be inserted
+ * @param {AbbreviationNode} node
+ * @returns {Boolean}
+ */
+ function hasOutputPlaceholder(node) {
+ if (locateOutputPlaceholder(node.content).length)
+ return true;
+
+ // check if attributes contains placeholder
+ return !!_.find(node.attributeList(), function(attr) {
+ return !!locateOutputPlaceholder(attr.value).length;
+ });
+ }
+
+ /**
+ * Insert pasted content into correct positions of parsed node
+ * @param {AbbreviationNode} node
+ * @param {String} content
+ * @param {Boolean} overwrite Overwrite node content if no value placeholders
+ * found instead of appending to existing content
+ */
+ function insertPastedContent(node, content, overwrite) {
+ var nodesWithPlaceholders = node.findAll(function(item) {
+ return hasOutputPlaceholder(item);
+ });
+
+ if (hasOutputPlaceholder(node))
+ nodesWithPlaceholders.unshift(node);
+
+ if (nodesWithPlaceholders.length) {
+ _.each(nodesWithPlaceholders, function(item) {
+ item.content = replaceOutputPlaceholders(item.content, content);
+ _.each(item._attributes, function(attr) {
+ attr.value = replaceOutputPlaceholders(attr.value, content);
+ });
+ });
+ } else {
+ // on output placeholders in subtree, insert content in the deepest
+ // child node
+ var deepest = node.deepestChild() || node;
+ if (overwrite) {
+ deepest.content = content;
+ } else {
+ deepest.content = require('abbreviationUtils').insertChildContent(deepest.content, content);
+ }
+ }
+ }
+
+ /**
+ * @param {AbbreviationNode} tree
+ * @param {Object} options
+ */
+ parser.addPreprocessor(function(tree, options) {
+ if (options.pastedContent) {
+ var utils = require('utils');
+ var lines = _.map(utils.splitByLines(options.pastedContent, true), utils.trim);
+
+ // set repeat count for implicitly repeated elements before
+ // tree is unrolled
+ tree.findAll(function(item) {
+ if (item.hasImplicitRepeat) {
+ item.data('paste', lines);
+ return item.repeatCount = lines.length;
+ }
+ });
+ }
+ });
+
+ /**
+ * @param {AbbreviationNode} tree
+ * @param {Object} options
+ */
+ parser.addPostprocessor(function(tree, options) {
+ // for each node with pasted content, update text data
+ var targets = tree.findAll(function(item) {
+ var pastedContentObj = item.data('paste');
+ var pastedContent = '';
+ if (_.isArray(pastedContentObj)) {
+ pastedContent = pastedContentObj[item.counter - 1];
+ } else if (_.isFunction(pastedContentObj)) {
+ pastedContent = pastedContentObj(item.counter - 1, item.content);
+ } else if (pastedContentObj) {
+ pastedContent = pastedContentObj;
+ }
+
+ if (pastedContent) {
+ insertPastedContent(item, pastedContent, !!item.data('pasteOverwrites'));
+ }
+
+ item.data('paste', null);
+ return !!pastedContentObj;
+ });
+
+ if (!targets.length && options.pastedContent) {
+ // no implicitly repeated elements, put pasted content in
+ // the deepest child
+ insertPastedContent(tree, options.pastedContent);
+ }
+ });
+});/**
+ * Resolves tag names in abbreviations with implied name
+ */
+emmet.exec(function(require, _) {
+ /**
+ * Resolves implicit node names in parsed tree
+ * @param {AbbreviationNode} tree
+ */
+ function resolveNodeNames(tree) {
+ var tagName = require('tagName');
+ _.each(tree.children, function(node) {
+ if (node.hasImplicitName() || node.data('forceNameResolving')) {
+ node._name = tagName.resolve(node.parent.name());
+ }
+ resolveNodeNames(node);
+ });
+
+ return tree;
+ }
+
+ require('abbreviationParser').addPostprocessor(resolveNodeNames);
+});/**
+ * @author Stoyan Stefanov
+ * @link https://github.com/stoyan/etc/tree/master/cssex
+ */
+
+emmet.define('cssParser', function(require, _) {
+var walker, tokens = [], isOp, isNameChar, isDigit;
+
+ // walks around the source
+ walker = {
+ lines: null,
+ total_lines: 0,
+ linenum: -1,
+ line: '',
+ ch: '',
+ chnum: -1,
+ init: function (source) {
+ var me = walker;
+
+ // source, yumm
+ me.lines = source
+ .replace(/\r\n/g, '\n')
+ .replace(/\r/g, '\n')
+ .split('\n');
+ me.total_lines = me.lines.length;
+
+ // reset
+ me.chnum = -1;
+ me.linenum = -1;
+ me.ch = '';
+ me.line = '';
+
+ // advance
+ me.nextLine();
+ me.nextChar();
+ },
+ nextLine: function () {
+ var me = this;
+ me.linenum += 1;
+ if (me.total_lines <= me.linenum) {
+ me.line = false;
+ } else {
+ me.line = me.lines[me.linenum];
+ }
+ if (me.chnum !== -1) {
+ me.chnum = 0;
+ }
+ return me.line;
+ },
+ nextChar: function () {
+ var me = this;
+ me.chnum += 1;
+ while (me.line.charAt(me.chnum) === '') {
+ if (this.nextLine() === false) {
+ me.ch = false;
+ return false; // end of source
+ }
+ me.chnum = -1;
+ me.ch = '\n';
+ return '\n';
+ }
+ me.ch = me.line.charAt(me.chnum);
+ return me.ch;
+ },
+ peek: function() {
+ return this.line.charAt(this.chnum + 1);
+ }
+ };
+
+ // utility helpers
+ isNameChar = function (c) {
+ // be more tolerate for name tokens: allow & character for LESS syntax
+ return (c == '&' || c === '_' || c === '-' || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'));
+ };
+
+ isDigit = function (ch) {
+ return (ch !== false && ch >= '0' && ch <= '9');
+ };
+
+ isOp = (function () {
+ var opsa = "{}[]()+*=.,;:>~|\\%$#@^!".split(''),
+ opsmatcha = "*^|$~".split(''),
+ ops = {},
+ opsmatch = {},
+ i = 0;
+ for (; i < opsa.length; i += 1) {
+ ops[opsa[i]] = true;
+ }
+ for (i = 0; i < opsmatcha.length; i += 1) {
+ opsmatch[opsmatcha[i]] = true;
+ }
+ return function (ch, matchattr) {
+ if (matchattr) {
+ return !!opsmatch[ch];
+ }
+ return !!ops[ch];
+ };
+ }());
+
+ // shorthands
+ function isset(v) {
+ return typeof v !== 'undefined';
+ }
+ function getConf() {
+ return {
+ 'char': walker.chnum,
+ line: walker.linenum
+ };
+ }
+
+
+ // creates token objects and pushes them to a list
+ function tokener(value, type, conf) {
+ var w = walker, c = conf || {};
+ tokens.push({
+ charstart: isset(c['char']) ? c['char'] : w.chnum,
+ charend: isset(c.charend) ? c.charend : w.chnum,
+ linestart: isset(c.line) ? c.line : w.linenum,
+ lineend: isset(c.lineend) ? c.lineend : w.linenum,
+ value: value,
+ type: type || value
+ });
+ }
+
+ // oops
+ function error(m, config) {
+ var w = walker,
+ conf = config || {},
+ c = isset(conf['char']) ? conf['char'] : w.chnum,
+ l = isset(conf.line) ? conf.line : w.linenum;
+ return {
+ name: "ParseError",
+ message: m + " at line " + (l + 1) + ' char ' + (c + 1),
+ walker: w,
+ tokens: tokens
+ };
+ }
+
+
+ // token handlers follow for:
+ // white space, comment, string, identifier, number, operator
+ function white() {
+
+ var c = walker.ch,
+ token = '',
+ conf = getConf();
+
+ while (c === " " || c === "\t") {
+ token += c;
+ c = walker.nextChar();
+ }
+
+ tokener(token, 'white', conf);
+
+ }
+
+ function comment() {
+
+ var w = walker,
+ c = w.ch,
+ token = c,
+ cnext,
+ conf = getConf();
+
+ cnext = w.nextChar();
+
+ if (cnext !== '*') {
+ // oops, not a comment, just a /
+ conf.charend = conf['char'];
+ conf.lineend = conf.line;
+ return tokener(token, token, conf);
+ }
+
+ while (!(c === "*" && cnext === "/")) {
+ token += cnext;
+ c = cnext;
+ cnext = w.nextChar();
+ }
+ token += cnext;
+ w.nextChar();
+ tokener(token, 'comment', conf);
+ }
+
+ function str() {
+ var w = walker,
+ c = w.ch,
+ q = c,
+ token = c,
+ cnext,
+ conf = getConf();
+
+ c = w.nextChar();
+
+ while (c !== q) {
+
+ if (c === '\n') {
+ cnext = w.nextChar();
+ if (cnext === "\\") {
+ token += c + cnext;
+ } else {
+ // end of line with no \ escape = bad
+ throw error("Unterminated string", conf);
+ }
+ } else {
+ if (c === "\\") {
+ token += c + w.nextChar();
+ } else {
+ token += c;
+ }
+ }
+
+ c = w.nextChar();
+
+ }
+ token += c;
+ w.nextChar();
+ tokener(token, 'string', conf);
+ }
+
+ function brace() {
+ var w = walker,
+ c = w.ch,
+ depth = 0,
+ token = c,
+ conf = getConf();
+
+ c = w.nextChar();
+
+ while (c !== ')' && !depth) {
+ if (c === '(') {
+ depth++;
+ } else if (c === ')') {
+ depth--;
+ } else if (c === false) {
+ throw error("Unterminated brace", conf);
+ }
+
+ token += c;
+ c = w.nextChar();
+ }
+
+ token += c;
+ w.nextChar();
+ tokener(token, 'brace', conf);
+ }
+
+ function identifier(pre) {
+ var w = walker,
+ c = w.ch,
+ conf = getConf(),
+ token = (pre) ? pre + c : c;
+
+ c = w.nextChar();
+
+ if (pre) { // adjust token position
+ conf['char'] -= pre.length;
+ }
+
+ while (isNameChar(c) || isDigit(c)) {
+ token += c;
+ c = w.nextChar();
+ }
+
+ tokener(token, 'identifier', conf);
+ }
+
+ function num() {
+ var w = walker,
+ c = w.ch,
+ conf = getConf(),
+ token = c,
+ point = token === '.',
+ nondigit;
+
+ c = w.nextChar();
+ nondigit = !isDigit(c);
+
+ // .2px or .classname?
+ if (point && nondigit) {
+ // meh, NaN, could be a class name, so it's an operator for now
+ conf.charend = conf['char'];
+ conf.lineend = conf.line;
+ return tokener(token, '.', conf);
+ }
+
+ // -2px or -moz-something
+ if (token === '-' && nondigit) {
+ return identifier('-');
+ }
+
+ while (c !== false && (isDigit(c) || (!point && c === '.'))) { // not end of source && digit or first instance of .
+ if (c === '.') {
+ point = true;
+ }
+ token += c;
+ c = w.nextChar();
+ }
+
+ tokener(token, 'number', conf);
+
+ }
+
+ function op() {
+ var w = walker,
+ c = w.ch,
+ conf = getConf(),
+ token = c,
+ next = w.nextChar();
+
+ if (next === "=" && isOp(token, true)) {
+ token += next;
+ tokener(token, 'match', conf);
+ w.nextChar();
+ return;
+ }
+
+ conf.charend = conf['char'] + 1;
+ conf.lineend = conf.line;
+ tokener(token, token, conf);
+ }
+
+
+ // call the appropriate handler based on the first character in a token suspect
+ function tokenize() {
+
+ var ch = walker.ch;
+
+ if (ch === " " || ch === "\t") {
+ return white();
+ }
+
+ if (ch === '/') {
+ return comment();
+ }
+
+ if (ch === '"' || ch === "'") {
+ return str();
+ }
+
+ if (ch === '(') {
+ return brace();
+ }
+
+ if (ch === '-' || ch === '.' || isDigit(ch)) { // tricky - char: minus (-1px) or dash (-moz-stuff)
+ return num();
+ }
+
+ if (isNameChar(ch)) {
+ return identifier();
+ }
+
+ if (isOp(ch)) {
+ return op();
+ }
+
+ if (ch === "\n") {
+ tokener("line");
+ walker.nextChar();
+ return;
+ }
+
+ throw error("Unrecognized character");
+ }
+
+ /**
+ * Returns newline character at specified position in content
+ * @param {String} content
+ * @param {Number} pos
+ * @return {String}
+ */
+ function getNewline(content, pos) {
+ return content.charAt(pos) == '\r' && content.charAt(pos + 1) == '\n'
+ ? '\r\n'
+ : content.charAt(pos);
+ }
+
+ return {
+ /**
+ * @param source
+ * @returns
+ * @memberOf emmet.cssParser
+ */
+ lex: function (source) {
+ walker.init(source);
+ tokens = [];
+ while (walker.ch !== false) {
+ tokenize();
+ }
+ return tokens;
+ },
+
+ /**
+ * Tokenizes CSS source
+ * @param {String} source
+ * @returns {Array}
+ */
+ parse: function(source) {
+ // transform tokens
+ var pos = 0;
+ return _.map(this.lex(source), function(token) {
+ if (token.type == 'line') {
+ token.value = getNewline(source, pos);
+ }
+
+ return {
+ type: token.type,
+ start: pos,
+ end: (pos += token.value.length)
+ };
+ });
+ },
+
+ toSource: function (toks) {
+ var i = 0, max = toks.length, t, src = '';
+ for (; i < max; i += 1) {
+ t = toks[i];
+ if (t.type === 'line') {
+ src += '\n';
+ } else {
+ src += t.value;
+ }
+ }
+ return src;
+ }
+ };
+});/**
+ * HTML tokenizer by Marijn Haverbeke
+ * http://codemirror.net/
+ * @constructor
+ * @memberOf __xmlParseDefine
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.define('xmlParser', function(require, _) {
+ var Kludges = {
+ autoSelfClosers : {},
+ implicitlyClosed : {},
+ contextGrabbers : {},
+ doNotIndent : {},
+ allowUnquoted : true,
+ allowMissing : true
+ };
+
+ // Return variables for tokenizers
+ var tagName = null, type = null;
+
+ function inText(stream, state) {
+ function chain(parser) {
+ state.tokenize = parser;
+ return parser(stream, state);
+ }
+
+ var ch = stream.next();
+ if (ch == "<") {
+ if (stream.eat("!")) {
+ if (stream.eat("[")) {
+ if (stream.match("CDATA["))
+ return chain(inBlock("atom", "]]>"));
+ else
+ return null;
+ } else if (stream.match("--"))
+ return chain(inBlock("comment", "-->"));
+ else if (stream.match("DOCTYPE", true, true)) {
+ stream.eatWhile(/[\w\._\-]/);
+ return chain(doctype(1));
+ } else
+ return null;
+ } else if (stream.eat("?")) {
+ stream.eatWhile(/[\w\._\-]/);
+ state.tokenize = inBlock("meta", "?>");
+ return "meta";
+ } else {
+ type = stream.eat("/") ? "closeTag" : "openTag";
+ stream.eatSpace();
+ tagName = "";
+ var c;
+ while ((c = stream.eat(/[^\s\u00a0=<>\"\'\/?]/)))
+ tagName += c;
+ state.tokenize = inTag;
+ return "tag";
+ }
+ } else if (ch == "&") {
+ var ok;
+ if (stream.eat("#")) {
+ if (stream.eat("x")) {
+ ok = stream.eatWhile(/[a-fA-F\d]/) && stream.eat(";");
+ } else {
+ ok = stream.eatWhile(/[\d]/) && stream.eat(";");
+ }
+ } else {
+ ok = stream.eatWhile(/[\w\.\-:]/) && stream.eat(";");
+ }
+ return ok ? "atom" : "error";
+ } else {
+ stream.eatWhile(/[^&<]/);
+ return "text";
+ }
+ }
+
+ function inTag(stream, state) {
+ var ch = stream.next();
+ if (ch == ">" || (ch == "/" && stream.eat(">"))) {
+ state.tokenize = inText;
+ type = ch == ">" ? "endTag" : "selfcloseTag";
+ return "tag";
+ } else if (ch == "=") {
+ type = "equals";
+ return null;
+ } else if (/[\'\"]/.test(ch)) {
+ state.tokenize = inAttribute(ch);
+ return state.tokenize(stream, state);
+ } else {
+ stream.eatWhile(/[^\s\u00a0=<>\"\'\/?]/);
+ return "word";
+ }
+ }
+
+ function inAttribute(quote) {
+ return function(stream, state) {
+ while (!stream.eol()) {
+ if (stream.next() == quote) {
+ state.tokenize = inTag;
+ break;
+ }
+ }
+ return "string";
+ };
+ }
+
+ function inBlock(style, terminator) {
+ return function(stream, state) {
+ while (!stream.eol()) {
+ if (stream.match(terminator)) {
+ state.tokenize = inText;
+ break;
+ }
+ stream.next();
+ }
+ return style;
+ };
+ }
+
+ function doctype(depth) {
+ return function(stream, state) {
+ var ch;
+ while ((ch = stream.next()) != null) {
+ if (ch == "<") {
+ state.tokenize = doctype(depth + 1);
+ return state.tokenize(stream, state);
+ } else if (ch == ">") {
+ if (depth == 1) {
+ state.tokenize = inText;
+ break;
+ } else {
+ state.tokenize = doctype(depth - 1);
+ return state.tokenize(stream, state);
+ }
+ }
+ }
+ return "meta";
+ };
+ }
+
+ var curState = null, setStyle;
+ function pass() {
+ for (var i = arguments.length - 1; i >= 0; i--)
+ curState.cc.push(arguments[i]);
+ }
+
+ function cont() {
+ pass.apply(null, arguments);
+ return true;
+ }
+
+ function pushContext(tagName, startOfLine) {
+ var noIndent = Kludges.doNotIndent.hasOwnProperty(tagName)
+ || (curState.context && curState.context.noIndent);
+ curState.context = {
+ prev : curState.context,
+ tagName : tagName,
+ indent : curState.indented,
+ startOfLine : startOfLine,
+ noIndent : noIndent
+ };
+ }
+
+ function popContext() {
+ if (curState.context)
+ curState.context = curState.context.prev;
+ }
+
+ function element(type) {
+ if (type == "openTag") {
+ curState.tagName = tagName;
+ return cont(attributes, endtag(curState.startOfLine));
+ } else if (type == "closeTag") {
+ var err = false;
+ if (curState.context) {
+ if (curState.context.tagName != tagName) {
+ if (Kludges.implicitlyClosed.hasOwnProperty(curState.context.tagName.toLowerCase())) {
+ popContext();
+ }
+ err = !curState.context || curState.context.tagName != tagName;
+ }
+ } else {
+ err = true;
+ }
+
+ if (err)
+ setStyle = "error";
+ return cont(endclosetag(err));
+ }
+ return cont();
+ }
+
+ function endtag(startOfLine) {
+ return function(type) {
+ if (type == "selfcloseTag"
+ || (type == "endTag" && Kludges.autoSelfClosers
+ .hasOwnProperty(curState.tagName
+ .toLowerCase()))) {
+ maybePopContext(curState.tagName.toLowerCase());
+ return cont();
+ }
+ if (type == "endTag") {
+ maybePopContext(curState.tagName.toLowerCase());
+ pushContext(curState.tagName, startOfLine);
+ return cont();
+ }
+ return cont();
+ };
+ }
+
+ function endclosetag(err) {
+ return function(type) {
+ if (err)
+ setStyle = "error";
+ if (type == "endTag") {
+ popContext();
+ return cont();
+ }
+ setStyle = "error";
+ return cont(arguments.callee);
+ };
+ }
+
+ function maybePopContext(nextTagName) {
+ var parentTagName;
+ while (true) {
+ if (!curState.context) {
+ return;
+ }
+ parentTagName = curState.context.tagName.toLowerCase();
+ if (!Kludges.contextGrabbers.hasOwnProperty(parentTagName)
+ || !Kludges.contextGrabbers[parentTagName].hasOwnProperty(nextTagName)) {
+ return;
+ }
+ popContext();
+ }
+ }
+
+ function attributes(type) {
+ if (type == "word") {
+ setStyle = "attribute";
+ return cont(attribute, attributes);
+ }
+ if (type == "endTag" || type == "selfcloseTag")
+ return pass();
+ setStyle = "error";
+ return cont(attributes);
+ }
+
+ function attribute(type) {
+ if (type == "equals")
+ return cont(attvalue, attributes);
+ if (!Kludges.allowMissing)
+ setStyle = "error";
+ return (type == "endTag" || type == "selfcloseTag") ? pass()
+ : cont();
+ }
+
+ function attvalue(type) {
+ if (type == "string")
+ return cont(attvaluemaybe);
+ if (type == "word" && Kludges.allowUnquoted) {
+ setStyle = "string";
+ return cont();
+ }
+ setStyle = "error";
+ return (type == "endTag" || type == "selfCloseTag") ? pass()
+ : cont();
+ }
+
+ function attvaluemaybe(type) {
+ if (type == "string")
+ return cont(attvaluemaybe);
+ else
+ return pass();
+ }
+
+ function startState() {
+ return {
+ tokenize : inText,
+ cc : [],
+ indented : 0,
+ startOfLine : true,
+ tagName : null,
+ context : null
+ };
+ }
+
+ function token(stream, state) {
+ if (stream.sol()) {
+ state.startOfLine = true;
+ state.indented = 0;
+ }
+
+ if (stream.eatSpace())
+ return null;
+
+ setStyle = type = tagName = null;
+ var style = state.tokenize(stream, state);
+ state.type = type;
+ if ((style || type) && style != "comment") {
+ curState = state;
+ while (true) {
+ var comb = state.cc.pop() || element;
+ if (comb(type || style))
+ break;
+ }
+ }
+ state.startOfLine = false;
+ return setStyle || style;
+ }
+
+ return {
+ /**
+ * @memberOf emmet.xmlParser
+ * @returns
+ */
+ parse: function(data, offset) {
+ offset = offset || 0;
+ var state = startState();
+ var stream = require('stringStream').create(data);
+ var tokens = [];
+ while (!stream.eol()) {
+ tokens.push({
+ type: token(stream, state),
+ start: stream.start + offset,
+ end: stream.pos + offset
+ });
+ stream.start = stream.pos;
+ }
+
+ return tokens;
+ }
+ };
+});
+/*!
+ * string_score.js: String Scoring Algorithm 0.1.10
+ *
+ * http://joshaven.com/string_score
+ * https://github.com/joshaven/string_score
+ *
+ * Copyright (C) 2009-2011 Joshaven Potter remove_empty
to true to filter
+ * empty lines
+ * @param {String} text Text to split
+ * @param {Boolean} removeEmpty Remove empty lines from result
+ * @return {Array}
+ */
+ splitByLines: function(text, removeEmpty) {
+ // IE fails to split string by regexp,
+ // need to normalize newlines first
+ // Also, Mozilla's Rhiho JS engine has a weird newline bug
+ var nl = this.getNewline();
+ var lines = (text || '')
+ .replace(/\r\n/g, '\n')
+ .replace(/\n\r/g, '\n')
+ .replace(/\r/g, '\n')
+ .replace(/\n/g, nl)
+ .split(nl);
+
+ if (removeEmpty) {
+ lines = _.filter(lines, function(line) {
+ return line.length && !!this.trim(line);
+ }, this);
+ }
+
+ return lines;
+ },
+
+ /**
+ * Normalizes newline character: replaces newlines in text
+ * with newline defined in preferences
+ * @param {String} text
+ * @returns {String}
+ */
+ normalizeNewline: function(text) {
+ return this.splitByLines(text).join(this.getNewline());
+ },
+
+ /**
+ * Repeats string howMany
times
+ * @param {String} str
+ * @param {Number} how_many
+ * @return {String}
+ */
+ repeatString: function(str, howMany) {
+ var result = [];
+
+ for (var i = 0; i < howMany; i++)
+ result.push(str);
+
+ return result.join('');
+ },
+
+ /**
+ * Indents text with padding
+ * @param {String} text Text to indent
+ * @param {String} pad Padding size (number) or padding itself (string)
+ * @return {String}
+ */
+ padString: function(text, pad) {
+ var padStr = (_.isNumber(pad))
+ ? this.repeatString(require('resources').getVariable('indentation') || '\t', pad)
+ : pad;
+
+ var result = [];
+
+ var lines = this.splitByLines(text);
+ var nl = this.getNewline();
+
+ result.push(lines[0]);
+ for (var j = 1; j < lines.length; j++)
+ result.push(nl + padStr + lines[j]);
+
+ return result.join('');
+ },
+
+ /**
+ * Pad string with zeroes
+ * @param {String} str String to pad
+ * @param {Number} pad Desired string length
+ * @return {String}
+ */
+ zeroPadString: function(str, pad) {
+ var padding = '';
+ var il = str.length;
+
+ while (pad > il++) padding += '0';
+ return padding + str;
+ },
+
+ /**
+ * Removes padding at the beginning of each text's line
+ * @param {String} text
+ * @param {String} pad
+ */
+ unindentString: function(text, pad) {
+ var lines = this.splitByLines(text);
+ for (var i = 0; i < lines.length; i++) {
+ if (lines[i].search(pad) == 0)
+ lines[i] = lines[i].substr(pad.length);
+ }
+
+ return lines.join(this.getNewline());
+ },
+
+ /**
+ * Replaces unescaped symbols in str
. For example, the '$' symbol
+ * will be replaced in 'item$count', but not in 'item\$count'.
+ * @param {String} str Original string
+ * @param {String} symbol Symbol to replace
+ * @param {String} replace Symbol replacement. Might be a function that
+ * returns new value
+ * @return {String}
+ */
+ replaceUnescapedSymbol: function(str, symbol, replace) {
+ var i = 0;
+ var il = str.length;
+ var sl = symbol.length;
+ var matchCount = 0;
+
+ while (i < il) {
+ if (str.charAt(i) == '\\') {
+ // escaped symbol, skip next character
+ i += sl + 1;
+ } else if (str.substr(i, sl) == symbol) {
+ // have match
+ var curSl = sl;
+ matchCount++;
+ var newValue = replace;
+ if (_.isFunction(replace)) {
+ var replaceData = replace(str, symbol, i, matchCount);
+ if (replaceData) {
+ curSl = replaceData[0].length;
+ newValue = replaceData[1];
+ } else {
+ newValue = false;
+ }
+ }
+
+ if (newValue === false) { // skip replacement
+ i++;
+ continue;
+ }
+
+ str = str.substring(0, i) + newValue + str.substring(i + curSl);
+ // adjust indexes
+ il = str.length;
+ i += newValue.length;
+ } else {
+ i++;
+ }
+ }
+
+ return str;
+ },
+
+ /**
+ * Replace variables like ${var} in string
+ * @param {String} str
+ * @param {Object} vars Variable set (defaults to variables defined in
+ * snippets.json
) or variable resolver (Function
)
+ * @return {String}
+ */
+ replaceVariables: function(str, vars) {
+ vars = vars || {};
+ var resolver = _.isFunction(vars) ? vars : function(str, p1) {
+ return p1 in vars ? vars[p1] : null;
+ };
+
+ var res = require('resources');
+ return require('tabStops').processText(str, {
+ variable: function(data) {
+ var newValue = resolver(data.token, data.name, data);
+ if (newValue === null) {
+ // try to find variable in resources
+ newValue = res.getVariable(data.name);
+ }
+
+ if (newValue === null || _.isUndefined(newValue))
+ // nothing found, return token itself
+ newValue = data.token;
+ return newValue;
+ }
+ });
+ },
+
+ /**
+ * Replaces '$' character in string assuming it might be escaped with '\'
+ * @param {String} str String where caracter should be replaced
+ * @param {String} value Replace value. Might be a Function
+ * @return {String}
+ */
+ replaceCounter: function(str, value) {
+ var symbol = '$';
+ // in case we received strings from Java, convert the to native strings
+ str = String(str);
+ value = String(value);
+ var that = this;
+ return this.replaceUnescapedSymbol(str, symbol, function(str, symbol, pos, matchNum){
+ if (str.charAt(pos + 1) == '{' || that.isNumeric(str.charAt(pos + 1)) ) {
+ // it's a variable, skip it
+ return false;
+ }
+
+ // replace sequense of $ symbols with padded number
+ var j = pos + 1;
+ while(str.charAt(j) == '$' && str.charAt(j + 1) != '{') j++;
+ return [str.substring(pos, j), that.zeroPadString(value, j - pos)];
+ });
+ },
+
+ /**
+ * Check if string matches against reTag
regexp. This
+ * function may be used to test if provided string contains HTML tags
+ * @param {String} str
+ * @returns {Boolean}
+ */
+ matchesTag: function(str) {
+ return this.reTag.test(str || '');
+ },
+
+ /**
+ * Escapes special characters used in Emmet, like '$', '|', etc.
+ * Use this method before passing to actions like "Wrap with Abbreviation"
+ * to make sure that existing special characters won't be altered
+ * @param {String} text
+ * @return {String}
+ */
+ escapeText: function(text) {
+ return text.replace(/([\$\\])/g, '\\$1');
+ },
+
+ /**
+ * Unescapes special characters used in Emmet, like '$', '|', etc.
+ * @param {String} text
+ * @return {String}
+ */
+ unescapeText: function(text) {
+ return text.replace(/\\(.)/g, '$1');
+ },
+
+ /**
+ * Returns caret placeholder
+ * @returns {String}
+ */
+ getCaretPlaceholder: function() {
+ return _.isFunction(caretPlaceholder)
+ ? caretPlaceholder.apply(this, arguments)
+ : caretPlaceholder;
+ },
+
+ /**
+ * Sets new representation for carets in generated output
+ * @param {String} value New caret placeholder. Might be a
+ * Function
+ */
+ setCaretPlaceholder: function(value) {
+ caretPlaceholder = value;
+ },
+
+ /**
+ * Returns line padding
+ * @param {String} line
+ * @return {String}
+ */
+ getLinePadding: function(line) {
+ return (line.match(/^(\s+)/) || [''])[0];
+ },
+
+ /**
+ * Helper function that returns padding of line of pos
+ * position in content
+ * @param {String} content
+ * @param {Number} pos
+ * @returns {String}
+ */
+ getLinePaddingFromPosition: function(content, pos) {
+ var lineRange = this.findNewlineBounds(content, pos);
+ return this.getLinePadding(lineRange.substring(content));
+ },
+
+ /**
+ * Escape special regexp chars in string, making it usable for creating dynamic
+ * regular expressions
+ * @param {String} str
+ * @return {String}
+ */
+ escapeForRegexp: function(str) {
+ var specials = new RegExp("[.*+?|()\\[\\]{}\\\\]", "g"); // .*+?|()[]{}\
+ return str.replace(specials, "\\$&");
+ },
+
+ /**
+ * Make decimal number look good: convert it to fixed precision end remove
+ * traling zeroes
+ * @param {Number} num
+ * @param {Number} fracion Fraction numbers (default is 2)
+ * @return {String}
+ */
+ prettifyNumber: function(num, fraction) {
+ return num.toFixed(typeof fraction == 'undefined' ? 2 : fraction).replace(/\.?0+$/, '');
+ },
+
+ /**
+ * A simple mutable string shim, optimized for faster text concatenation
+ * @param {String} value Initial value
+ * @returns {StringBuilder}
+ */
+ stringBuilder: function(value) {
+ return new StringBuilder(value);
+ },
+
+ /**
+ * Replace substring of str
with value
+ * @param {String} str String where to replace substring
+ * @param {String} value New substring value
+ * @param {Number} start Start index of substring to replace. May also
+ * be a Range
object: in this case, the end
+ * argument is not required
+ * @param {Number} end End index of substring to replace. If ommited,
+ * start
argument is used
+ */
+ replaceSubstring: function(str, value, start, end) {
+ if (_.isObject(start) && 'end' in start) {
+ end = start.end;
+ start = start.start;
+ }
+
+ if (_.isString(end))
+ end = start + end.length;
+
+ if (_.isUndefined(end))
+ end = start;
+
+ if (start < 0 || start > str.length)
+ return str;
+
+ return str.substring(0, start) + value + str.substring(end);
+ },
+
+ /**
+ * Narrows down text range, adjusting selection to non-space characters
+ * @param {String} text
+ * @param {Number} start Starting range in text
where
+ * slection should be adjusted. Can also be any object that is accepted
+ * by Range
class
+ * @return {Range}
+ */
+ narrowToNonSpace: function(text, start, end) {
+ var range = require('range').create(start, end);
+
+ var reSpace = /[\s\n\r\u00a0]/;
+ // narrow down selection until first non-space character
+ while (range.start < range.end) {
+ if (!reSpace.test(text.charAt(range.start)))
+ break;
+
+ range.start++;
+ }
+
+ while (range.end > range.start) {
+ range.end--;
+ if (!reSpace.test(text.charAt(range.end))) {
+ range.end++;
+ break;
+ }
+ }
+
+ return range;
+ },
+
+ /**
+ * Find start and end index of text line for from
index
+ * @param {String} text
+ * @param {Number} from
+ */
+ findNewlineBounds: function(text, from) {
+ var len = text.length,
+ start = 0,
+ end = len - 1;
+
+ // search left
+ for (var i = from - 1; i > 0; i--) {
+ var ch = text.charAt(i);
+ if (ch == '\n' || ch == '\r') {
+ start = i + 1;
+ break;
+ }
+ }
+ // search right
+ for (var j = from; j < len; j++) {
+ var ch = text.charAt(j);
+ if (ch == '\n' || ch == '\r') {
+ end = j;
+ break;
+ }
+ }
+
+ return require('range').create(start, end - start);
+ },
+
+ /**
+ * Deep merge of two or more objects. Taken from jQuery.extend()
+ */
+ deepMerge: function() {
+ var options, name, src, copy, copyIsArray, clone,
+ target = arguments[0] || {},
+ i = 1,
+ length = arguments.length;
+
+
+ // Handle case when target is a string or something (possible in deep copy)
+ if (!_.isObject(target) && !_.isFunction(target)) {
+ target = {};
+ }
+
+ for ( ; i < length; i++ ) {
+ // Only deal with non-null/undefined values
+ if ( (options = arguments[ i ]) != null ) {
+ // Extend the base object
+ for ( name in options ) {
+ src = target[ name ];
+ copy = options[ name ];
+
+ // Prevent never-ending loop
+ if ( target === copy ) {
+ continue;
+ }
+
+ // Recurse if we're merging plain objects or arrays
+ if ( copy && ( _.isObject(copy) || (copyIsArray = _.isArray(copy)) ) ) {
+ if ( copyIsArray ) {
+ copyIsArray = false;
+ clone = src && _.isArray(src) ? src : [];
+
+ } else {
+ clone = src && _.isObject(src) ? src : {};
+ }
+
+ // Never move original objects, clone them
+ target[ name ] = this.deepMerge(clone, copy );
+
+ // Don't bring in undefined values
+ } else if ( copy !== undefined ) {
+ target[ name ] = copy;
+ }
+ }
+ }
+ }
+
+ // Return the modified object
+ return target;
+ }
+ };
+});
+/**
+ * Helper module to work with ranges
+ * @constructor
+ * @memberOf __rangeDefine
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.define('range', function(require, _) {
+ function cmp(a, b, op) {
+ switch (op) {
+ case 'eq':
+ case '==':
+ return a === b;
+ case 'lt':
+ case '<':
+ return a < b;
+ case 'lte':
+ case '<=':
+ return a <= b;
+ case 'gt':
+ case '>':
+ return a > b;
+ case 'gte':
+ case '>=':
+ return a >= b;
+ }
+ }
+
+
+ /**
+ * @type Range
+ * @constructor
+ * @param {Object} start
+ * @param {Number} len
+ */
+ function Range(start, len) {
+ if (_.isObject(start) && 'start' in start) {
+ // create range from object stub
+ this.start = Math.min(start.start, start.end);
+ this.end = Math.max(start.start, start.end);
+ } else if (_.isArray(start)) {
+ this.start = start[0];
+ this.end = start[1];
+ } else {
+ len = _.isString(len) ? len.length : +len;
+ this.start = start;
+ this.end = start + len;
+ }
+ }
+
+ Range.prototype = {
+ length: function() {
+ return Math.abs(this.end - this.start);
+ },
+
+ /**
+ * Returns true
if passed range is equals to current one
+ * @param {Range} range
+ * @returns {Boolean}
+ */
+ equal: function(range) {
+ return this.cmp(range, 'eq', 'eq');
+// return this.start === range.start && this.end === range.end;
+ },
+
+ /**
+ * Shifts indexes position with passed delat
+ * @param {Number} delta
+ * @returns {Range} range itself
+ */
+ shift: function(delta) {
+ this.start += delta;
+ this.end += delta;
+ return this;
+ },
+
+ /**
+ * Check if two ranges are overlapped
+ * @param {Range} range
+ * @returns {Boolean}
+ */
+ overlap: function(range) {
+ return range.start <= this.end && range.end >= this.start;
+ },
+
+ /**
+ * Finds intersection of two ranges
+ * @param {Range} range
+ * @returns {Range} null
if ranges does not overlap
+ */
+ intersection: function(range) {
+ if (this.overlap(range)) {
+ var start = Math.max(range.start, this.start);
+ var end = Math.min(range.end, this.end);
+ return new Range(start, end - start);
+ }
+
+ return null;
+ },
+
+ /**
+ * Returns the union of the thow ranges.
+ * @param {Range} range
+ * @returns {Range} null
if ranges are not overlapped
+ */
+ union: function(range) {
+ if (this.overlap(range)) {
+ var start = Math.min(range.start, this.start);
+ var end = Math.max(range.end, this.end);
+ return new Range(start, end - start);
+ }
+
+ return null;
+ },
+
+ /**
+ * Returns a Boolean value that indicates whether a specified position
+ * is in a given range.
+ * @param {Number} loc
+ */
+ inside: function(loc) {
+ return this.cmp(loc, 'lte', 'gt');
+// return this.start <= loc && this.end > loc;
+ },
+
+ /**
+ * Returns a Boolean value that indicates whether a specified position
+ * is in a given range, but not equals bounds.
+ * @param {Number} loc
+ */
+ contains: function(loc) {
+ return this.cmp(loc, 'lt', 'gt');
+ },
+
+ /**
+ * Check if current range completely includes specified one
+ * @param {Range} r
+ * @returns {Boolean}
+ */
+ include: function(r) {
+ return this.cmp(loc, 'lte', 'gte');
+// return this.start <= r.start && this.end >= r.end;
+ },
+
+ /**
+ * Low-level comparision method
+ * @param {Number} loc
+ * @param {String} left Left comparison operator
+ * @param {String} right Right comaprison operator
+ */
+ cmp: function(loc, left, right) {
+ var a, b;
+ if (loc instanceof Range) {
+ a = loc.start;
+ b = loc.end;
+ } else {
+ a = b = loc;
+ }
+
+ return cmp(this.start, a, left || '<=') && cmp(this.end, b, right || '>');
+ },
+
+ /**
+ * Returns substring of specified str
for current range
+ * @param {String} str
+ * @returns {String}
+ */
+ substring: function(str) {
+ return this.length() > 0
+ ? str.substring(this.start, this.end)
+ : '';
+ },
+
+ /**
+ * Creates copy of current range
+ * @returns {Range}
+ */
+ clone: function() {
+ return new Range(this.start, this.length());
+ },
+
+ /**
+ * @returns {Array}
+ */
+ toArray: function() {
+ return [this.start, this.end];
+ },
+
+ toString: function() {
+ return '{' + this.start + ', ' + this.length() + '}';
+ }
+ };
+
+ return {
+ /**
+ * Creates new range object instance
+ * @param {Object} start Range start or array with 'start' and 'end'
+ * as two first indexes or object with 'start' and 'end' properties
+ * @param {Number} len Range length or string to produce range from
+ * @returns {Range}
+ * @memberOf emmet.range
+ */
+ create: function(start, len) {
+ if (_.isUndefined(start) || start === null)
+ return null;
+
+ if (start instanceof Range)
+ return start;
+
+ if (_.isObject(start) && 'start' in start && 'end' in start) {
+ len = start.end - start.start;
+ start = start.start;
+ }
+
+ return new Range(start, len);
+ },
+
+ /**
+ * Range
object factory, the same as this.create()
+ * but last argument represents end of range, not length
+ * @returns {Range}
+ */
+ create2: function(start, end) {
+ if (_.isNumber(start) && _.isNumber(end)) {
+ end -= start;
+ }
+
+ return this.create(start, end);
+ }
+ };
+});/**
+ * Utility module that provides ordered storage of function handlers.
+ * Many Emmet modules' functionality can be extended/overridden by custom
+ * function. This modules provides unified storage of handler functions, their
+ * management and execution
+ *
+ * @constructor
+ * @memberOf __handlerListDefine
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.define('handlerList', function(require, _) {
+ /**
+ * @type HandlerList
+ * @constructor
+ */
+ function HandlerList() {
+ this._list = [];
+ }
+
+ HandlerList.prototype = {
+ /**
+ * Adds function handler
+ * @param {Function} fn Handler
+ * @param {Object} options Handler options. Possible values are:Number
) – order in handler list. Handlers
+ * with higher order value will be executed earlier.
+ */
+ add: function(fn, options) {
+ this._list.push(_.extend({order: 0}, options || {}, {fn: fn}));
+ },
+
+ /**
+ * Removes handler from list
+ * @param {Function} fn
+ */
+ remove: function(fn) {
+ this._list = _.without(this._list, _.find(this._list, function(item) {
+ return item.fn === fn;
+ }));
+ },
+
+ /**
+ * Returns ordered list of handlers. By default, handlers
+ * with the same order
option returned in reverse order,
+ * i.e. the latter function was added into the handlers list, the higher
+ * it will be in the returned array
+ * @returns {Array}
+ */
+ list: function() {
+ return _.sortBy(this._list, 'order').reverse();
+ },
+
+ /**
+ * Returns ordered list of handler functions
+ * @returns {Array}
+ */
+ listFn: function() {
+ return _.pluck(this.list(), 'fn');
+ },
+
+ /**
+ * Executes handler functions in their designated order. If function
+ * returns skipVal
, meaning that function was unable to
+ * handle passed args
, the next function will be executed
+ * and so on.
+ * @param {Object} skipValue If function returns this value, execute
+ * next handler.
+ * @param {Array} args Arguments to pass to handler function
+ * @returns {Boolean} Whether any of registered handlers performed
+ * successfully
+ */
+ exec: function(skipValue, args) {
+ args = args || [];
+ var result = null;
+ _.find(this.list(), function(h) {
+ result = h.fn.apply(h, args);
+ if (result !== skipValue)
+ return true;
+ });
+
+ return result;
+ }
+ };
+
+ return {
+ /**
+ * Factory method that produces HandlerList
instance
+ * @returns {HandlerList}
+ * @memberOf handlerList
+ */
+ create: function() {
+ return new HandlerList();
+ }
+ };
+});/**
+ * Helper class for convenient token iteration
+ */
+emmet.define('tokenIterator', function(require, _) {
+ /**
+ * @type TokenIterator
+ * @param {Array} tokens
+ * @type TokenIterator
+ * @constructor
+ */
+ function TokenIterator(tokens) {
+ /** @type Array */
+ this.tokens = tokens;
+ this._position = 0;
+ this.reset();
+ }
+
+ TokenIterator.prototype = {
+ next: function() {
+ if (this.hasNext()) {
+ var token = this.tokens[++this._i];
+ this._position = token.start;
+ return token;
+ }
+
+ return null;
+ },
+
+ current: function() {
+ return this.tokens[this._i];
+ },
+
+ position: function() {
+ return this._position;
+ },
+
+ hasNext: function() {
+ return this._i < this._il - 1;
+ },
+
+ reset: function() {
+ this._i = -1;
+ this._il = this.tokens.length;
+ },
+
+ item: function() {
+ return this.tokens[this._i];
+ },
+
+ itemNext: function() {
+ return this.tokens[this._i + 1];
+ },
+
+ itemPrev: function() {
+ return this.tokens[this._i - 1];
+ },
+
+ nextUntil: function(type, callback) {
+ var token;
+ var test = _.isString(type)
+ ? function(t){return t.type == type;}
+ : type;
+
+ while (token = this.next()) {
+ if (callback)
+ callback.call(this, token);
+ if (test.call(this, token))
+ break;
+ }
+ }
+ };
+
+ return {
+ create: function(tokens) {
+ return new TokenIterator(tokens);
+ }
+ };
+});/**
+ * A trimmed version of CodeMirror's StringStream module for string parsing
+ */
+emmet.define('stringStream', function(require, _) {
+ /**
+ * @type StringStream
+ * @constructor
+ * @param {String} string
+ */
+ function StringStream(string) {
+ this.pos = this.start = 0;
+ this.string = string;
+ }
+
+ StringStream.prototype = {
+ /**
+ * Returns true only if the stream is at the end of the line.
+ * @returns {Boolean}
+ */
+ eol: function() {
+ return this.pos >= this.string.length;
+ },
+
+ /**
+ * Returns true only if the stream is at the start of the line
+ * @returns {Boolean}
+ */
+ sol: function() {
+ return this.pos == 0;
+ },
+
+ /**
+ * Returns the next character in the stream without advancing it.
+ * Will return undefined
at the end of the line.
+ * @returns {String}
+ */
+ peek: function() {
+ return this.string.charAt(this.pos);
+ },
+
+ /**
+ * Returns the next character in the stream and advances it.
+ * Also returns undefined
when no more characters are available.
+ * @returns {String}
+ */
+ next: function() {
+ if (this.pos < this.string.length)
+ return this.string.charAt(this.pos++);
+ },
+
+ /**
+ * match can be a character, a regular expression, or a function that
+ * takes a character and returns a boolean. If the next character in the
+ * stream 'matches' the given argument, it is consumed and returned.
+ * Otherwise, undefined is returned.
+ * @param {Object} match
+ * @returns {String}
+ */
+ eat: function(match) {
+ var ch = this.string.charAt(this.pos), ok;
+ if (typeof match == "string")
+ ok = ch == match;
+ else
+ ok = ch && (match.test ? match.test(ch) : match(ch));
+
+ if (ok) {
+ ++this.pos;
+ return ch;
+ }
+ },
+
+ /**
+ * Repeatedly calls eat
with the given argument, until it
+ * fails. Returns true
if any characters were eaten.
+ * @param {Object} match
+ * @returns {Boolean}
+ */
+ eatWhile: function(match) {
+ var start = this.pos;
+ while (this.eat(match)) {}
+ return this.pos > start;
+ },
+
+ /**
+ * Shortcut for eatWhile
when matching white-space.
+ * @returns {Boolean}
+ */
+ eatSpace: function() {
+ var start = this.pos;
+ while (/[\s\u00a0]/.test(this.string.charAt(this.pos)))
+ ++this.pos;
+ return this.pos > start;
+ },
+
+ /**
+ * Moves the position to the end of the line.
+ */
+ skipToEnd: function() {
+ this.pos = this.string.length;
+ },
+
+ /**
+ * Skips to the next occurrence of the given character, if found on the
+ * current line (doesn't advance the stream if the character does not
+ * occur on the line). Returns true if the character was found.
+ * @param {String} ch
+ * @returns {Boolean}
+ */
+ skipTo: function(ch) {
+ var found = this.string.indexOf(ch, this.pos);
+ if (found > -1) {
+ this.pos = found;
+ return true;
+ }
+ },
+
+ /**
+ * Skips to close
character which is pair to open
+ * character, considering possible pair nesting. This function is used
+ * to consume pair of characters, like opening and closing braces
+ * @param {String} open
+ * @param {String} close
+ * @returns {Boolean} Returns true
if pair was successfully
+ * consumed
+ */
+ skipToPair: function(open, close) {
+ var braceCount = 0, ch;
+ var pos = this.pos, len = this.string.length;
+ while (pos < len) {
+ ch = this.string.charAt(pos++);
+ if (ch == open) {
+ braceCount++;
+ } else if (ch == close) {
+ braceCount--;
+ if (braceCount < 1) {
+ this.pos = pos;
+ return true;
+ }
+ }
+ }
+
+ return false;
+ },
+
+ /**
+ * 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 : function(n) {
+ this.pos -= n;
+ },
+
+ /**
+ * Act like a multi-character eat
—if consume
is true or
+ * not given—or a look-ahead that doesn't update the stream position—if
+ * it is false. pattern
can be either a string or a
+ * regular expression starting with ^. When it is a string,
+ * caseInsensitive
can be set to true to make the match
+ * case-insensitive. When successfully matching a regular expression,
+ * the returned value will be the array returned by match
,
+ * in case you need to extract matched groups.
+ *
+ * @param {RegExp} pattern
+ * @param {Boolean} consume
+ * @param {Boolean} caseInsensitive
+ * @returns
+ */
+ match: function(pattern, consume, caseInsensitive) {
+ if (typeof pattern == "string") {
+ var cased = caseInsensitive
+ ? function(str) {return str.toLowerCase();}
+ : function(str) {return str;};
+
+ if (cased(this.string).indexOf(cased(pattern), this.pos) == this.pos) {
+ if (consume !== false)
+ this.pos += pattern.length;
+ return true;
+ }
+ } else {
+ var match = this.string.slice(this.pos).match(pattern);
+ if (match && consume !== false)
+ this.pos += match[0].length;
+ return match;
+ }
+ },
+
+ /**
+ * Get the string between the start of the current token and the
+ * current stream position.
+ * @returns {String}
+ */
+ current: function() {
+ return this.string.slice(this.start, this.pos);
+ }
+ };
+
+ return {
+ create: function(string) {
+ return new StringStream(string);
+ }
+ };
+});/**
+ * Parsed resources (snippets, abbreviations, variables, etc.) for Emmet.
+ * Contains convenient method to get access for snippets with respect of
+ * inheritance. Also provides ability to store data in different vocabularies
+ * ('system' and 'user') for fast and safe resource update
+ * @author Sergey Chikuyonok (serge.che@gmail.com)
+ * @link http://chikuyonok.ru
+ *
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.define('resources', function(require, _) {
+ var VOC_SYSTEM = 'system';
+ var VOC_USER = 'user';
+
+ var cache = {};
+
+ /** Regular expression for XML tag matching */
+ var reTag = /^<(\w+\:?[\w\-]*)((?:\s+[\w\:\-]+\s*=\s*(['"]).*?\3)*)\s*(\/?)>/;
+
+ var systemSettings = {};
+ var userSettings = {};
+
+ /** @type HandlerList List of registered abbreviation resolvers */
+ var resolvers = require('handlerList').create();
+
+ /**
+ * Normalizes caret plceholder in passed text: replaces | character with
+ * default caret placeholder
+ * @param {String} text
+ * @returns {String}
+ */
+ function normalizeCaretPlaceholder(text) {
+ var utils = require('utils');
+ return utils.replaceUnescapedSymbol(text, '|', utils.getCaretPlaceholder());
+ }
+
+ function parseItem(name, value, type) {
+ value = normalizeCaretPlaceholder(value);
+
+ if (type == 'snippets') {
+ return require('elements').create('snippet', value);
+ }
+
+ if (type == 'abbreviations') {
+ return parseAbbreviation(name, value);
+ }
+ }
+
+ /**
+ * Parses single abbreviation
+ * @param {String} key Abbreviation name
+ * @param {String} value Abbreviation value
+ * @return {Object}
+ */
+ function parseAbbreviation(key, value) {
+ key = require('utils').trim(key);
+ var elements = require('elements');
+ var m;
+ if (m = reTag.exec(value)) {
+ return elements.create('element', m[1], m[2], m[4] == '/');
+ } else {
+ // assume it's reference to another abbreviation
+ return elements.create('reference', value);
+ }
+ }
+
+ /**
+ * Normalizes snippet key name for better fuzzy search
+ * @param {String} str
+ * @returns {String}
+ */
+ function normalizeName(str) {
+ return str.replace(/:$/, '').replace(/:/g, '-');
+ }
+
+ return {
+ /**
+ * Sets new unparsed data for specified settings vocabulary
+ * @param {Object} data
+ * @param {String} type Vocabulary type ('system' or 'user')
+ * @memberOf resources
+ */
+ setVocabulary: function(data, type) {
+ cache = {};
+ if (type == VOC_SYSTEM)
+ systemSettings = data;
+ else
+ userSettings = data;
+ },
+
+ /**
+ * Returns resource vocabulary by its name
+ * @param {String} name Vocabulary name ('system' or 'user')
+ * @return {Object}
+ */
+ getVocabulary: function(name) {
+ return name == VOC_SYSTEM ? systemSettings : userSettings;
+ },
+
+ /**
+ * Returns resource (abbreviation, snippet, etc.) matched for passed
+ * abbreviation
+ * @param {TreeNode} node
+ * @param {String} syntax
+ * @returns {Object}
+ */
+ getMatchedResource: function(node, syntax) {
+ return resolvers.exec(null, _.toArray(arguments))
+ || this.findSnippet(syntax, node.name());
+ },
+
+ /**
+ * Returns variable value
+ * @return {String}
+ */
+ getVariable: function(name) {
+ return (this.getSection('variables') || {})[name];
+ },
+
+ /**
+ * Store runtime variable in user storage
+ * @param {String} name Variable name
+ * @param {String} value Variable value
+ */
+ setVariable: function(name, value){
+ var voc = this.getVocabulary('user') || {};
+ if (!('variables' in voc))
+ voc.variables = {};
+
+ voc.variables[name] = value;
+ this.setVocabulary(voc, 'user');
+ },
+
+ /**
+ * Check if there are resources for specified syntax
+ * @param {String} syntax
+ * @return {Boolean}
+ */
+ hasSyntax: function(syntax) {
+ return syntax in this.getVocabulary(VOC_USER)
+ || syntax in this.getVocabulary(VOC_SYSTEM);
+ },
+
+ /**
+ * Registers new abbreviation resolver.
+ * @param {Function} fn Abbreviation resolver which will receive
+ * abbreviation as first argument and should return parsed abbreviation
+ * object if abbreviation has handled successfully, null
+ * otherwise
+ * @param {Object} options Options list as described in
+ * {@link HandlerList#add()} method
+ */
+ addResolver: function(fn, options) {
+ resolvers.add(fn, options);
+ },
+
+ removeResolver: function(fn) {
+ resolvers.remove(fn);
+ },
+
+ /**
+ * Returns actual section data, merged from both
+ * system and user data
+ * @param {String} name Section name (syntax)
+ * @param {String} ...args Subsections
+ * @returns
+ */
+ getSection: function(name) {
+ if (!name)
+ return null;
+
+ if (!(name in cache)) {
+ cache[name] = require('utils').deepMerge({}, systemSettings[name], userSettings[name]);
+ }
+
+ var data = cache[name], subsections = _.rest(arguments), key;
+ while (data && (key = subsections.shift())) {
+ if (key in data) {
+ data = data[key];
+ } else {
+ return null;
+ }
+ }
+
+ return data;
+ },
+
+ /**
+ * Recursively searches for a item inside top level sections (syntaxes)
+ * with respect of `extends` attribute
+ * @param {String} topSection Top section name (syntax)
+ * @param {String} subsection Inner section name
+ * @returns {Object}
+ */
+ findItem: function(topSection, subsection) {
+ var data = this.getSection(topSection);
+ while (data) {
+ if (subsection in data)
+ return data[subsection];
+
+ data = this.getSection(data['extends']);
+ }
+ },
+
+ /**
+ * Recursively searches for a snippet definition inside syntax section.
+ * Definition is searched inside `snippets` and `abbreviations`
+ * subsections
+ * @param {String} syntax Top-level section name (syntax)
+ * @param {String} name Snippet name
+ * @returns {Object}
+ */
+ findSnippet: function(syntax, name, memo) {
+ if (!syntax || !name)
+ return null;
+
+ memo = memo || [];
+
+ var names = [name];
+ // create automatic aliases to properties with colons,
+ // e.g. pos-a == pos:a
+ if (~name.indexOf('-'))
+ names.push(name.replace(/\-/g, ':'));
+
+ var data = this.getSection(syntax), matchedItem = null;
+ _.find(['snippets', 'abbreviations'], function(sectionName) {
+ var data = this.getSection(syntax, sectionName);
+ if (data) {
+ return _.find(names, function(n) {
+ if (data[n])
+ return matchedItem = parseItem(n, data[n], sectionName);
+ });
+ }
+ }, this);
+
+ memo.push(syntax);
+ if (!matchedItem && data['extends'] && !_.include(memo, data['extends'])) {
+ // try to find item in parent syntax section
+ return this.findSnippet(data['extends'], name, memo);
+ }
+
+ return matchedItem;
+ },
+
+ /**
+ * Performs fuzzy search of snippet definition
+ * @param {String} syntax Top-level section name (syntax)
+ * @param {String} name Snippet name
+ * @returns
+ */
+ fuzzyFindSnippet: function(syntax, name, minScore) {
+ minScore = minScore || 0.3;
+
+ var payload = this.getAllSnippets(syntax);
+ var sc = require('string-score');
+
+ name = normalizeName(name);
+ var scores = _.map(payload, function(value, key) {
+ return {
+ key: key,
+ score: sc.score(value.nk, name, 0.1)
+ };
+ });
+
+ var result = _.last(_.sortBy(scores, 'score'));
+ if (result && result.score >= minScore) {
+ var k = result.key;
+ return payload[k].parsedValue;
+// return parseItem(k, payload[k].value, payload[k].type);
+ }
+ },
+
+ /**
+ * Returns plain dictionary of all available abbreviations and snippets
+ * for specified syntax with respect of inheritance
+ * @param {String} syntax
+ * @returns {Object}
+ */
+ getAllSnippets: function(syntax) {
+ var cacheKey = 'all-' + syntax;
+ if (!cache[cacheKey]) {
+ var stack = [], sectionKey = syntax;
+ var memo = [];
+
+ do {
+ var section = this.getSection(sectionKey);
+ if (!section)
+ break;
+
+ _.each(['snippets', 'abbreviations'], function(sectionName) {
+ var stackItem = {};
+ _.each(section[sectionName] || null, function(v, k) {
+ stackItem[k] = {
+ nk: normalizeName(k),
+ value: v,
+ parsedValue: parseItem(k, v, sectionName),
+ type: sectionName
+ };
+ });
+
+ stack.push(stackItem);
+ });
+
+ memo.push(sectionKey);
+ sectionKey = section['extends'];
+ } while (sectionKey && !_.include(memo, sectionKey));
+
+
+ cache[cacheKey] = _.extend.apply(_, stack.reverse());
+ }
+
+ return cache[cacheKey];
+ }
+ };
+});/**
+ * Module describes and performs Emmet actions. The actions themselves are
+ * defined in actions folder
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.define('actions', function(require, _, zc) {
+ var actions = {};
+
+ /**
+ * “Humanizes” action name, makes it more readable for people
+ * @param {String} name Action name (like 'expand_abbreviation')
+ * @return Humanized name (like 'Expand Abbreviation')
+ */
+ function humanizeActionName(name) {
+ return require('utils').trim(name.charAt(0).toUpperCase()
+ + name.substring(1).replace(/_[a-z]/g, function(str) {
+ return ' ' + str.charAt(1).toUpperCase();
+ }));
+ }
+
+ return {
+ /**
+ * Registers new action
+ * @param {String} name Action name
+ * @param {Function} fn Action function
+ * @param {Object} options Custom action options:String
) – Human-readable action name.
+ * May contain '/' symbols as submenu separatorsBoolean
) – Indicates whether action
+ * should be displayed in menu (getMenu()
method)
+ *
+ * @memberOf actions
+ */
+ add: function(name, fn, options) {
+ name = name.toLowerCase();
+ options = options || {};
+ if (!options.label) {
+ options.label = humanizeActionName(name);
+ }
+
+ actions[name] = {
+ name: name,
+ fn: fn,
+ options: options
+ };
+ },
+
+ /**
+ * Returns action object
+ * @param {String} name Action name
+ * @returns {Object}
+ */
+ get: function(name) {
+ return actions[name.toLowerCase()];
+ },
+
+ /**
+ * Runs Emmet action. For list of available actions and their
+ * arguments see actions folder.
+ * @param {String} name Action name
+ * @param {Array} args Additional arguments. It may be array of arguments
+ * or inline arguments. The first argument should be IEmmetEditor
instance
+ * @returns {Boolean} Status of performed operation, true
+ * means action was performed successfully.
+ * @example
+ * emmet.require('actions').run('expand_abbreviation', editor);
+ * emmet.require('actions').run('wrap_with_abbreviation', [editor, 'div']);
+ */
+ run: function(name, args) {
+ if (!_.isArray(args)) {
+ args = _.rest(arguments);
+ }
+
+ var action = this.get(name);
+ if (action) {
+ return action.fn.apply(emmet, args);
+ } else {
+ emmet.log('Action "%s" is not defined', name);
+ return false;
+ }
+ },
+
+ /**
+ * Returns all registered actions as object
+ * @returns {Object}
+ */
+ getAll: function() {
+ return actions;
+ },
+
+ /**
+ * Returns all registered actions as array
+ * @returns {Array}
+ */
+ getList: function() {
+ return _.values(this.getAll());
+ },
+
+ /**
+ * Returns actions list as structured menu. If action has label,
+ * it will be splitted by '/' symbol into submenus (for example:
+ * CSS/Reflect Value) and grouped with other items
+ * @param {Array} skipActions List of action identifiers that should be
+ * skipped from menu
+ * @returns {Array}
+ */
+ getMenu: function(skipActions) {
+ var result = [];
+ skipActions = skipActions || [];
+ _.each(this.getList(), function(action) {
+ if (action.options.hidden || _.include(skipActions, action.name))
+ return;
+
+ var actionName = humanizeActionName(action.name);
+ var ctx = result;
+ if (action.options.label) {
+ var parts = action.options.label.split('/');
+ actionName = parts.pop();
+
+ // create submenus, if needed
+ var menuName, submenu;
+ while (menuName = parts.shift()) {
+ submenu = _.find(ctx, function(item) {
+ return item.type == 'submenu' && item.name == menuName;
+ });
+
+ if (!submenu) {
+ submenu = {
+ name: menuName,
+ type: 'submenu',
+ items: []
+ };
+ ctx.push(submenu);
+ }
+
+ ctx = submenu.items;
+ }
+ }
+
+ ctx.push({
+ type: 'action',
+ name: action.name,
+ label: actionName
+ });
+ });
+
+ return result;
+ },
+
+ /**
+ * Returns action name associated with menu item title
+ * @param {String} title
+ * @returns {String}
+ */
+ getActionNameForMenuTitle: function(title, menu) {
+ var item = null;
+ _.find(menu || this.getMenu(), function(val) {
+ if (val.type == 'action') {
+ if (val.label == title || val.name == title) {
+ return item = val.name;
+ }
+ } else {
+ return item = this.getActionNameForMenuTitle(title, val.items);
+ }
+ }, this);
+
+ return item || null;
+ }
+ };
+});/**
+ * Output profile module.
+ * Profile defines how XHTML output data should look like
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.define('profile', function(require, _) {
+ var profiles = {};
+
+ var defaultProfile = {
+ tag_case: 'asis',
+ attr_case: 'asis',
+ attr_quotes: 'double',
+
+ // each tag on new line
+ tag_nl: 'decide',
+
+ // with tag_nl === true, defines if leaf node (e.g. node with no children)
+ // should have formatted line breaks
+ tag_nl_leaf: false,
+
+ place_cursor: true,
+
+ // indent tags
+ indent: true,
+
+ // how many inline elements should be to force line break
+ // (set to 0 to disable)
+ inline_break: 3,
+
+ // use self-closing style for writing empty elements, e.g. caseValue
+ * @param {String} str String to transform
+ * @param {String} caseValue Case value: can be lower,
+ * upper and leave
+ * @returns {String}
+ */
+ function stringCase(str, caseValue) {
+ switch (String(caseValue || '').toLowerCase()) {
+ case 'lower':
+ return str.toLowerCase();
+ case 'upper':
+ return str.toUpperCase();
+ }
+
+ return str;
+ }
+
+ /**
+ * Creates new output profile
+ * @param {String} name Profile name
+ * @param {Object} options Profile options
+ */
+ function createProfile(name, options) {
+ return profiles[name.toLowerCase()] = new OutputProfile(options);
+ }
+
+ function createDefaultProfiles() {
+ createProfile('xhtml');
+ createProfile('html', {self_closing_tag: false});
+ createProfile('xml', {self_closing_tag: true, tag_nl: true});
+ createProfile('plain', {tag_nl: false, indent: false, place_cursor: false});
+ createProfile('line', {tag_nl: false, indent: false, extraFilters: 's'});
+ }
+
+ createDefaultProfiles();
+
+ return {
+ /**
+ * Creates new output profile and adds it into internal dictionary
+ * @param {String} name Profile name
+ * @param {Object} options Profile options
+ * @memberOf emmet.profile
+ * @returns {Object} New profile
+ */
+ create: function(name, options) {
+ if (arguments.length == 2)
+ return createProfile(name, options);
+ else
+ // create profile object only
+ return new OutputProfile(_.defaults(name || {}, defaultProfile));
+ },
+
+ /**
+ * Returns profile by its name. If profile wasn't found, returns
+ * 'plain' profile
+ * @param {String} name Profile name. Might be profile itself
+ * @param {String} syntax. Optional. Current editor syntax. If defined,
+ * profile is searched in resources first, then in predefined profiles
+ * @returns {Object}
+ */
+ get: function(name, syntax) {
+ if (!name && syntax) {
+ // search in user resources first
+ var profile = require('resources').findItem(syntax, 'profile');
+ if (profile) {
+ name = profile;
+ }
+ }
+
+ if (!name) {
+ return profiles.plain;
+ }
+
+ if (name instanceof OutputProfile) {
+ return name;
+ }
+
+ if (_.isString(name) && name.toLowerCase() in profiles) {
+ return profiles[name.toLowerCase()];
+ }
+
+ return this.create(name);
+ },
+
+ /**
+ * Deletes profile with specified name
+ * @param {String} name Profile name
+ */
+ remove: function(name) {
+ name = (name || '').toLowerCase();
+ if (name in profiles)
+ delete profiles[name];
+ },
+
+ /**
+ * Resets all user-defined profiles
+ */
+ reset: function() {
+ profiles = {};
+ createDefaultProfiles();
+ },
+
+ /**
+ * Helper function that converts string case depending on
+ * caseValue
+ * @param {String} str String to transform
+ * @param {String} caseValue Case value: can be lower,
+ * upper and leave
+ * @returns {String}
+ */
+ stringCase: stringCase
+ };
+});/**
+ * Utility module used to prepare text for pasting into back-end editor
+ * @param {Function} require
+ * @param {Underscore} _
+ * @author Sergey Chikuyonok (serge.che@gmail.com) IEmmetFile.read()
)
+ * @return {Object} Object with width
and height
properties
+ */
+ getImageSize: function(stream) {
+ var pngMagicNum = "\211PNG\r\n\032\n",
+ jpgMagicNum = "\377\330",
+ gifMagicNum = "GIF8",
+ nextByte = function() {
+ return stream.charCodeAt(pos++);
+ };
+
+ if (stream.substr(0, 8) === pngMagicNum) {
+ // PNG. Easy peasy.
+ var pos = stream.indexOf('IHDR') + 4;
+
+ return { width: (nextByte() << 24) | (nextByte() << 16) |
+ (nextByte() << 8) | nextByte(),
+ height: (nextByte() << 24) | (nextByte() << 16) |
+ (nextByte() << 8) | nextByte() };
+
+ } else if (stream.substr(0, 4) === gifMagicNum) {
+ pos = 6;
+
+ return {
+ width: nextByte() | (nextByte() << 8),
+ height: nextByte() | (nextByte() << 8)
+ };
+
+ } else if (stream.substr(0, 2) === jpgMagicNum) {
+ pos = 2;
+
+ var l = stream.length;
+ while (pos < l) {
+ if (nextByte() != 0xFF) return;
+
+ var marker = nextByte();
+ if (marker == 0xDA) break;
+
+ var size = (nextByte() << 8) | nextByte();
+
+ if (marker >= 0xC0 && marker <= 0xCF && !(marker & 0x4) && !(marker & 0x8)) {
+ pos += 1;
+ return { height: (nextByte() << 8) | nextByte(),
+ width: (nextByte() << 8) | nextByte() };
+
+ } else {
+ pos += size - 2;
+ }
+ }
+ }
+ },
+
+ /**
+ * Captures context XHTML element from editor under current caret position.
+ * This node can be used as a helper for abbreviation extraction
+ * @param {IEmmetEditor} editor
+ * @returns {Object}
+ */
+ captureContext: function(editor) {
+ var allowedSyntaxes = {'html': 1, 'xml': 1, 'xsl': 1};
+ var syntax = String(editor.getSyntax());
+ if (syntax in allowedSyntaxes) {
+ var content = String(editor.getContent());
+ var tag = require('htmlMatcher').find(content, editor.getCaretPos());
+
+ if (tag && tag.type == 'tag') {
+ var reAttr = /([\w\-:]+)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g;
+ var startTag = tag.open;
+ var tagAttrs = startTag.range.substring(content).replace(/^<[\w\-\:]+/, '');
+// var tagAttrs = startTag.full_tag.replace(/^<[\w\-\:]+/, '');
+ var contextNode = {
+ name: startTag.name,
+ attributes: []
+ };
+
+ // parse attributes
+ var m;
+ while (m = reAttr.exec(tagAttrs)) {
+ contextNode.attributes.push({
+ name: m[1],
+ value: m[2]
+ });
+ }
+
+ return contextNode;
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Find expression bounds in current editor at caret position.
+ * On each character a fn
function will be called and must
+ * return true
if current character meets requirements,
+ * false
otherwise
+ * @param {IEmmetEditor} editor
+ * @param {Function} fn Function to test each character of expression
+ * @return {Range}
+ */
+ findExpressionBounds: function(editor, fn) {
+ var content = String(editor.getContent());
+ var il = content.length;
+ var exprStart = editor.getCaretPos() - 1;
+ var exprEnd = exprStart + 1;
+
+ // start by searching left
+ while (exprStart >= 0 && fn(content.charAt(exprStart), exprStart, content)) exprStart--;
+
+ // then search right
+ while (exprEnd < il && fn(content.charAt(exprEnd), exprEnd, content)) exprEnd++;
+
+ if (exprEnd > exprStart) {
+ return require('range').create([++exprStart, exprEnd]);
+ }
+ },
+
+ /**
+ * @param {IEmmetEditor} editor
+ * @param {Object} data
+ * @returns {Boolean}
+ */
+ compoundUpdate: function(editor, data) {
+ if (data) {
+ var sel = editor.getSelectionRange();
+ editor.replaceContent(data.data, data.start, data.end, true);
+ editor.createSelection(data.caret, data.caret + sel.end - sel.start);
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Common syntax detection method for editors that doesn’t provide any
+ * info about current syntax scope.
+ * @param {IEmmetEditor} editor Current editor
+ * @param {String} hint Any syntax hint that editor can provide
+ * for syntax detection. Default is 'html'
+ * @returns {String}
+ */
+ detectSyntax: function(editor, hint) {
+ var syntax = hint || 'html';
+
+ if (!require('resources').hasSyntax(syntax)) {
+ syntax = 'html';
+ }
+
+ if (syntax == 'html' && (this.isStyle(editor) || this.isInlineCSS(editor))) {
+ syntax = 'css';
+ }
+
+ return syntax;
+ },
+
+ /**
+ * Common method for detecting output profile
+ * @param {IEmmetEditor} editor
+ * @returns {String}
+ */
+ detectProfile: function(editor) {
+ var syntax = editor.getSyntax();
+
+ // get profile from syntax definition
+ var profile = require('resources').findItem(syntax, 'profile');
+ if (profile) {
+ return profile;
+ }
+
+ switch(syntax) {
+ case 'xml':
+ case 'xsl':
+ return 'xml';
+ case 'css':
+ if (this.isInlineCSS(editor)) {
+ return 'line';
+ }
+ break;
+ case 'html':
+ var profile = require('resources').getVariable('profile');
+ if (!profile) { // no forced profile, guess from content
+ // html or xhtml?
+ profile = this.isXHTML(editor) ? 'xhtml': 'html';
+ }
+
+ return profile;
+ }
+
+ return 'xhtml';
+ },
+
+ /**
+ * Tries to detect if current document is XHTML one.
+ * @param {IEmmetEditor} editor
+ * @returns {Boolean}
+ */
+ isXHTML: function(editor) {
+ return editor.getContent().search(/]+XHTML/i) != -1;
+ },
+
+ /**
+ * Check if current caret position is inside <style> tag
+ * @param {IEmmetEditor} editor
+ * @returns
+ */
+ isStyle: function(editor) {
+ var content = String(editor.getContent());
+ var caretPos = editor.getCaretPos();
+ var tag = require('htmlMatcher').tag(content, caretPos);
+ return tag && tag.open.name.toLowerCase() == 'style'
+ && tag.innerRange.cmp(caretPos, 'lte', 'gte');
+ },
+
+ /**
+ * Check if current caret position is inside "style" attribute of HTML
+ * element
+ * @param {IEmmetEditor} editor
+ * @returns {Boolean}
+ */
+ isInlineCSS: function(editor) {
+ var content = String(editor.getContent());
+ var caretPos = editor.getCaretPos();
+ var tree = require('xmlEditTree').parseFromPosition(content, caretPos, true);
+ if (tree) {
+ var attr = tree.itemFromPosition(caretPos, true);
+ return attr && attr.name().toLowerCase() == 'style'
+ && attr.valueRange(true).cmp(caretPos, 'lte', 'gte');
+ }
+
+ return false;
+ }
+ };
+});/**
+ * Utility functions to work with AbbreviationNode
as HTML element
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.define('abbreviationUtils', function(require, _) {
+ return {
+ /**
+ * Check if passed abbreviation node has matched snippet resource
+ * @param {AbbreviationNode} node
+ * @returns {Boolean}
+ * @memberOf abbreviationUtils
+ */
+ isSnippet: function(node) {
+ return require('elements').is(node.matchedResource(), 'snippet');
+ },
+
+ /**
+ * Test if passed node is unary (no closing tag)
+ * @param {AbbreviationNode} node
+ * @return {Boolean}
+ */
+ isUnary: function(node) {
+ var r = node.matchedResource();
+ if (node.children.length || this.isSnippet(node))
+ return false;
+
+ return r && r.is_empty || require('tagName').isEmptyElement(node.name());
+ },
+
+ /**
+ * Test if passed node is inline-level (like <strong>, <img>)
+ * @param {AbbreviationNode} node
+ * @return {Boolean}
+ */
+ isInline: function(node) {
+ return node.isTextNode()
+ || !node.name()
+ || require('tagName').isInlineLevel(node.name());
+ },
+
+ /**
+ * Test if passed node is block-level
+ * @param {AbbreviationNode} node
+ * @return {Boolean}
+ */
+ isBlock: function(node) {
+ return this.isSnippet(node) || !this.isInline(node);
+ },
+
+ /**
+ * Test if given node is a snippet
+ * @param {AbbreviationNode} node
+ * @return {Boolean}
+ */
+ isSnippet: function(node) {
+ return require('elements').is(node.matchedResource(), 'snippet');
+ },
+
+ /**
+ * This function tests if passed node content contains HTML tags.
+ * This function is mostly used for output formatting
+ * @param {AbbreviationNode} node
+ * @returns {Boolean}
+ */
+ hasTagsInContent: function(node) {
+ return require('utils').matchesTag(node.content);
+ },
+
+ /**
+ * Test if current element contains block-level children
+ * @param {AbbreviationNode} node
+ * @return {Boolean}
+ */
+ hasBlockChildren: function(node) {
+ return (this.hasTagsInContent(node) && this.isBlock(node))
+ || _.any(node.children, function(child) {
+ return this.isBlock(child);
+ }, this);
+ },
+
+ /**
+ * Utility function that inserts content instead of ${child}
+ * variables on text
+ * @param {String} text Text where child content should be inserted
+ * @param {String} childContent Content to insert
+ * @param {Object} options
+ * @returns {String
+ */
+ insertChildContent: function(text, childContent, options) {
+ options = _.extend({
+ keepVariable: true,
+ appendIfNoChild: true
+ }, options || {});
+
+ var childVariableReplaced = false;
+ var utils = require('utils');
+ text = utils.replaceVariables(text, function(variable, name, data) {
+ var output = variable;
+ if (name == 'child') {
+ // add correct indentation
+ output = utils.padString(childContent, utils.getLinePaddingFromPosition(text, data.start));
+ childVariableReplaced = true;
+ if (options.keepVariable)
+ output += variable;
+ }
+
+ return output;
+ });
+
+ if (!childVariableReplaced && options.appendIfNoChild) {
+ text += childContent;
+ }
+
+ return text;
+ }
+ };
+});/**
+ * @author Sergey Chikuyonok (serge.che@gmail.com)
+ * @link http://chikuyonok.ru
+ */
+emmet.define('base64', function(require, _) {
+ var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
+
+ return {
+ /**
+ * Encodes data using base64 algorithm
+ * @author Tyler Akins (http://rumkin.com)
+ * @param {String} input
+ * @returns {String}
+ * @memberOf emmet.base64
+ */
+ encode : function(input) {
+ var output = [];
+ var chr1, chr2, chr3, enc1, enc2, enc3, enc4, cdp1, cdp2, cdp3;
+ var i = 0, il = input.length, b64 = chars;
+
+ while (i < il) {
+
+ cdp1 = input.charCodeAt(i++);
+ cdp2 = input.charCodeAt(i++);
+ cdp3 = input.charCodeAt(i++);
+
+ chr1 = cdp1 & 0xff;
+ chr2 = cdp2 & 0xff;
+ chr3 = cdp3 & 0xff;
+
+ enc1 = chr1 >> 2;
+ enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
+ enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
+ enc4 = chr3 & 63;
+
+ if (isNaN(cdp2)) {
+ enc3 = enc4 = 64;
+ } else if (isNaN(cdp3)) {
+ enc4 = 64;
+ }
+
+ output.push(b64.charAt(enc1) + b64.charAt(enc2) + b64.charAt(enc3) + b64.charAt(enc4));
+ }
+
+ return output.join('');
+ },
+
+ /**
+ * Decodes string using MIME base64 algorithm
+ *
+ * @author Tyler Akins (http://rumkin.com)
+ * @param {String} data
+ * @return {String}
+ */
+ decode : function(data) {
+ var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, tmpArr = [];
+ var b64 = chars, il = data.length;
+
+ if (!data) {
+ return data;
+ }
+
+ data += '';
+
+ do { // unpack four hexets into three octets using index points in b64
+ h1 = b64.indexOf(data.charAt(i++));
+ h2 = b64.indexOf(data.charAt(i++));
+ h3 = b64.indexOf(data.charAt(i++));
+ h4 = b64.indexOf(data.charAt(i++));
+
+ bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;
+
+ o1 = bits >> 16 & 0xff;
+ o2 = bits >> 8 & 0xff;
+ o3 = bits & 0xff;
+
+ if (h3 == 64) {
+ tmpArr[ac++] = String.fromCharCode(o1);
+ } else if (h4 == 64) {
+ tmpArr[ac++] = String.fromCharCode(o1, o2);
+ } else {
+ tmpArr[ac++] = String.fromCharCode(o1, o2, o3);
+ }
+ } while (i < il);
+
+ return tmpArr.join('');
+ }
+ };
+});/**
+ * HTML matcher: takes string and searches for HTML tag pairs for given position
+ *
+ * Unlike “classic” matchers, it parses content from the specified
+ * position, not from the start, so it may work even outside HTML documents
+ * (for example, inside strings of programming languages like JavaScript, Python
+ * etc.)
+ * @constructor
+ * @memberOf __htmlMatcherDefine
+ */
+emmet.define('htmlMatcher', function(require, _) {
+ // Regular Expressions for parsing tags and attributes
+ var reOpenTag = /^<([\w\:\-]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/;
+ var reCloseTag = /^<\/([\w\:\-]+)[^>]*>/;
+
+ function openTag(i, match) {
+ return {
+ name: match[1],
+ selfClose: !!match[3],
+ /** @type Range */
+ range: require('range').create(i, match[0]),
+ type: 'open'
+ };
+ }
+
+ function closeTag(i, match) {
+ return {
+ name: match[1],
+ /** @type Range */
+ range: require('range').create(i, match[0]),
+ type: 'close'
+ };
+ }
+
+ function comment(i, match) {
+ return {
+ /** @type Range */
+ range: require('range').create(i, _.isNumber(match) ? match - i : match[0]),
+ type: 'comment'
+ };
+ }
+
+ /**
+ * Creates new tag matcher session
+ * @param {String} text
+ */
+ function createMatcher(text) {
+ var memo = {}, m;
+ return {
+ /**
+ * Test if given position matches opening tag
+ * @param {Number} i
+ * @returns {Object} Matched tag object
+ */
+ open: function(i) {
+ var m = this.matches(i);
+ return m && m.type == 'open' ? m : null;
+ },
+
+ /**
+ * Test if given position matches closing tag
+ * @param {Number} i
+ * @returns {Object} Matched tag object
+ */
+ close: function(i) {
+ var m = this.matches(i);
+ return m && m.type == 'close' ? m : null;
+ },
+
+ /**
+ * Matches either opening or closing tag for given position
+ * @param i
+ * @returns
+ */
+ matches: function(i) {
+ var key = 'p' + i;
+
+ if (!(key in memo)) {
+ if (text.charAt(i) == '<') {
+ var substr = text.slice(i);
+ if (m = substr.match(reOpenTag)) {
+ memo[key] = openTag(i, m);
+ } else if (m = substr.match(reCloseTag)) {
+ memo[key] = closeTag(i, m);
+ } else {
+ // remember that given position contains no valid tag
+ memo[key] = false;
+ }
+ }
+ }
+
+ return memo[key];
+ },
+
+ /**
+ * Returns original text
+ * @returns {String}
+ */
+ text: function() {
+ return text;
+ }
+ };
+ }
+
+ function matches(text, pos, pattern) {
+ return text.substring(pos, pos + pattern.length) == pattern;
+ }
+
+ /**
+ * Search for closing pair of opening tag
+ * @param {Object} open Open tag instance
+ * @param {Object} matcher Matcher instance
+ */
+ function findClosingPair(open, matcher) {
+ var stack = [], tag = null;
+ var text = matcher.text();
+
+ for (var pos = open.range.end, len = text.length; pos < len; pos++) {
+ if (matches(text, pos, '')) {
+ pos = j + 3;
+ break;
+ }
+ }
+ }
+
+ if (tag = matcher.matches(pos)) {
+ if (tag.type == 'open' && !tag.selfClose) {
+ stack.push(tag.name);
+ } else if (tag.type == 'close') {
+ if (!stack.length) { // found valid pair?
+ return tag.name == open.name ? tag : null;
+ }
+
+ // check if current closing tag matches previously opened one
+ if (_.last(stack) == tag.name) {
+ stack.pop();
+ } else {
+ var found = false;
+ while (stack.length && !found) {
+ var last = stack.pop();
+ if (last == tag.name) {
+ found = true;
+ }
+ }
+
+ if (!stack.length && !found) {
+ return tag.name == open.name ? tag : null;
+ }
+ }
+ }
+ }
+
+ }
+ }
+
+ return {
+ /**
+ * Main function: search for tag pair in text
for given
+ * position
+ * @memberOf htmlMatcher
+ * @param {String} text
+ * @param {Number} pos
+ * @returns {Object}
+ */
+ find: function(text, pos) {
+ var range = require('range');
+ var matcher = createMatcher(text);
+ var open = null, close = null;
+
+ for (var i = pos; i >= 0; i--) {
+ if (open = matcher.open(i)) {
+ // found opening tag
+ if (open.selfClose && open.range.cmp(pos, 'lt', 'gt')) {
+ // inside self-closing tag
+ break;
+ }
+
+ close = findClosingPair(open, matcher);
+ if (close) {
+ // found closing tag.
+ var r = range.create2(open.range.start, close.range.end);
+ if (r.contains(pos)) {
+ break;
+ }
+ } else if (open.range.contains(pos)) {
+ // we inside empty HTML tag like find()
method, but restricts matched result
+ * to tag
type
+ * @param {String} text
+ * @param {Number} pos
+ * @returns {Object}
+ */
+ tag: function(text, pos) {
+ var result = this.find(text, pos);
+ if (result && result.type == 'tag') {
+ return result;
+ }
+ }
+ };
+});/**
+ * Utility module for handling tabstops tokens generated by Emmet's
+ * "Expand Abbreviation" action. The main extract
method will take
+ * raw text (for example: ${0} some ${1:text}), find all tabstops
+ * occurrences, replace them with tokens suitable for your editor of choice and
+ * return object with processed text and list of found tabstops and their ranges.
+ * For sake of portability (Objective-C/Java) the tabstops list is a plain
+ * sorted array with plain objects.
+ *
+ * Placeholders with the same are meant to be linked in your editor.
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.define('tabStops', function(require, _) {
+ /**
+ * Global placeholder value, automatically incremented by
+ * variablesResolver()
function
+ */
+ var startPlaceholderNum = 100;
+
+ var tabstopIndex = 0;
+
+ var defaultOptions = {
+ replaceCarets: false,
+ escape: function(ch) {
+ return '\\' + ch;
+ },
+ tabstop: function(data) {
+ return data.token;
+ },
+ variable: function(data) {
+ return data.token;
+ }
+ };
+
+ // XXX register output processor that will upgrade tabstops of parsed node
+ // in order to prevent tabstop index conflicts
+ require('abbreviationParser').addOutputProcessor(function(text, node, type) {
+ var maxNum = 0;
+ var tabstops = require('tabStops');
+ var utils = require('utils');
+
+ var tsOptions = {
+ tabstop: function(data) {
+ var group = parseInt(data.group);
+ if (group == 0)
+ return '${0}';
+
+ if (group > maxNum) maxNum = group;
+ if (data.placeholder) {
+ // respect nested placeholders
+ var ix = group + tabstopIndex;
+ var placeholder = tabstops.processText(data.placeholder, tsOptions);
+ return '${' + ix + ':' + placeholder + '}';
+ } else {
+ return '${' + (group + tabstopIndex) + '}';
+ }
+ }
+ };
+
+ // upgrade tabstops
+ text = tabstops.processText(text, tsOptions);
+
+ // resolve variables
+ text = utils.replaceVariables(text, tabstops.variablesResolver(node));
+
+ tabstopIndex += maxNum + 1;
+ return text;
+ });
+
+ return {
+ /**
+ * Main function that looks for a tabstops in provided text
+ * and returns a processed version of text
with expanded
+ * placeholders and list of tabstops found.
+ * @param {String} text Text to process
+ * @param {Object} options List of processor options:Boolean
— replace all default
+ * caret placeholders (like {%::emmet-caret::%}) with ${0:caret}Function
— function that handle escaped
+ * characters (mostly '$'). By default, it returns the character itself
+ * to be displayed as is in output, but sometimes you will use
+ * extract
method as intermediate solution for further
+ * processing and want to keep character escaped. Thus, you should override
+ * escape
method to return escaped symbol (e.g. '\\$')Function
– a tabstop handler. Receives
+ * a single argument – an object describing token: its position, number
+ * group, placeholder and token itself. Should return a replacement
+ * string that will appear in final output
+ *
+ * variable : Function
– variable handler. Receives
+ * a single argument – an object describing token: its position, name
+ * and original token itself. Should return a replacement
+ * string that will appear in final output
+ *
+ * @returns {Object} Object with processed text
property
+ * and array of tabstops
found
+ * @memberOf tabStops
+ */
+ extract: function(text, options) {
+ // prepare defaults
+ var utils = require('utils');
+ var placeholders = {carets: ''};
+ var marks = [];
+
+ options = _.extend({}, defaultOptions, options, {
+ tabstop: function(data) {
+ var token = data.token;
+ var ret = '';
+ if (data.placeholder == 'cursor') {
+ marks.push({
+ start: data.start,
+ end: data.start + token.length,
+ group: 'carets',
+ value: ''
+ });
+ } else {
+ // unify placeholder value for single group
+ if ('placeholder' in data)
+ placeholders[data.group] = data.placeholder;
+
+ if (data.group in placeholders)
+ ret = placeholders[data.group];
+
+ marks.push({
+ start: data.start,
+ end: data.start + token.length,
+ group: data.group,
+ value: ret
+ });
+ }
+
+ return token;
+ }
+ });
+
+ if (options.replaceCarets) {
+ text = text.replace(new RegExp( utils.escapeForRegexp( utils.getCaretPlaceholder() ), 'g'), '${0:cursor}');
+ }
+
+ // locate tabstops and unify group's placeholders
+ text = this.processText(text, options);
+
+ // now, replace all tabstops with placeholders
+ var buf = utils.stringBuilder(), lastIx = 0;
+ var tabStops = _.map(marks, function(mark) {
+ buf.append(text.substring(lastIx, mark.start));
+
+ var pos = buf.length;
+ var ph = placeholders[mark.group] || '';
+
+ buf.append(ph);
+ lastIx = mark.end;
+
+ return {
+ group: mark.group,
+ start: pos,
+ end: pos + ph.length
+ };
+ });
+
+ buf.append(text.substring(lastIx));
+
+ return {
+ text: buf.toString(),
+ tabstops: _.sortBy(tabStops, 'start')
+ };
+ },
+
+ /**
+ * Text processing routine. Locates escaped characters and tabstops and
+ * replaces them with values returned by handlers defined in
+ * options
+ * @param {String} text
+ * @param {Object} options See extract
method options
+ * description
+ * @returns {String}
+ */
+ processText: function(text, options) {
+ options = _.extend({}, defaultOptions, options);
+
+ var buf = require('utils').stringBuilder();
+ /** @type StringStream */
+ var stream = require('stringStream').create(text);
+ var ch, m, a;
+
+ while (ch = stream.next()) {
+ if (ch == '\\' && !stream.eol()) {
+ // handle escaped character
+ buf.append(options.escape(stream.next()));
+ continue;
+ }
+
+ a = ch;
+
+ if (ch == '$') {
+ // looks like a tabstop
+ stream.start = stream.pos - 1;
+
+ if (m = stream.match(/^[0-9]+/)) {
+ // it's $N
+ a = options.tabstop({
+ start: buf.length,
+ group: stream.current().substr(1),
+ token: stream.current()
+ });
+ } else if (m = stream.match(/^\{([a-z_\-][\w\-]*)\}/)) {
+ // ${variable}
+ a = options.variable({
+ start: buf.length,
+ name: m[1],
+ token: stream.current()
+ });
+ } else if (m = stream.match(/^\{([0-9]+)(:.+?)?\}/, false)) {
+ // ${N:value} or ${N} placeholder
+ // parse placeholder, including nested ones
+ stream.skipToPair('{', '}');
+
+ var obj = {
+ start: buf.length,
+ group: m[1],
+ token: stream.current()
+ };
+
+ var placeholder = obj.token.substring(obj.group.length + 2, obj.token.length - 1);
+
+ if (placeholder) {
+ obj.placeholder = placeholder.substr(1);
+ }
+
+ a = options.tabstop(obj);
+ }
+ }
+
+ buf.append(a);
+ }
+
+ return buf.toString();
+ },
+
+ /**
+ * Upgrades tabstops in output node in order to prevent naming conflicts
+ * @param {AbbreviationNode} node
+ * @param {Number} offset Tab index offset
+ * @returns {Number} Maximum tabstop index in element
+ */
+ upgrade: function(node, offset) {
+ var maxNum = 0;
+ var options = {
+ tabstop: function(data) {
+ var group = parseInt(data.group);
+ if (group > maxNum) maxNum = group;
+
+ if (data.placeholder)
+ return '${' + (group + offset) + ':' + data.placeholder + '}';
+ else
+ return '${' + (group + offset) + '}';
+ }
+ };
+
+ _.each(['start', 'end', 'content'], function(p) {
+ node[p] = this.processText(node[p], options);
+ }, this);
+
+ return maxNum;
+ },
+
+ /**
+ * Helper function that produces a callback function for
+ * replaceVariables()
method from {@link utils}
+ * module. This callback will replace variable definitions (like
+ * ${var_name}) with their value defined in resource module,
+ * or outputs tabstop with variable name otherwise.
+ * @param {AbbreviationNode} node Context node
+ * @returns {Function}
+ */
+ variablesResolver: function(node) {
+ var placeholderMemo = {};
+ var res = require('resources');
+ return function(str, varName) {
+ // do not mark `child` variable as placeholder – it‘s a reserved
+ // variable name
+ if (varName == 'child')
+ return str;
+
+ if (varName == 'cursor')
+ return require('utils').getCaretPlaceholder();
+
+ var attr = node.attribute(varName);
+ if (!_.isUndefined(attr))
+ return attr;
+
+ var varValue = res.getVariable(varName);
+ if (varValue)
+ return varValue;
+
+ // output as placeholder
+ if (!placeholderMemo[varName])
+ placeholderMemo[varName] = startPlaceholderNum++;
+
+ return '${' + placeholderMemo[varName] + ':' + varName + '}';
+ };
+ },
+
+ resetPlaceholderCounter: function() {
+ console.log('deprecated');
+ startPlaceholderNum = 100;
+ },
+
+ /**
+ * Resets global tabstop index. When parsed tree is converted to output
+ * string (AbbreviationNode.toString()
), all tabstops
+ * defined in snippets and elements are upgraded in order to prevent
+ * naming conflicts of nested. For example, ${1}
of a node
+ * should not be linked with the same placehilder of the child node.
+ * By default, AbbreviationNode.toString()
automatically
+ * upgrades tabstops of the same index for each node and writes maximum
+ * tabstop index into the tabstopIndex
variable. To keep
+ * this variable at reasonable value, it is recommended to call
+ * resetTabstopIndex()
method each time you expand variable
+ * @returns
+ */
+ resetTabstopIndex: function() {
+ tabstopIndex = 0;
+ startPlaceholderNum = 100;
+ }
+ };
+});/**
+ * Common module's preferences storage. This module
+ * provides general storage for all module preferences, their description and
+ * default values.define
method.
+ * @param {String} name Preference name. You can also pass object
+ * with many options
+ * @param {Object} value Preference default value
+ * @memberOf preferences
+ */
+ set: function(name, value) {
+ var prefs = name;
+ if (_.isString(name)) {
+ prefs = {};
+ prefs[name] = value;
+ }
+
+ _.each(prefs, function(v, k) {
+ if (!(k in defaults)) {
+ throw 'Property "' + k + '" is not defined. You should define it first with `define` method of current module';
+ }
+
+ // do not set value if it equals to default value
+ if (v !== defaults[k].value) {
+ // make sure we have value of correct type
+ switch (typeof defaults[k].value) {
+ case 'boolean':
+ v = toBoolean(v);
+ break;
+ case 'number':
+ v = parseInt(v + '', 10) || 0;
+ break;
+ default: // convert to string
+ v += '';
+ }
+
+ preferences[k] = v;
+ } else if (k in preferences) {
+ delete preferences[k];
+ }
+ });
+ },
+
+ /**
+ * Returns preference value
+ * @param {String} name
+ * @returns {String} Returns undefined
if preference is
+ * not defined
+ */
+ get: function(name) {
+ if (name in preferences)
+ return preferences[name];
+
+ if (name in defaults)
+ return defaults[name].value;
+
+ return void 0;
+ },
+
+ /**
+ * Returns comma-separated preference value as array of values
+ * @param {String} name
+ * @returns {Array} Returns undefined
if preference is
+ * not defined, null
if string cannot be converted to array
+ */
+ getArray: function(name) {
+ var val = this.get(name);
+ if (!_.isUndefined(val)) {
+ val = _.map(val.split(','), require('utils').trim);
+ if (!val.length)
+ val = null;
+ }
+
+ return val;
+ },
+
+ /**
+ * Returns comma and colon-separated preference value as dictionary
+ * @param {String} name
+ * @returns {Object}
+ */
+ getDict: function(name) {
+ var result = {};
+ _.each(this.getArray(name), function(val) {
+ var parts = val.split(':');
+ result[parts[0]] = parts[1];
+ });
+
+ return result;
+ },
+
+ /**
+ * Returns description of preference item
+ * @param {String} name Preference name
+ * @returns {Object}
+ */
+ description: function(name) {
+ return name in defaults ? defaults[name].description : void 0;
+ },
+
+ /**
+ * Completely removes specified preference(s)
+ * @param {String} name Preference name (or array of names)
+ */
+ remove: function(name) {
+ if (!_.isArray(name))
+ name = [name];
+
+ _.each(name, function(key) {
+ if (key in preferences)
+ delete preferences[key];
+
+ if (key in defaults)
+ delete defaults[key];
+ });
+ },
+
+ /**
+ * Returns sorted list of all available properties
+ * @returns {Array}
+ */
+ list: function() {
+ return _.map(_.keys(defaults).sort(), function(key) {
+ return {
+ name: key,
+ value: this.get(key),
+ type: typeof defaults[key].value,
+ description: defaults[key].description
+ };
+ }, this);
+ },
+
+ /**
+ * Loads user-defined preferences from JSON
+ * @param {Object} json
+ * @returns
+ */
+ load: function(json) {
+ _.each(json, function(value, key) {
+ this.set(key, value);
+ }, this);
+ },
+
+ /**
+ * Returns hash of user-modified preferences
+ * @returns {Object}
+ */
+ exportModified: function() {
+ return _.clone(preferences);
+ },
+
+ /**
+ * Reset to defaults
+ * @returns
+ */
+ reset: function() {
+ preferences = {};
+ },
+
+ /**
+ * For unit testing: use empty storage
+ */
+ _startTest: function() {
+ _dbgDefaults = defaults;
+ _dbgPreferences = preferences;
+ defaults = {};
+ preferences = {};
+ },
+
+ /**
+ * For unit testing: restore original storage
+ */
+ _stopTest: function() {
+ defaults = _dbgDefaults;
+ preferences = _dbgPreferences;
+ }
+ };
+});/**
+ * Module for handling filters
+ * @param {Function} require
+ * @param {Underscore} _
+ * @author Sergey Chikuyonok (serge.che@gmail.com) String
+ * @param {Object} profile Output profile, defined in profile
+ * module. Filters defined it profile are not used, profile
+ * is passed to filter function
+ * @memberOf emmet.filters
+ * @returns {AbbreviationNode}
+ */
+ apply: function(tree, filters, profile) {
+ var utils = require('utils');
+ profile = require('profile').get(profile);
+
+ _.each(list(filters), function(filter) {
+ var name = utils.trim(filter.toLowerCase());
+ if (name && name in registeredFilters) {
+ tree = registeredFilters[name](tree, profile);
+ }
+ });
+
+ return tree;
+ },
+
+ /**
+ * Composes list of filters that should be applied to a tree, based on
+ * passed data
+ * @param {String} syntax Syntax name ('html', 'css', etc.)
+ * @param {Object} profile Output profile
+ * @param {String} additionalFilters List or pipe-separated
+ * string of additional filters to apply
+ * @returns {Array}
+ */
+ composeList: function(syntax, profile, additionalFilters) {
+ profile = require('profile').get(profile);
+ var filters = list(profile.filters || require('resources').findItem(syntax, 'filters') || basicFilters);
+
+ if (profile.extraFilters) {
+ filters = filters.concat(list(profile.extraFilters));
+ }
+
+ if (additionalFilters) {
+ filters = filters.concat(list(additionalFilters));
+ }
+
+ if (!filters || !filters.length) {
+ // looks like unknown syntax, apply basic filters
+ filters = list(basicFilters);
+ }
+
+ return filters;
+ },
+
+ /**
+ * Extracts filter list from abbreviation
+ * @param {String} abbr
+ * @returns {Array} Array with cleaned abbreviation and list of
+ * extracted filters
+ */
+ extractFromAbbreviation: function(abbr) {
+ var filters = '';
+ abbr = abbr.replace(/\|([\w\|\-]+)$/, function(str, p1){
+ filters = p1;
+ return '';
+ });
+
+ return [abbr, list(filters)];
+ }
+ };
+});/**
+ * Module that contains factories for element types used by Emmet
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.define('elements', function(require, _) {
+ var factories = {};
+ var reAttrs = /([\w\-]+)\s*=\s*(['"])(.*?)\2/g;
+
+ var result = {
+ /**
+ * Create new element factory
+ * @param {String} name Element identifier
+ * @param {Function} factory Function that produces element of specified
+ * type. The object generated by this factory is automatically
+ * augmented with type
property pointing to element
+ * name
+ * @memberOf elements
+ */
+ add: function(name, factory) {
+ var that = this;
+ factories[name] = function() {
+ var elem = factory.apply(that, arguments);
+ if (elem)
+ elem.type = name;
+
+ return elem;
+ };
+ },
+
+ /**
+ * Returns factory for specified name
+ * @param {String} name
+ * @returns {Function}
+ */
+ get: function(name) {
+ return factories[name];
+ },
+
+ /**
+ * Creates new element with specified type
+ * @param {String} name
+ * @returns {Object}
+ */
+ create: function(name) {
+ var args = [].slice.call(arguments, 1);
+ var factory = this.get(name);
+ return factory ? factory.apply(this, args) : null;
+ },
+
+ /**
+ * Check if passed element is of specified type
+ * @param {Object} elem
+ * @param {String} type
+ * @returns {Boolean}
+ */
+ is: function(elem, type) {
+ return elem && elem.type === type;
+ }
+ };
+
+ // register resource references
+ function commonFactory(value) {
+ return {data: value};
+ }
+
+ /**
+ * Element factory
+ * @param {String} elementName Name of output element
+ * @param {String} attrs Attributes definition. You may also pass
+ * Array
where each contains object with name
+ * and value
properties, or Object
+ * @param {Boolean} isEmpty Is expanded element should be empty
+ */
+ result.add('element', function(elementName, attrs, isEmpty) {
+ var ret = {
+ /** @memberOf __emmetDataElement */
+ name: elementName,
+ is_empty: !!isEmpty
+ };
+
+ if (attrs) {
+ ret.attributes = [];
+ if (_.isArray(attrs)) {
+ ret.attributes = attrs;
+ } else if (_.isString(attrs)) {
+ var m;
+ while (m = reAttrs.exec(attrs)) {
+ ret.attributes.push({
+ name: m[1],
+ value: m[3]
+ });
+ }
+ } else {
+ _.each(attrs, function(value, name) {
+ ret.attributes.push({
+ name: name,
+ value: value
+ });
+ });
+ }
+ }
+
+ return ret;
+ });
+
+ result.add('snippet', commonFactory);
+ result.add('reference', commonFactory);
+ result.add('empty', function() {
+ return {};
+ });
+
+ return result;
+});/**
+ * Abstract implementation of edit tree interface.
+ * Edit tree is a named container of editable “name-value” child elements,
+ * parsed from source
. This container provides convenient methods
+ * for editing/adding/removing child elements. All these update actions are
+ * instantly reflected in the source
code with respect of formatting.
+ * extend
method to create new class and
+ * initialize
method to define custom class constructor.
+ *
+ * @example
+ *
+ * var MyClass = require('editTree').EditElement.extend({
+ * initialize: function() {
+ * // constructor code here
+ * }
+ * });
+ *
+ * var elem = new MyClass();
+ *
+ *
+ *
+ * @param {Function} require
+ * @param {Underscore} _
+ * @constructor
+ * @memberOf __editTreeDefine
+ */
+emmet.define('editTree', function(require, _, core) {
+ var range = require('range').create;
+
+ /**
+ * Named container of edited source
+ * @type EditContainer
+ * @param {String} source
+ * @param {Object} options
+ */
+ function EditContainer(source, options) {
+ this.options = _.extend({offset: 0}, options);
+ /**
+ * Source code of edited structure. All changes in the structure are
+ * immediately reflected into this property
+ */
+ this.source = source;
+
+ /**
+ * List of all editable children
+ * @private
+ */
+ this._children = [];
+
+ /**
+ * Hash of all positions of container
+ * @private
+ */
+ this._positions = {
+ name: 0
+ };
+
+ this.initialize.apply(this, arguments);
+ }
+
+ /**
+ * The self-propagating extend function for classes.
+ * @type Function
+ */
+ EditContainer.extend = core.extend;
+
+ EditContainer.prototype = {
+ /**
+ * Child class constructor
+ */
+ initialize: function() {},
+
+ /**
+ * Replace substring of tag's source
+ * @param {String} value
+ * @param {Number} start
+ * @param {Number} end
+ * @private
+ */
+ _updateSource: function(value, start, end) {
+ // create modification range
+ var r = range(start, _.isUndefined(end) ? 0 : end - start);
+ var delta = value.length - r.length();
+
+ var update = function(obj) {
+ _.each(obj, function(v, k) {
+ if (v >= r.end)
+ obj[k] += delta;
+ });
+ };
+
+ // update affected positions of current container
+ update(this._positions);
+
+ // update affected positions of children
+ _.each(this.list(), function(item) {
+ update(item._positions);
+ });
+
+ this.source = require('utils').replaceSubstring(this.source, value, r);
+ },
+
+
+ /**
+ * Adds new attribute
+ * @param {String} name Property name
+ * @param {String} value Property value
+ * @param {Number} pos Position at which to insert new property. By
+ * default the property is inserted at the end of rule
+ * @returns {EditElement} Newly created element
+ */
+ add: function(name, value, pos) {
+ // this is abstract implementation
+ var item = new EditElement(name, value);
+ this._children.push(item);
+ return item;
+ },
+
+ /**
+ * Returns attribute object
+ * @param {String} name Attribute name or its index
+ * @returns {EditElement}
+ */
+ get: function(name) {
+ if (_.isNumber(name))
+ return this.list()[name];
+
+ if (_.isString(name))
+ return _.find(this.list(), function(prop) {
+ return prop.name() === name;
+ });
+
+ return name;
+ },
+
+ /**
+ * Returns all children by name or indexes
+ * @param {Object} name Element name(s) or indexes (String
,
+ * Array
, Number
)
+ * @returns {Array}
+ */
+ getAll: function(name) {
+ if (!_.isArray(name))
+ name = [name];
+
+ // split names and indexes
+ var names = [], indexes = [];
+ _.each(name, function(item) {
+ if (_.isString(item))
+ names.push(item);
+ else if (_.isNumber(item))
+ indexes.push(item);
+ });
+
+ return _.filter(this.list(), function(attribute, i) {
+ return _.include(indexes, i) || _.include(names, attribute.name());
+ });
+ },
+
+ /**
+ * Returns or updates element value. If such element doesn't exists,
+ * it will be created automatically and added at the end of child list.
+ * @param {String} name Element name or its index
+ * @param {String} value New element value
+ * @returns {String}
+ */
+ value: function(name, value, pos) {
+ var element = this.get(name);
+ if (element)
+ return element.value(value);
+
+ if (!_.isUndefined(value)) {
+ // no such element — create it
+ return this.add(name, value, pos);
+ }
+ },
+
+ /**
+ * Returns all values of child elements found by getAll()
+ * method
+ * @param {Object} name Element name(s) or indexes (String
,
+ * Array
, Number
)
+ * @returns {Array}
+ */
+ values: function(name) {
+ return _.map(this.getAll(name), function(element) {
+ return element.value();
+ });
+ },
+
+ /**
+ * Remove child element
+ * @param {String} name Property name or its index
+ */
+ remove: function(name) {
+ var element = this.get(name);
+ if (element) {
+ this._updateSource('', element.fullRange());
+ this._children = _.without(this._children, element);
+ }
+ },
+
+ /**
+ * Returns list of all editable child elements
+ * @returns {Array}
+ */
+ list: function() {
+ return this._children;
+ },
+
+ /**
+ * Returns index of editble child in list
+ * @param {Object} item
+ * @returns {Number}
+ */
+ indexOf: function(item) {
+ return _.indexOf(this.list(), this.get(item));
+ },
+
+ /**
+ * Sets or gets container name
+ * @param {String} val New name. If not passed, current
+ * name is returned
+ * @return {String}
+ */
+ name: function(val) {
+ if (!_.isUndefined(val) && this._name !== (val = String(val))) {
+ this._updateSource(val, this._positions.name, this._positions.name + this._name.length);
+ this._name = val;
+ }
+
+ return this._name;
+ },
+
+ /**
+ * Returns name range object
+ * @param {Boolean} isAbsolute Return absolute range (with respect of
+ * rule offset)
+ * @returns {Range}
+ */
+ nameRange: function(isAbsolute) {
+ return range(this._positions.name + (isAbsolute ? this.options.offset : 0), this.name());
+ },
+
+ /**
+ * Returns range of current source
+ * @param {Boolean} isAbsolute
+ */
+ range: function(isAbsolute) {
+ return range(isAbsolute ? this.options.offset : 0, this.toString());
+ },
+
+ /**
+ * Returns element that belongs to specified position
+ * @param {Number} pos
+ * @param {Boolean} isAbsolute
+ * @returns {EditElement}
+ */
+ itemFromPosition: function(pos, isAbsolute) {
+ return _.find(this.list(), function(elem) {
+ return elem.range(isAbsolute).inside(pos);
+ });
+ },
+
+ /**
+ * Returns source code of current container
+ * @returns {String}
+ */
+ toString: function() {
+ return this.source;
+ }
+ };
+
+ /**
+ * @param {EditContainer} parent
+ * @param {Object} nameToken
+ * @param {Object} valueToken
+ */
+ function EditElement(parent, nameToken, valueToken) {
+ /** @type EditContainer */
+ this.parent = parent;
+
+ this._name = nameToken.value;
+ this._value = valueToken ? valueToken.value : '';
+
+ this._positions = {
+ name: nameToken.start,
+ value: valueToken ? valueToken.start : -1
+ };
+
+ this.initialize.apply(this, arguments);
+ }
+
+ /**
+ * The self-propagating extend function for classes.
+ * @type Function
+ */
+ EditElement.extend = core.extend;
+
+ EditElement.prototype = {
+ /**
+ * Child class constructor
+ */
+ initialize: function() {},
+
+ /**
+ * Make position absolute
+ * @private
+ * @param {Number} num
+ * @param {Boolean} isAbsolute
+ * @returns {Boolean}
+ */
+ _pos: function(num, isAbsolute) {
+ return num + (isAbsolute ? this.parent.options.offset : 0);
+ },
+
+ /**
+ * Sets of gets element value
+ * @param {String} val New element value. If not passed, current
+ * value is returned
+ * @returns {String}
+ */
+ value: function(val) {
+ if (!_.isUndefined(val) && this._value !== (val = String(val))) {
+ this.parent._updateSource(val, this.valueRange());
+ this._value = val;
+ }
+
+ return this._value;
+ },
+
+ /**
+ * Sets of gets element name
+ * @param {String} val New element name. If not passed, current
+ * name is returned
+ * @returns {String}
+ */
+ name: function(val) {
+ if (!_.isUndefined(val) && this._name !== (val = String(val))) {
+ this.parent._updateSource(val, this.nameRange());
+ this._name = val;
+ }
+
+ return this._name;
+ },
+
+ /**
+ * Returns position of element name token
+ * @param {Boolean} isAbsolute Return absolute position
+ * @returns {Number}
+ */
+ namePosition: function(isAbsolute) {
+ return this._pos(this._positions.name, isAbsolute);
+ },
+
+ /**
+ * Returns position of element value token
+ * @param {Boolean} isAbsolute Return absolute position
+ * @returns {Number}
+ */
+ valuePosition: function(isAbsolute) {
+ return this._pos(this._positions.value, isAbsolute);
+ },
+
+ /**
+ * Returns element name
+ * @param {Boolean} isAbsolute Return absolute range
+ * @returns {Range}
+ */
+ range: function(isAbsolute) {
+ return range(this.namePosition(isAbsolute), this.toString());
+ },
+
+ /**
+ * Returns full element range, including possible indentation
+ * @param {Boolean} isAbsolute Return absolute range
+ * @returns {Range}
+ */
+ fullRange: function(isAbsolute) {
+ return this.range(isAbsolute);
+ },
+
+ /**
+ * Returns element name range
+ * @param {Boolean} isAbsolute Return absolute range
+ * @returns {Range}
+ */
+ nameRange: function(isAbsolute) {
+ return range(this.namePosition(isAbsolute), this.name());
+ },
+
+ /**
+ * Returns element value range
+ * @param {Boolean} isAbsolute Return absolute range
+ * @returns {Range}
+ */
+ valueRange: function(isAbsolute) {
+ return range(this.valuePosition(isAbsolute), this.value());
+ },
+
+ /**
+ * Returns current element string representation
+ * @returns {String}
+ */
+ toString: function() {
+ return this.name() + this.value();
+ },
+
+ valueOf: function() {
+ return this.toString();
+ }
+ };
+
+ return {
+ EditContainer: EditContainer,
+ EditElement: EditElement,
+
+ /**
+ * Creates token that can be fed to EditElement
+ * @param {Number} start
+ * @param {String} value
+ * @param {String} type
+ * @returns
+ */
+ createToken: function(start, value, type) {
+ var obj = {
+ start: start || 0,
+ value: value || '',
+ type: type
+ };
+
+ obj.end = obj.start + obj.value.length;
+ return obj;
+ }
+ };
+});/**
+ * CSS EditTree is a module that can parse a CSS rule into a tree with
+ * convenient methods for adding, modifying and removing CSS properties. These
+ * changes can be written back to string with respect of code formatting.
+ *
+ * @memberOf __cssEditTreeDefine
+ * @constructor
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.define('cssEditTree', function(require, _) {
+ var defaultOptions = {
+ styleBefore: '\n\t',
+ styleSeparator: ': ',
+ offset: 0
+ };
+
+ var WHITESPACE_REMOVE_FROM_START = 1;
+ var WHITESPACE_REMOVE_FROM_END = 2;
+
+ /**
+ * Returns range object
+ * @param {Number} start
+ * @param {Number} len
+ * @returns {Range}
+ */
+ function range(start, len) {
+ return require('range').create(start, len);
+ }
+
+ /**
+ * Removes whitespace tokens from the array ends
+ * @param {Array} tokens
+ * @param {Number} mask Mask indicating from which end whitespace should be
+ * removed
+ * @returns {Array}
+ */
+ function trimWhitespaceTokens(tokens, mask) {
+ mask = mask || (WHITESPACE_REMOVE_FROM_START | WHITESPACE_REMOVE_FROM_END);
+ var whitespace = ['white', 'line'];
+
+ if ((mask & WHITESPACE_REMOVE_FROM_END) == WHITESPACE_REMOVE_FROM_END)
+ while (tokens.length && _.include(whitespace, _.last(tokens).type)) {
+ tokens.pop();
+ }
+
+ if ((mask & WHITESPACE_REMOVE_FROM_START) == WHITESPACE_REMOVE_FROM_START)
+ while (tokens.length && _.include(whitespace, tokens[0].type)) {
+ tokens.shift();
+ }
+
+ return tokens;
+ }
+
+ /**
+ * Helper function that searches for selector range for CSSEditRule
+ * @param {TokenIterator} it
+ * @returns {Range}
+ */
+ function findSelectorRange(it) {
+ var tokens = [], token;
+ var start = it.position(), end;
+
+ while (token = it.next()) {
+ if (token.type == '{')
+ break;
+ tokens.push(token);
+ }
+
+ trimWhitespaceTokens(tokens);
+
+ if (tokens.length) {
+ start = tokens[0].start;
+ end = _.last(tokens).end;
+ } else {
+ end = start;
+ }
+
+ return range(start, end - start);
+ }
+
+ /**
+ * Helper function that searches for CSS property value range next to
+ * iterator's current position
+ * @param {TokenIterator} it
+ * @returns {Range}
+ */
+ function findValueRange(it) {
+ // find value start position
+ var skipTokens = ['white', 'line', ':'];
+ var tokens = [], token, start, end;
+
+ it.nextUntil(function(tok) {
+ return !_.include(skipTokens, this.itemNext().type);
+ });
+
+ start = it.current().end;
+ // consume value
+ while (token = it.next()) {
+ if (token.type == '}' || token.type == ';') {
+ // found value end
+ trimWhitespaceTokens(tokens, WHITESPACE_REMOVE_FROM_START
+ | (token.type == '}' ? WHITESPACE_REMOVE_FROM_END : 0));
+
+ if (tokens.length) {
+ start = tokens[0].start;
+ end = _.last(tokens).end;
+ } else {
+ end = start;
+ }
+
+ return range(start, end - start);
+ }
+
+ tokens.push(token);
+ }
+
+ // reached the end of tokens list
+ if (tokens.length) {
+ return range(tokens[0].start, _.last(tokens).end - tokens[0].start);
+ }
+ }
+
+ /**
+ * Finds parts of complex CSS value
+ * @param {String} str
+ * @returns {Array} Returns list of Range
's
+ */
+ function findParts(str) {
+ /** @type StringStream */
+ var stream = require('stringStream').create(str);
+ var ch;
+ var result = [];
+ var sep = /[\s\u00a0,]/;
+
+ var add = function() {
+ stream.next();
+ result.push(range(stream.start, stream.current()));
+ stream.start = stream.pos;
+ };
+
+ // skip whitespace
+ stream.eatSpace();
+ stream.start = stream.pos;
+
+ while (ch = stream.next()) {
+ if (ch == '"' || ch == "'") {
+ stream.next();
+ if (!stream.skipTo(ch)) break;
+ add();
+ } else if (ch == '(') {
+ // function found, may have nested function
+ stream.backUp(1);
+ if (!stream.skipToPair('(', ')')) break;
+ stream.backUp(1);
+ add();
+ } else {
+ if (sep.test(ch)) {
+ result.push(range(stream.start, stream.current().length - 1));
+ stream.eatWhile(sep);
+ stream.start = stream.pos;
+ }
+ }
+ }
+
+ add();
+
+ return _.chain(result)
+ .filter(function(item) {
+ return !!item.length();
+ })
+ .uniq(false, function(item) {
+ return item.toString();
+ })
+ .value();
+ }
+
+ /**
+ * A bit hacky way to identify invalid CSS property definition: when user
+ * starts writing new abbreviation in CSS rule, he actually creates invalid
+ * CSS property definition and this method tries to identify such abbreviation
+ * and prevent it from being added to CSS edit tree
+ * @param {TokenIterator} it
+ */
+ function isValidIdentifier(it) {
+// return true;
+ var tokens = it.tokens;
+ for (var i = it._i + 1, il = tokens.length; i < il; i++) {
+ if (tokens[i].type == ':')
+ return true;
+
+ if (tokens[i].type == 'identifier' || tokens[i].type == 'line')
+ return false;
+ }
+
+ return false;
+ }
+
+ /**
+ * @class
+ * @extends EditContainer
+ */
+ var CSSEditContainer = require('editTree').EditContainer.extend({
+ initialize: function(source, options) {
+ _.defaults(this.options, defaultOptions);
+ var editTree = require('editTree');
+
+ /** @type TokenIterator */
+ var it = require('tokenIterator').create(
+ require('cssParser').parse(source));
+
+ var selectorRange = findSelectorRange(it);
+ this._positions.name = selectorRange.start;
+ this._name = selectorRange.substring(source);
+
+ if (!it.current() || it.current().type != '{')
+ throw 'Invalid CSS rule';
+
+ this._positions.contentStart = it.position() + 1;
+
+ // consume properties
+ var propertyRange, valueRange, token;
+ while (token = it.next()) {
+ if (token.type == 'identifier' && isValidIdentifier(it)) {
+ propertyRange = range(token);
+ valueRange = findValueRange(it);
+ var end = (it.current() && it.current().type == ';')
+ ? range(it.current())
+ : range(valueRange.end, 0);
+ this._children.push(new CSSEditElement(this,
+ editTree.createToken(propertyRange.start, propertyRange.substring(source)),
+ editTree.createToken(valueRange.start, valueRange.substring(source)),
+ editTree.createToken(end.start, end.substring(source))
+ ));
+ }
+ }
+
+ this._saveStyle();
+ },
+
+ /**
+ * Remembers all styles of properties
+ * @private
+ */
+ _saveStyle: function() {
+ var start = this._positions.contentStart;
+ var source = this.source;
+ var utils = require('utils');
+
+ _.each(this.list(), /** @param {CSSEditProperty} p */ function(p) {
+ p.styleBefore = source.substring(start, p.namePosition());
+ // a small hack here:
+ // Sometimes users add empty lines before properties to logically
+ // separate groups of properties. In this case, a blind copy of
+ // characters between rules may lead to undesired behavior,
+ // especially when current rule is duplicated or used as a donor
+ // to create new rule.
+ // To solve this issue, we‘ll take only last newline indentation
+ var lines = utils.splitByLines(p.styleBefore);
+ if (lines.length > 1) {
+ p.styleBefore = '\n' + _.last(lines);
+ }
+
+ p.styleSeparator = source.substring(p.nameRange().end, p.valuePosition());
+
+ // graceful and naive comments removal
+ p.styleBefore = _.last(p.styleBefore.split('*/'));
+ p.styleSeparator = p.styleSeparator.replace(/\/\*.*?\*\//g, '');
+
+ start = p.range().end;
+ });
+ },
+
+ /**
+ * Adds new CSS property
+ * @param {String} name Property name
+ * @param {String} value Property value
+ * @param {Number} pos Position at which to insert new property. By
+ * default the property is inserted at the end of rule
+ * @returns {CSSEditProperty}
+ */
+ add: function(name, value, pos) {
+ var list = this.list();
+ var start = this._positions.contentStart;
+ var styles = _.pick(this.options, 'styleBefore', 'styleSeparator');
+ var editTree = require('editTree');
+
+ if (_.isUndefined(pos))
+ pos = list.length;
+
+ /** @type CSSEditProperty */
+ var donor = list[pos];
+ if (donor) {
+ start = donor.fullRange().start;
+ } else if (donor = list[pos - 1]) {
+ // make sure that donor has terminating semicolon
+ donor.end(';');
+ start = donor.range().end;
+ }
+
+ if (donor) {
+ styles = _.pick(donor, 'styleBefore', 'styleSeparator');
+ }
+
+ var nameToken = editTree.createToken(start + styles.styleBefore.length, name);
+ var valueToken = editTree.createToken(nameToken.end + styles.styleSeparator.length, value);
+
+ var property = new CSSEditElement(this, nameToken, valueToken,
+ editTree.createToken(valueToken.end, ';'));
+
+ _.extend(property, styles);
+
+ // write new property into the source
+ this._updateSource(property.styleBefore + property.toString(), start);
+
+ // insert new property
+ this._children.splice(pos, 0, property);
+ return property;
+ }
+ });
+
+ /**
+ * @class
+ * @type CSSEditElement
+ * @constructor
+ */
+ var CSSEditElement = require('editTree').EditElement.extend({
+ initialize: function(rule, name, value, end) {
+ this.styleBefore = rule.options.styleBefore;
+ this.styleSeparator = rule.options.styleSeparator;
+
+ this._end = end.value;
+ this._positions.end = end.start;
+ },
+
+ /**
+ * Returns ranges of complex value parts
+ * @returns {Array} Returns null
if value is not complex
+ */
+ valueParts: function(isAbsolute) {
+ var parts = findParts(this.value());
+ if (isAbsolute) {
+ var offset = this.valuePosition(true);
+ _.each(parts, function(p) {
+ p.shift(offset);
+ });
+ }
+
+ return parts;
+ },
+
+ /**
+ * Sets of gets property end value (basically, it's a semicolon)
+ * @param {String} val New end value. If not passed, current
+ * value is returned
+ */
+ end: function(val) {
+ if (!_.isUndefined(val) && this._end !== val) {
+ this.parent._updateSource(val, this._positions.end, this._positions.end + this._end.length);
+ this._end = val;
+ }
+
+ return this._end;
+ },
+
+ /**
+ * Returns full rule range, with indentation
+ * @param {Boolean} isAbsolute Return absolute range (with respect of
+ * rule offset)
+ * @returns {Range}
+ */
+ fullRange: function(isAbsolute) {
+ var r = this.range(isAbsolute);
+ r.start -= this.styleBefore.length;
+ return r;
+ },
+
+ /**
+ * Returns item string representation
+ * @returns {String}
+ */
+ toString: function() {
+ return this.name() + this.styleSeparator + this.value() + this.end();
+ }
+ });
+
+ return {
+ /**
+ * Parses CSS rule into editable tree
+ * @param {String} source
+ * @param {Object} options
+ * @memberOf emmet.cssEditTree
+ * @returns {EditContainer}
+ */
+ parse: function(source, options) {
+ return new CSSEditContainer(source, options);
+ },
+
+ /**
+ * Extract and parse CSS rule from specified position in content
+ * @param {String} content CSS source code
+ * @param {Number} pos Character position where to start source code extraction
+ * @returns {EditContainer}
+ */
+ parseFromPosition: function(content, pos, isBackward) {
+ var bounds = this.extractRule(content, pos, isBackward);
+ if (!bounds || !bounds.inside(pos))
+ // no matching CSS rule or caret outside rule bounds
+ return null;
+
+ return this.parse(bounds.substring(content), {
+ offset: bounds.start
+ });
+ },
+
+ /**
+ * Extracts single CSS selector definition from source code
+ * @param {String} content CSS source code
+ * @param {Number} pos Character position where to start source code extraction
+ * @returns {Range}
+ */
+ extractRule: function(content, pos, isBackward) {
+ var result = '';
+ var len = content.length;
+ var offset = pos;
+ var stopChars = '{}/\\<>\n\r';
+ var bracePos = -1, ch;
+
+ // search left until we find rule edge
+ while (offset >= 0) {
+ ch = content.charAt(offset);
+ if (ch == '{') {
+ bracePos = offset;
+ break;
+ }
+ else if (ch == '}' && !isBackward) {
+ offset++;
+ break;
+ }
+
+ offset--;
+ }
+
+ // search right for full rule set
+ while (offset < len) {
+ ch = content.charAt(offset);
+ if (ch == '{') {
+ bracePos = offset;
+ } else if (ch == '}') {
+ if (bracePos != -1)
+ result = content.substring(bracePos, offset + 1);
+ break;
+ }
+
+ offset++;
+ }
+
+ if (result) {
+ // find CSS selector
+ offset = bracePos - 1;
+ var selector = '';
+ while (offset >= 0) {
+ ch = content.charAt(offset);
+ if (stopChars.indexOf(ch) != -1) break;
+ offset--;
+ }
+
+ // also trim whitespace
+ selector = content.substring(offset + 1, bracePos).replace(/^[\s\n\r]+/m, '');
+ return require('range').create(bracePos - selector.length, result.length + selector.length);
+ }
+
+ return null;
+ },
+
+ /**
+ * Removes vendor prefix from CSS property
+ * @param {String} name CSS property
+ * @return {String}
+ */
+ baseName: function(name) {
+ return name.replace(/^\s*\-\w+\-/, '');
+ },
+
+ /**
+ * Finds parts of complex CSS value
+ * @param {String} str
+ * @returns {Array}
+ */
+ findParts: findParts
+ };
+});/**
+ * XML EditTree is a module that can parse an XML/HTML element into a tree with
+ * convenient methods for adding, modifying and removing attributes. These
+ * changes can be written back to string with respect of code formatting.
+ *
+ * @memberOf __xmlEditTreeDefine
+ * @constructor
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.define('xmlEditTree', function(require, _) {
+ var defaultOptions = {
+ styleBefore: ' ',
+ styleSeparator: '=',
+ styleQuote: '"',
+ offset: 0
+ };
+
+ var startTag = /^<([\w\:\-]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/m;
+
+ var XMLEditContainer = require('editTree').EditContainer.extend({
+ initialize: function(source, options) {
+ _.defaults(this.options, defaultOptions);
+ this._positions.name = 1;
+
+ var attrToken = null;
+ var tokens = require('xmlParser').parse(source);
+ var range = require('range');
+
+ _.each(tokens, function(token) {
+ token.value = range.create(token).substring(source);
+ switch (token.type) {
+ case 'tag':
+ if (/^<[^\/]+/.test(token.value)) {
+ this._name = token.value.substring(1);
+ }
+ break;
+
+ case 'attribute':
+ // add empty attribute
+ if (attrToken) {
+ this._children.push(new XMLEditElement(this, attrToken));
+ }
+
+ attrToken = token;
+ break;
+
+ case 'string':
+ this._children.push(new XMLEditElement(this, attrToken, token));
+ attrToken = null;
+ break;
+ }
+ }, this);
+
+ if (attrToken) {
+ this._children.push(new XMLEditElement(this, attrToken));
+ }
+
+ this._saveStyle();
+ },
+
+ /**
+ * Remembers all styles of properties
+ * @private
+ */
+ _saveStyle: function() {
+ var start = this.nameRange().end;
+ var source = this.source;
+
+ _.each(this.list(), /** @param {EditElement} p */ function(p) {
+ p.styleBefore = source.substring(start, p.namePosition());
+
+ if (p.valuePosition() !== -1) {
+ p.styleSeparator = source.substring(p.namePosition() + p.name().length, p.valuePosition() - p.styleQuote.length);
+ }
+
+ start = p.range().end;
+ });
+ },
+
+ /**
+ * Adds new attribute
+ * @param {String} name Property name
+ * @param {String} value Property value
+ * @param {Number} pos Position at which to insert new property. By
+ * default the property is inserted at the end of rule
+ */
+ add: function(name, value, pos) {
+ var list = this.list();
+ var start = this.nameRange().end;
+ var editTree = require('editTree');
+ var styles = _.pick(this.options, 'styleBefore', 'styleSeparator', 'styleQuote');
+
+ if (_.isUndefined(pos))
+ pos = list.length;
+
+
+ /** @type XMLEditAttribute */
+ var donor = list[pos];
+ if (donor) {
+ start = donor.fullRange().start;
+ } else if (donor = list[pos - 1]) {
+ start = donor.range().end;
+ }
+
+ if (donor) {
+ styles = _.pick(donor, 'styleBefore', 'styleSeparator', 'styleQuote');
+ }
+
+ value = styles.styleQuote + value + styles.styleQuote;
+
+ var attribute = new XMLEditElement(this,
+ editTree.createToken(start + styles.styleBefore.length, name),
+ editTree.createToken(start + styles.styleBefore.length + name.length
+ + styles.styleSeparator.length, value)
+ );
+
+ _.extend(attribute, styles);
+
+ // write new attribute into the source
+ this._updateSource(attribute.styleBefore + attribute.toString(), start);
+
+ // insert new attribute
+ this._children.splice(pos, 0, attribute);
+ return attribute;
+ }
+ });
+
+ var XMLEditElement = require('editTree').EditElement.extend({
+ initialize: function(parent, nameToken, valueToken) {
+ this.styleBefore = parent.options.styleBefore;
+ this.styleSeparator = parent.options.styleSeparator;
+
+ var value = '', quote = parent.options.styleQuote;
+ if (valueToken) {
+ value = valueToken.value;
+ quote = value.charAt(0);
+ if (quote == '"' || quote == "'") {
+ value = value.substring(1);
+ } else {
+ quote = '';
+ }
+
+ if (quote && value.charAt(value.length - 1) == quote) {
+ value = value.substring(0, value.length - 1);
+ }
+ }
+
+ this.styleQuote = quote;
+
+ this._value = value;
+ this._positions.value = valueToken ? valueToken.start + quote.length : -1;
+ },
+
+ /**
+ * Returns full rule range, with indentation
+ * @param {Boolean} isAbsolute Return absolute range (with respect of
+ * rule offset)
+ * @returns {Range}
+ */
+ fullRange: function(isAbsolute) {
+ var r = this.range(isAbsolute);
+ r.start -= this.styleBefore.length;
+ return r;
+ },
+
+ toString: function() {
+ return this.name() + this.styleSeparator
+ + this.styleQuote + this.value() + this.styleQuote;
+ }
+ });
+
+ return {
+ /**
+ * Parses HTML element into editable tree
+ * @param {String} source
+ * @param {Object} options
+ * @memberOf emmet.htmlEditTree
+ * @returns {EditContainer}
+ */
+ parse: function(source, options) {
+ return new XMLEditContainer(source, options);
+ },
+
+ /**
+ * Extract and parse HTML from specified position in content
+ * @param {String} content CSS source code
+ * @param {Number} pos Character position where to start source code extraction
+ * @returns {XMLEditElement}
+ */
+ parseFromPosition: function(content, pos, isBackward) {
+ var bounds = this.extractTag(content, pos, isBackward);
+ if (!bounds || !bounds.inside(pos))
+ // no matching HTML tag or caret outside tag bounds
+ return null;
+
+ return this.parse(bounds.substring(content), {
+ offset: bounds.start
+ });
+ },
+
+ /**
+ * Extracts nearest HTML tag range from content
, starting at
+ * pos
position
+ * @param {String} content
+ * @param {Number} pos
+ * @param {Boolean} isBackward
+ * @returns {Range}
+ */
+ extractTag: function(content, pos, isBackward) {
+ var len = content.length, i;
+ var range = require('range');
+
+ // max extraction length. I don't think there may be tags larger
+ // than 2000 characters length
+ var maxLen = Math.min(2000, len);
+
+ /** @type Range */
+ var r = null;
+
+ var match = function(pos) {
+ var m;
+ if (content.charAt(pos) == '<' && (m = content.substr(pos, maxLen).match(startTag)))
+ return range.create(pos, m[0]);
+ };
+
+ // lookup backward, in case we are inside tag already
+ for (i = pos; i >= 0; i--) {
+ if (r = match(i)) break;
+ }
+
+ if (r && (r.inside(pos) || isBackward))
+ return r;
+
+ if (!r && isBackward)
+ return null;
+
+ // search forward
+ for (i = pos; i < len; i++) {
+ if (r = match(i))
+ return r;
+ }
+ }
+ };
+});/**
+ * 'Expand abbreviation' editor action: extracts abbreviation from current caret
+ * position and replaces it with formatted output.
+ * true
if abbreviation was expanded
+ * successfully
+ */
+ actions.add('expand_abbreviation', function(editor, syntax, profile) {
+ var args = _.toArray(arguments);
+
+ // normalize incoming arguments
+ var info = require('editorUtils').outputInfo(editor, syntax, profile);
+ args[1] = info.syntax;
+ args[2] = info.profile;
+
+ return handlers.exec(false, args);
+ });
+
+ /**
+ * A special version of expandAbbreviation
function: if it can't
+ * find abbreviation, it will place Tab character at caret position
+ * @param {IEmmetEditor} editor Editor instance
+ * @param {String} syntax Syntax type (html, css, etc.)
+ * @param {String} profile Output profile name (html, xml, xhtml)
+ */
+ actions.add('expand_abbreviation_with_tab', function(editor, syntax, profile) {
+ var sel = editor.getSelection();
+ var indent = require('resources').getVariable('indentation');
+ if (sel) {
+ // indent selection
+ var utils = require('utils');
+ var selRange = require('range').create(editor.getSelectionRange());
+ var content = utils.padString(sel, indent);
+
+ editor.replaceContent(indent + '${0}', editor.getCaretPos());
+ var replaceRange = require('range').create(editor.getCaretPos(), selRange.length());
+ editor.replaceContent(content, replaceRange.start, replaceRange.end, true);
+ editor.createSelection(replaceRange.start, replaceRange.start + content.length);
+ return true;
+ }
+
+ if (!actions.run('expand_abbreviation', editor, syntax, profile)) {
+ editor.replaceContent(indent, editor.getCaretPos());
+ }
+
+ return true;
+ }, {hidden: true});
+
+ // XXX setup default handler
+ /**
+ * Extracts abbreviation from current caret
+ * position and replaces it with formatted output
+ * @param {IEmmetEditor} editor Editor instance
+ * @param {String} syntax Syntax type (html, css, etc.)
+ * @param {String} profile Output profile name (html, xml, xhtml)
+ * @return {Boolean} Returns true
if abbreviation was expanded
+ * successfully
+ */
+ handlers.add(function(editor, syntax, profile) {
+ var caretPos = editor.getSelectionRange().end;
+ var abbr = module.findAbbreviation(editor);
+
+ if (abbr) {
+ var content = emmet.expandAbbreviation(abbr, syntax, profile,
+ require('actionUtils').captureContext(editor));
+ if (content) {
+ editor.replaceContent(content, caretPos - abbr.length, caretPos);
+ return true;
+ }
+ }
+
+ return false;
+ }, {order: -1});
+
+ return module = {
+ /**
+ * Adds custom expand abbreviation handler. The passed function should
+ * return true
if it was performed successfully,
+ * false
otherwise.
+ *
+ * Added handlers will be called when 'Expand Abbreviation' is called
+ * in order they were added
+ * @memberOf expandAbbreviation
+ * @param {Function} fn
+ * @param {Object} options
+ */
+ addHandler: function(fn, options) {
+ handlers.add(fn, options);
+ },
+
+ /**
+ * Removes registered handler
+ * @returns
+ */
+ removeHandler: function(fn) {
+ handlers.remove(fn, options);
+ },
+
+ /**
+ * Search for abbreviation in editor from current caret position
+ * @param {IEmmetEditor} editor Editor instance
+ * @return {String}
+ */
+ findAbbreviation: function(editor) {
+ /** @type Range */
+ var range = require('range').create(editor.getSelectionRange());
+ var content = String(editor.getContent());
+ if (range.length()) {
+ // abbreviation is selected by user
+ return range.substring(content);
+ }
+
+ // search for new abbreviation from current caret position
+ var curLine = editor.getCurrentLineRange();
+ return require('actionUtils').extractAbbreviation(content.substring(curLine.start, range.start));
+ }
+ };
+});/**
+ * Action that wraps content with abbreviation. For convenience, action is
+ * defined as reusable module
+ * @constructor
+ * @memberOf __wrapWithAbbreviationDefine
+ */
+emmet.define('wrapWithAbbreviation', function(require, _) {
+ /** Back-references to current module */
+ var module = null;
+
+ /**
+ * Wraps content with abbreviation
+ * @param {IEmmetEditor} Editor instance
+ * @param {String} abbr Abbreviation to wrap with
+ * @param {String} syntax Syntax type (html, css, etc.)
+ * @param {String} profile Output profile name (html, xml, xhtml)
+ */
+ require('actions').add('wrap_with_abbreviation', function (editor, abbr, syntax, profile) {
+ var info = require('editorUtils').outputInfo(editor, syntax, profile);
+ var utils = require('utils');
+ /** @type emmet.editorUtils */
+ var editorUtils = require('editorUtils');
+ abbr = abbr || editor.prompt("Enter abbreviation");
+
+ if (!abbr)
+ return null;
+
+ abbr = String(abbr);
+
+ var range = require('range').create(editor.getSelectionRange());
+
+ if (!range.length()) {
+ // no selection, find tag pair
+ var match = require('htmlMatcher').tag(info.content, range.start);
+ if (!match) { // nothing to wrap
+ return false;
+ }
+
+ range = utils.narrowToNonSpace(info.content, match.range);
+ }
+
+ var newContent = utils.escapeText(range.substring(info.content));
+ var result = module
+ .wrap(abbr, editorUtils.unindent(editor, newContent), info.syntax,
+ info.profile, require('actionUtils').captureContext(editor));
+
+ if (result) {
+ editor.replaceContent(result, range.start, range.end);
+ return true;
+ }
+
+ return false;
+ });
+
+ return module = {
+ /**
+ * Wraps passed text with abbreviation. Text will be placed inside last
+ * expanded element
+ * @memberOf wrapWithAbbreviation
+ * @param {String} abbr Abbreviation
+ * @param {String} text Text to wrap
+ * @param {String} syntax Document type (html, xml, etc.). Default is 'html'
+ * @param {String} profile Output profile's name. Default is 'plain'
+ * @param {Object} contextNode Context node inside which abbreviation
+ * is wrapped. It will be used as a reference for node name resolvers
+ * @return {String}
+ */
+ wrap: function(abbr, text, syntax, profile, contextNode) {
+ /** @type emmet.filters */
+ var filters = require('filters');
+ /** @type emmet.utils */
+ var utils = require('utils');
+
+ syntax = syntax || emmet.defaultSyntax();
+ profile = require('profile').get(profile, syntax);
+
+ require('tabStops').resetTabstopIndex();
+
+ var data = filters.extractFromAbbreviation(abbr);
+ var parsedTree = require('abbreviationParser').parse(data[0], {
+ syntax: syntax,
+ pastedContent: text,
+ contextNode: contextNode
+ });
+ if (parsedTree) {
+ var filtersList = filters.composeList(syntax, profile, data[1]);
+ filters.apply(parsedTree, filtersList, profile);
+ return utils.replaceVariables(parsedTree.toString());
+ }
+
+ return null;
+ }
+ };
+});/**
+ * Toggles HTML and CSS comments depending on current caret context. Unlike
+ * the same action in most editors, this action toggles comment on currently
+ * matched item—HTML tag or CSS selector—when nothing is selected.
+ *
+ * @param {Function} require
+ * @param {Underscore} _
+ * @memberOf __toggleCommentAction
+ * @constructor
+ */
+emmet.exec(function(require, _) {
+ /**
+ * Toggle HTML comment on current selection or tag
+ * @param {IEmmetEditor} editor
+ * @return {Boolean} Returns true
if comment was toggled
+ */
+ function toggleHTMLComment(editor) {
+ /** @type Range */
+ var range = require('range').create(editor.getSelectionRange());
+ var info = require('editorUtils').outputInfo(editor);
+
+ if (!range.length()) {
+ // no selection, find matching tag
+ var tag = require('htmlMatcher').tag(info.content, editor.getCaretPos());
+ if (tag) { // found pair
+ range = tag.outerRange;
+ }
+ }
+
+ return genericCommentToggle(editor, '', range);
+ }
+
+ /**
+ * Simple CSS commenting
+ * @param {IEmmetEditor} editor
+ * @return {Boolean} Returns true
if comment was toggled
+ */
+ function toggleCSSComment(editor) {
+ /** @type Range */
+ var range = require('range').create(editor.getSelectionRange());
+ var info = require('editorUtils').outputInfo(editor);
+
+ if (!range.length()) {
+ // no selection, try to get current rule
+ /** @type CSSRule */
+ var rule = require('cssEditTree').parseFromPosition(info.content, editor.getCaretPos());
+ if (rule) {
+ var property = cssItemFromPosition(rule, editor.getCaretPos());
+ range = property
+ ? property.range(true)
+ : require('range').create(rule.nameRange(true).start, rule.source);
+ }
+ }
+
+ if (!range.length()) {
+ // still no selection, get current line
+ range = require('range').create(editor.getCurrentLineRange());
+ require('utils').narrowToNonSpace(info.content, range);
+ }
+
+ return genericCommentToggle(editor, '/*', '*/', range);
+ }
+
+ /**
+ * Returns CSS property from rule
that matches passed position
+ * @param {EditContainer} rule
+ * @param {Number} absPos
+ * @returns {EditElement}
+ */
+ function cssItemFromPosition(rule, absPos) {
+ // do not use default EditContainer.itemFromPosition() here, because
+ // we need to make a few assumptions to make CSS commenting more reliable
+ var relPos = absPos - (rule.options.offset || 0);
+ var reSafeChar = /^[\s\n\r]/;
+ return _.find(rule.list(), function(item) {
+ if (item.range().end === relPos) {
+ // at the end of property, but outside of it
+ // if there’s a space character at current position,
+ // use current property
+ return reSafeChar.test(rule.source.charAt(relPos));
+ }
+
+ return item.range().inside(relPos);
+ });
+ }
+
+ /**
+ * Search for nearest comment in str
, starting from index from
+ * @param {String} text Where to search
+ * @param {Number} from Search start index
+ * @param {String} start_token Comment start string
+ * @param {String} end_token Comment end string
+ * @return {Range} Returns null if comment wasn't found
+ */
+ function searchComment(text, from, startToken, endToken) {
+ var commentStart = -1;
+ var commentEnd = -1;
+
+ var hasMatch = function(str, start) {
+ return text.substr(start, str.length) == str;
+ };
+
+ // search for comment start
+ while (from--) {
+ if (hasMatch(startToken, from)) {
+ commentStart = from;
+ break;
+ }
+ }
+
+ if (commentStart != -1) {
+ // search for comment end
+ from = commentStart;
+ var contentLen = text.length;
+ while (contentLen >= from++) {
+ if (hasMatch(endToken, from)) {
+ commentEnd = from + endToken.length;
+ break;
+ }
+ }
+ }
+
+ return (commentStart != -1 && commentEnd != -1)
+ ? require('range').create(commentStart, commentEnd - commentStart)
+ : null;
+ }
+
+ /**
+ * Generic comment toggling routine
+ * @param {IEmmetEditor} editor
+ * @param {String} commentStart Comment start token
+ * @param {String} commentEnd Comment end token
+ * @param {Range} range Selection range
+ * @return {Boolean}
+ */
+ function genericCommentToggle(editor, commentStart, commentEnd, range) {
+ var editorUtils = require('editorUtils');
+ var content = editorUtils.outputInfo(editor).content;
+ var caretPos = editor.getCaretPos();
+ var newContent = null;
+
+ var utils = require('utils');
+
+ /**
+ * Remove comment markers from string
+ * @param {Sting} str
+ * @return {String}
+ */
+ function removeComment(str) {
+ return str
+ .replace(new RegExp('^' + utils.escapeForRegexp(commentStart) + '\\s*'), function(str){
+ caretPos -= str.length;
+ return '';
+ }).replace(new RegExp('\\s*' + utils.escapeForRegexp(commentEnd) + '$'), '');
+ }
+
+ // first, we need to make sure that this substring is not inside
+ // comment
+ var commentRange = searchComment(content, caretPos, commentStart, commentEnd);
+ if (commentRange && commentRange.overlap(range)) {
+ // we're inside comment, remove it
+ range = commentRange;
+ newContent = removeComment(range.substring(content));
+ } else {
+ // should add comment
+ // make sure that there's no comment inside selection
+ newContent = commentStart + ' ' +
+ range.substring(content)
+ .replace(new RegExp(utils.escapeForRegexp(commentStart) + '\\s*|\\s*' + utils.escapeForRegexp(commentEnd), 'g'), '') +
+ ' ' + commentEnd;
+
+ // adjust caret position
+ caretPos += commentStart.length + 1;
+ }
+
+ // replace editor content
+ if (newContent !== null) {
+ newContent = utils.escapeText(newContent);
+ editor.setCaretPos(range.start);
+ editor.replaceContent(editorUtils.unindent(editor, newContent), range.start, range.end);
+ editor.setCaretPos(caretPos);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Toggle comment on current editor's selection or HTML tag/CSS rule
+ * @param {IEmmetEditor} editor
+ */
+ require('actions').add('toggle_comment', function(editor) {
+ var info = require('editorUtils').outputInfo(editor);
+ if (info.syntax == 'css') {
+ // in case our editor is good enough and can recognize syntax from
+ // current token, we have to make sure that cursor is not inside
+ // 'style' attribute of html element
+ var caretPos = editor.getCaretPos();
+ var tag = require('htmlMatcher').tag(info.content, caretPos);
+ if (tag && tag.open.range.inside(caretPos)) {
+ info.syntax = 'html';
+ }
+ }
+
+ if (info.syntax == 'css')
+ return toggleCSSComment(editor);
+
+ return toggleHTMLComment(editor);
+ });
+});/**
+ * Move between next/prev edit points. 'Edit points' are places between tags
+ * and quotes of empty attributes in html
+ * @constructor
+ *
+ * @memberOf __editPointActionDefine
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _) {
+ /**
+ * Search for new caret insertion point
+ * @param {IEmmetEditor} editor Editor instance
+ * @param {Number} inc Search increment: -1 — search left, 1 — search right
+ * @param {Number} offset Initial offset relative to current caret position
+ * @return {Number} Returns -1 if insertion point wasn't found
+ */
+ function findNewEditPoint(editor, inc, offset) {
+ inc = inc || 1;
+ offset = offset || 0;
+
+ var curPoint = editor.getCaretPos() + offset;
+ var content = String(editor.getContent());
+ var maxLen = content.length;
+ var nextPoint = -1;
+ var reEmptyLine = /^\s+$/;
+
+ function getLine(ix) {
+ var start = ix;
+ while (start >= 0) {
+ var c = content.charAt(start);
+ if (c == '\n' || c == '\r')
+ break;
+ start--;
+ }
+
+ return content.substring(start, ix);
+ }
+
+ while (curPoint <= maxLen && curPoint >= 0) {
+ curPoint += inc;
+ var curChar = content.charAt(curPoint);
+ var nextChar = content.charAt(curPoint + 1);
+ var prevChar = content.charAt(curPoint - 1);
+
+ switch (curChar) {
+ case '"':
+ case '\'':
+ if (nextChar == curChar && prevChar == '=') {
+ // empty attribute
+ nextPoint = curPoint + 1;
+ }
+ break;
+ case '>':
+ if (nextChar == '<') {
+ // between tags
+ nextPoint = curPoint + 1;
+ }
+ break;
+ case '\n':
+ case '\r':
+ // empty line
+ if (reEmptyLine.test(getLine(curPoint - 1))) {
+ nextPoint = curPoint;
+ }
+ break;
+ }
+
+ if (nextPoint != -1)
+ break;
+ }
+
+ return nextPoint;
+ }
+
+ /** @type emmet.actions */
+ var actions = require('actions');
+
+ /**
+ * Move caret to previous edit point
+ * @param {IEmmetEditor} editor Editor instance
+ */
+ actions.add('prev_edit_point', function(editor) {
+ var curPos = editor.getCaretPos();
+ var newPoint = findNewEditPoint(editor, -1);
+
+ if (newPoint == curPos)
+ // we're still in the same point, try searching from the other place
+ newPoint = findNewEditPoint(editor, -1, -2);
+
+ if (newPoint != -1) {
+ editor.setCaretPos(newPoint);
+ return true;
+ }
+
+ return false;
+ }, {label: 'Previous Edit Point'});
+
+ /**
+ * Move caret to next edit point
+ * @param {IEmmetEditor} editor Editor instance
+ */
+ actions.add('next_edit_point', function(editor) {
+ var newPoint = findNewEditPoint(editor, 1);
+ if (newPoint != -1) {
+ editor.setCaretPos(newPoint);
+ return true;
+ }
+
+ return false;
+ });
+});/**
+ * Actions that use stream parsers and tokenizers for traversing:
+ * -- Search for next/previous items in HTML
+ * -- Search for next/previous items in CSS
+ * @constructor
+ * @memberOf __selectItemActionDefine
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _) {
+ var startTag = /^<([\w\:\-]+)((?:\s+[\w\-:]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/;
+
+ /**
+ * Generic function for searching for items to select
+ * @param {IEmmetEditor} editor
+ * @param {Boolean} isBackward Search backward (search forward otherwise)
+ * @param {Function} extractFn Function that extracts item content
+ * @param {Function} rangeFn Function that search for next token range
+ */
+ function findItem(editor, isBackward, extractFn, rangeFn) {
+ var range = require('range');
+ var content = require('editorUtils').outputInfo(editor).content;
+
+ var contentLength = content.length;
+ var itemRange, rng;
+ /** @type Range */
+ var prevRange = range.create(-1, 0);
+ /** @type Range */
+ var sel = range.create(editor.getSelectionRange());
+
+ var searchPos = sel.start, loop = 100000; // endless loop protection
+ while (searchPos >= 0 && searchPos < contentLength && --loop > 0) {
+ if ( (itemRange = extractFn(content, searchPos, isBackward)) ) {
+ if (prevRange.equal(itemRange)) {
+ break;
+ }
+
+ prevRange = itemRange.clone();
+ rng = rangeFn(itemRange.substring(content), itemRange.start, sel.clone());
+
+ if (rng) {
+ editor.createSelection(rng.start, rng.end);
+ return true;
+ } else {
+ searchPos = isBackward ? itemRange.start : itemRange.end - 1;
+ }
+ }
+
+ searchPos += isBackward ? -1 : 1;
+ }
+
+ return false;
+ }
+
+ // XXX HTML section
+
+ /**
+ * Find next HTML item
+ * @param {IEmmetEditor} editor
+ */
+ function findNextHTMLItem(editor) {
+ var isFirst = true;
+ return findItem(editor, false, function(content, searchPos){
+ if (isFirst) {
+ isFirst = false;
+ return findOpeningTagFromPosition(content, searchPos);
+ } else {
+ return getOpeningTagFromPosition(content, searchPos);
+ }
+ }, function(tag, offset, selRange) {
+ return getRangeForHTMLItem(tag, offset, selRange, false);
+ });
+ }
+
+ /**
+ * Find previous HTML item
+ * @param {IEmmetEditor} editor
+ */
+ function findPrevHTMLItem(editor) {
+ return findItem(editor, true, getOpeningTagFromPosition, function (tag, offset, selRange) {
+ return getRangeForHTMLItem(tag, offset, selRange, true);
+ });
+ }
+
+ /**
+ * Creates possible selection ranges for HTML tag
+ * @param {String} source Original HTML source for tokens
+ * @param {Array} tokens List of HTML tokens
+ * @returns {Array}
+ */
+ function makePossibleRangesHTML(source, tokens, offset) {
+ offset = offset || 0;
+ var range = require('range');
+ var result = [];
+ var attrStart = -1, attrName = '', attrValue = '', attrValueRange, tagName;
+ _.each(tokens, function(tok) {
+ switch (tok.type) {
+ case 'tag':
+ tagName = source.substring(tok.start, tok.end);
+ if (/^<[\w\:\-]/.test(tagName)) {
+ // add tag name
+ result.push(range.create({
+ start: tok.start + 1,
+ end: tok.end
+ }));
+ }
+ break;
+ case 'attribute':
+ attrStart = tok.start;
+ attrName = source.substring(tok.start, tok.end);
+ break;
+
+ case 'string':
+ // attribute value
+ // push full attribute first
+ result.push(range.create(attrStart, tok.end - attrStart));
+
+ attrValueRange = range.create(tok);
+ attrValue = attrValueRange.substring(source);
+
+ // is this a quoted attribute?
+ if (isQuote(attrValue.charAt(0)))
+ attrValueRange.start++;
+
+ if (isQuote(attrValue.charAt(attrValue.length - 1)))
+ attrValueRange.end--;
+
+ result.push(attrValueRange);
+
+ if (attrName == 'class') {
+ result = result.concat(classNameRanges(attrValueRange.substring(source), attrValueRange.start));
+ }
+
+ break;
+ }
+ });
+
+ // offset ranges
+ _.each(result, function(r) {
+ r.shift(offset);
+ });
+
+ return _.chain(result)
+ .filter(function(item) { // remove empty
+ return !!item.length();
+ })
+ .uniq(false, function(item) { // remove duplicates
+ return item.toString();
+ })
+ .value();
+ }
+
+ /**
+ * Returns ranges of class names in "class" attribute value
+ * @param {String} className
+ * @returns {Array}
+ */
+ function classNameRanges(className, offset) {
+ offset = offset || 0;
+ var result = [];
+ /** @type StringStream */
+ var stream = require('stringStream').create(className);
+ var range = require('range');
+
+ // skip whitespace
+ stream.eatSpace();
+ stream.start = stream.pos;
+
+ var ch;
+ while (ch = stream.next()) {
+ if (/[\s\u00a0]/.test(ch)) {
+ result.push(range.create(stream.start + offset, stream.pos - stream.start - 1));
+ stream.eatSpace();
+ stream.start = stream.pos;
+ }
+ }
+
+ result.push(range.create(stream.start + offset, stream.pos - stream.start));
+ return result;
+ }
+
+ /**
+ * Returns best HTML tag range match for current selection
+ * @param {String} tag Tag declaration
+ * @param {Number} offset Tag's position index inside content
+ * @param {Range} selRange Selection range
+ * @return {Range} Returns range if next item was found, null
otherwise
+ */
+ function getRangeForHTMLItem(tag, offset, selRange, isBackward) {
+ var ranges = makePossibleRangesHTML(tag, require('xmlParser').parse(tag), offset);
+
+ if (isBackward)
+ ranges.reverse();
+
+ // try to find selected range
+ var curRange = _.find(ranges, function(r) {
+ return r.equal(selRange);
+ });
+
+ if (curRange) {
+ var ix = _.indexOf(ranges, curRange);
+ if (ix < ranges.length - 1)
+ return ranges[ix + 1];
+
+ return null;
+ }
+
+ // no selected range, find nearest one
+ if (isBackward)
+ // search backward
+ return _.find(ranges, function(r) {
+ return r.start < selRange.start;
+ });
+
+ // search forward
+ // to deal with overlapping ranges (like full attribute definition
+ // and attribute value) let's find range under caret first
+ if (!curRange) {
+ var matchedRanges = _.filter(ranges, function(r) {
+ return r.inside(selRange.end);
+ });
+
+ if (matchedRanges.length > 1)
+ return matchedRanges[1];
+ }
+
+
+ return _.find(ranges, function(r) {
+ return r.end > selRange.end;
+ });
+ }
+
+ /**
+ * Search for opening tag in content, starting at specified position
+ * @param {String} html Where to search tag
+ * @param {Number} pos Character index where to start searching
+ * @return {Range} Returns range if valid opening tag was found,
+ * null
otherwise
+ */
+ function findOpeningTagFromPosition(html, pos) {
+ var tag;
+ while (pos >= 0) {
+ if (tag = getOpeningTagFromPosition(html, pos))
+ return tag;
+ pos--;
+ }
+
+ return null;
+ }
+
+ /**
+ * @param {String} html Where to search tag
+ * @param {Number} pos Character index where to start searching
+ * @return {Range} Returns range if valid opening tag was found,
+ * null
otherwise
+ */
+ function getOpeningTagFromPosition(html, pos) {
+ var m;
+ if (html.charAt(pos) == '<' && (m = html.substring(pos, html.length).match(startTag))) {
+ return require('range').create(pos, m[0]);
+ }
+ }
+
+ function isQuote(ch) {
+ return ch == '"' || ch == "'";
+ }
+
+ /**
+ * Makes all possible selection ranges for specified CSS property
+ * @param {CSSProperty} property
+ * @returns {Array}
+ */
+ function makePossibleRangesCSS(property) {
+ // find all possible ranges, sorted by position and size
+ var valueRange = property.valueRange(true);
+ var result = [property.range(true), valueRange];
+ var stringStream = require('stringStream');
+ var cssEditTree = require('cssEditTree');
+ var range = require('range');
+
+ // locate parts of complex values.
+ // some examples:
+ // – 1px solid red: 3 parts
+ // – arial, sans-serif: enumeration, 2 parts
+ // – url(image.png): function value part
+ var value = property.value();
+ _.each(property.valueParts(), function(r) {
+ // add absolute range
+ var clone = r.clone();
+ result.push(clone.shift(valueRange.start));
+
+ /** @type StringStream */
+ var stream = stringStream.create(r.substring(value));
+ if (stream.match(/^[\w\-]+\(/, true)) {
+ // we have a function, find values in it.
+ // but first add function contents
+ stream.start = stream.pos;
+ stream.skipToPair('(', ')');
+ var fnBody = stream.current();
+ result.push(range.create(clone.start + stream.start, fnBody));
+
+ // find parts
+ _.each(cssEditTree.findParts(fnBody), function(part) {
+ result.push(range.create(clone.start + stream.start + part.start, part.substring(fnBody)));
+ });
+ }
+ });
+
+ // optimize result: remove empty ranges and duplicates
+ return _.chain(result)
+ .filter(function(item) {
+ return !!item.length();
+ })
+ .uniq(false, function(item) {
+ return item.toString();
+ })
+ .value();
+ }
+
+ /**
+ * Tries to find matched CSS property and nearest range for selection
+ * @param {CSSRule} rule
+ * @param {Range} selRange
+ * @param {Boolean} isBackward
+ * @returns {Range}
+ */
+ function matchedRangeForCSSProperty(rule, selRange, isBackward) {
+ /** @type CSSProperty */
+ var property = null;
+ var possibleRanges, curRange = null, ix;
+ var list = rule.list();
+ var searchFn, nearestItemFn;
+
+ if (isBackward) {
+ list.reverse();
+ searchFn = function(p) {
+ return p.range(true).start <= selRange.start;
+ };
+ nearestItemFn = function(r) {
+ return r.start < selRange.start;
+ };
+ } else {
+ searchFn = function(p) {
+ return p.range(true).end >= selRange.end;
+ };
+ nearestItemFn = function(r) {
+ return r.end > selRange.start;
+ };
+ }
+
+ // search for nearest to selection CSS property
+ while (property = _.find(list, searchFn)) {
+ possibleRanges = makePossibleRangesCSS(property);
+ if (isBackward)
+ possibleRanges.reverse();
+
+ // check if any possible range is already selected
+ curRange = _.find(possibleRanges, function(r) {
+ return r.equal(selRange);
+ });
+
+ if (!curRange) {
+ // no selection, select nearest item
+ var matchedRanges = _.filter(possibleRanges, function(r) {
+ return r.inside(selRange.end);
+ });
+
+ if (matchedRanges.length > 1) {
+ curRange = matchedRanges[1];
+ break;
+ }
+
+ if (curRange = _.find(possibleRanges, nearestItemFn))
+ break;
+ } else {
+ ix = _.indexOf(possibleRanges, curRange);
+ if (ix != possibleRanges.length - 1) {
+ curRange = possibleRanges[ix + 1];
+ break;
+ }
+ }
+
+ curRange = null;
+ selRange.start = selRange.end = isBackward
+ ? property.range(true).start - 1
+ : property.range(true).end + 1;
+ }
+
+ return curRange;
+ }
+
+ function findNextCSSItem(editor) {
+ return findItem(editor, false, require('cssEditTree').extractRule, getRangeForNextItemInCSS);
+ }
+
+ function findPrevCSSItem(editor) {
+ return findItem(editor, true, require('cssEditTree').extractRule, getRangeForPrevItemInCSS);
+ }
+
+ /**
+ * Returns range for item to be selected in CSS after current caret
+ * (selection) position
+ * @param {String} rule CSS rule declaration
+ * @param {Number} offset Rule's position index inside content
+ * @param {Range} selRange Selection range
+ * @return {Range} Returns range if next item was found, null
otherwise
+ */
+ function getRangeForNextItemInCSS(rule, offset, selRange) {
+ var tree = require('cssEditTree').parse(rule, {
+ offset: offset
+ });
+
+ // check if selector is matched
+ var range = tree.nameRange(true);
+ if (selRange.end < range.end) {
+ return range;
+ }
+
+ return matchedRangeForCSSProperty(tree, selRange, false);
+ }
+
+ /**
+ * Returns range for item to be selected in CSS before current caret
+ * (selection) position
+ * @param {String} rule CSS rule declaration
+ * @param {Number} offset Rule's position index inside content
+ * @param {Range} selRange Selection range
+ * @return {Range} Returns range if previous item was found, null
otherwise
+ */
+ function getRangeForPrevItemInCSS(rule, offset, selRange) {
+ var tree = require('cssEditTree').parse(rule, {
+ offset: offset
+ });
+
+ var curRange = matchedRangeForCSSProperty(tree, selRange, true);
+
+ if (!curRange) {
+ // no matched property, try to match selector
+ var range = tree.nameRange(true);
+ if (selRange.start > range.start) {
+ return range;
+ }
+ }
+
+ return curRange;
+ }
+
+ // XXX register actions
+ var actions = require('actions');
+ actions.add('select_next_item', function(editor){
+ if (editor.getSyntax() == 'css')
+ return findNextCSSItem(editor);
+ else
+ return findNextHTMLItem(editor);
+ });
+
+ actions.add('select_previous_item', function(editor){
+ if (editor.getSyntax() == 'css')
+ return findPrevCSSItem(editor);
+ else
+ return findPrevHTMLItem(editor);
+ });
+});/**
+ * HTML pair matching (balancing) actions
+ * @constructor
+ * @memberOf __matchPairActionDefine
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _) {
+ /** @type emmet.actions */
+ var actions = require('actions');
+ var matcher = require('htmlMatcher');
+ var lastMatch = null;
+
+ /**
+ * Find and select HTML tag pair
+ * @param {IEmmetEditor} editor Editor instance
+ * @param {String} direction Direction of pair matching: 'in' or 'out'.
+ * Default is 'out'
+ */
+ function matchPair(editor, direction) {
+ direction = String((direction || 'out').toLowerCase());
+ var info = require('editorUtils').outputInfo(editor);
+
+ var range = require('range');
+ /** @type Range */
+ var sel = range.create(editor.getSelectionRange());
+ var content = info.content;
+
+ // validate previous match
+ if (lastMatch && !lastMatch.range.equal(sel)) {
+ lastMatch = null;
+ }
+
+ if (lastMatch && sel.length()) {
+ if (direction == 'in') {
+ // user has previously selected tag and wants to move inward
+ if (lastMatch.type == 'tag' && !lastMatch.close) {
+ // unary tag was selected, can't move inward
+ return false;
+ } else {
+ if (lastMatch.range.equal(lastMatch.outerRange)) {
+ lastMatch.range = lastMatch.innerRange;
+ } else {
+ var narrowed = require('utils').narrowToNonSpace(content, lastMatch.innerRange);
+ lastMatch = matcher.find(content, narrowed.start + 1);
+ if (lastMatch && lastMatch.range.equal(sel) && lastMatch.outerRange.equal(sel)) {
+ lastMatch.range = lastMatch.innerRange;
+ }
+ }
+ }
+ } else {
+ if (
+ !lastMatch.innerRange.equal(lastMatch.outerRange)
+ && lastMatch.range.equal(lastMatch.innerRange)
+ && sel.equal(lastMatch.range)) {
+ lastMatch.range = lastMatch.outerRange;
+ } else {
+ lastMatch = matcher.find(content, sel.start);
+ if (lastMatch && lastMatch.range.equal(sel) && lastMatch.innerRange.equal(sel)) {
+ lastMatch.range = lastMatch.outerRange;
+ }
+ }
+ }
+ } else {
+ lastMatch = matcher.find(content, sel.start);
+ }
+
+ if (lastMatch && !lastMatch.range.equal(sel)) {
+ editor.createSelection(lastMatch.range.start, lastMatch.range.end);
+ return true;
+ }
+
+ lastMatch = null;
+ return false;
+ }
+
+ actions.add('match_pair', matchPair, {hidden: true});
+ actions.add('match_pair_inward', function(editor){
+ return matchPair(editor, 'in');
+ }, {label: 'HTML/Match Pair Tag (inward)'});
+
+ actions.add('match_pair_outward', function(editor){
+ return matchPair(editor, 'out');
+ }, {label: 'HTML/Match Pair Tag (outward)'});
+
+ /**
+ * Moves caret to matching opening or closing tag
+ * @param {IEmmetEditor} editor
+ */
+ actions.add('matching_pair', function(editor) {
+ var content = String(editor.getContent());
+ var caretPos = editor.getCaretPos();
+
+ if (content.charAt(caretPos) == '<')
+ // looks like caret is outside of tag pair
+ caretPos++;
+
+ var tag = matcher.tag(content, caretPos);
+ if (tag && tag.close) { // exclude unary tags
+ if (tag.open.range.inside(caretPos)) {
+ editor.setCaretPos(tag.close.range.start);
+ } else {
+ editor.setCaretPos(tag.open.range.start);
+ }
+
+ return true;
+ }
+
+ return false;
+ }, {label: 'HTML/Go To Matching Tag Pair'});
+});/**
+ * Gracefully removes tag under cursor
+ *
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _) {
+ require('actions').add('remove_tag', function(editor) {
+ var utils = require('utils');
+ var info = require('editorUtils').outputInfo(editor);
+
+ // search for tag
+ var tag = require('htmlMatcher').tag(info.content, editor.getCaretPos());
+ if (tag) {
+ if (!tag.close) {
+ // simply remove unary tag
+ editor.replaceContent(utils.getCaretPlaceholder(), tag.range.start, tag.range.end);
+ } else {
+ // remove tag and its newlines
+ /** @type Range */
+ var tagContentRange = utils.narrowToNonSpace(info.content, tag.innerRange);
+ /** @type Range */
+ var startLineBounds = utils.findNewlineBounds(info.content, tagContentRange.start);
+ var startLinePad = utils.getLinePadding(startLineBounds.substring(info.content));
+ var tagContent = tagContentRange.substring(info.content);
+
+ tagContent = utils.unindentString(tagContent, startLinePad);
+ editor.replaceContent(utils.getCaretPlaceholder() + utils.escapeText(tagContent), tag.outerRange.start, tag.outerRange.end);
+ }
+
+ return true;
+ }
+
+ return false;
+ }, {label: 'HTML/Remove Tag'});
+});
+/**
+ * Splits or joins tag, e.g. transforms it into a short notation and vice versa:donor
into receiver
+ * @param {CSSProperty} donor Donor CSS property from which value should
+ * be reflected
+ * @param {CSSProperty} receiver Property that should receive reflected
+ * value from donor
+ */
+ function reflectValue(donor, receiver) {
+ var value = getReflectedValue(donor.name(), donor.value(),
+ receiver.name(), receiver.value());
+
+ receiver.value(value);
+ }
+
+ /**
+ * Returns value that should be reflected for refName
CSS property
+ * from curName
property. This function is used for special cases,
+ * when the same result must be achieved with different properties for different
+ * browsers. For example: opаcity:0.5; → filter:alpha(opacity=50);CSSEditElement
object) and should
+ * return true
if it was performed successfully (handled
+ * reflection), false
otherwise.
+ * @param {Function} fn
+ * @param {Object} options
+ */
+ addHandler: function(fn, options) {
+ handlers.add(fn, options);
+ },
+
+ /**
+ * Removes registered handler
+ * @returns
+ */
+ removeHandler: function(fn) {
+ handlers.remove(fn, options);
+ }
+ };
+});/**
+ * Evaluates simple math expression under caret
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _) {
+ require('actions').add('evaluate_math_expression', function(editor) {
+ var actionUtils = require('actionUtils');
+ var utils = require('utils');
+
+ var content = String(editor.getContent());
+ var chars = '.+-*/\\';
+
+ /** @type Range */
+ var sel = require('range').create(editor.getSelectionRange());
+ if (!sel.length()) {
+ sel = actionUtils.findExpressionBounds(editor, function(ch) {
+ return utils.isNumeric(ch) || chars.indexOf(ch) != -1;
+ });
+ }
+
+ if (sel && sel.length()) {
+ var expr = sel.substring(content);
+
+ // replace integral division: 11\2 => Math.round(11/2)
+ expr = expr.replace(/([\d\.\-]+)\\([\d\.\-]+)/g, 'Math.round($1/$2)');
+
+ try {
+ var result = utils.prettifyNumber(new Function('return ' + expr)());
+ editor.replaceContent(result, sel.start, sel.end);
+ editor.setCaretPos(sel.start + result.length);
+ return true;
+ } catch (e) {}
+ }
+
+ return false;
+ }, {label: 'Numbers/Evaluate Math Expression'});
+});
+/**
+ * Increment/decrement number under cursor
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _) {
+ /**
+ * Extract number from current caret position of the editor
and
+ * increment it by step
+ * @param {IEmmetEditor} editor
+ * @param {Number} step Increment step (may be negative)
+ */
+ function incrementNumber(editor, step) {
+ var utils = require('utils');
+ var actionUtils = require('actionUtils');
+
+ var hasSign = false;
+ var hasDecimal = false;
+
+ var r = actionUtils.findExpressionBounds(editor, function(ch, pos, content) {
+ if (utils.isNumeric(ch))
+ return true;
+ if (ch == '.') {
+ // make sure that next character is numeric too
+ if (!utils.isNumeric(content.charAt(pos + 1)))
+ return false;
+
+ return hasDecimal ? false : hasDecimal = true;
+ }
+ if (ch == '-')
+ return hasSign ? false : hasSign = true;
+
+ return false;
+ });
+
+ if (r && r.length()) {
+ var strNum = r.substring(String(editor.getContent()));
+ var num = parseFloat(strNum);
+ if (!_.isNaN(num)) {
+ num = utils.prettifyNumber(num + step);
+
+ // do we have zero-padded number?
+ if (/^(\-?)0+[1-9]/.test(strNum)) {
+ var minus = '';
+ if (RegExp.$1) {
+ minus = '-';
+ num = num.substring(1);
+ }
+
+ var parts = num.split('.');
+ parts[0] = utils.zeroPadString(parts[0], intLength(strNum));
+ num = minus + parts.join('.');
+ }
+
+ editor.replaceContent(num, r.start, r.end);
+ editor.createSelection(r.start, r.start + num.length);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns length of integer part of number
+ * @param {String} num
+ */
+ function intLength(num) {
+ num = num.replace(/^\-/, '');
+ if (~num.indexOf('.')) {
+ return num.split('.')[0].length;
+ }
+
+ return num.length;
+ }
+
+ var actions = require('actions');
+ _.each([1, -1, 10, -10, 0.1, -0.1], function(num) {
+ var prefix = num > 0 ? 'increment' : 'decrement';
+
+ actions.add(prefix + '_number_by_' + String(Math.abs(num)).replace('.', '').substring(0, 2), function(editor) {
+ return incrementNumber(editor, num);
+ }, {label: 'Numbers/' + prefix.charAt(0).toUpperCase() + prefix.substring(1) + ' number by ' + Math.abs(num)});
+ });
+});/**
+ * Actions to insert line breaks. Some simple editors (like browser's
+ * <textarea>, for example) do not provide such simple things
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _) {
+ var actions = require('actions');
+ /** @type emmet.preferences */
+ var prefs = require('preferences');
+
+ // setup default preferences
+ prefs.define('css.closeBraceIndentation', '\n',
+ 'Indentation before closing brace of CSS rule. Some users prefere '
+ + 'indented closing brace of CSS rule for better readability. '
+ + 'This preference’s value will be automatically inserted before '
+ + 'closing brace when user adds newline in newly created CSS rule '
+ + '(e.g. when “Insert formatted linebreak” action will be performed '
+ + 'in CSS file). If you’re such user, you may want to write put a value '
+ + 'like \\n\\t
in this preference.');
+
+ /**
+ * Inserts newline character with proper indentation in specific positions only.
+ * @param {IEmmetEditor} editor
+ * @return {Boolean} Returns true
if line break was inserted
+ */
+ actions.add('insert_formatted_line_break_only', function(editor) {
+ var utils = require('utils');
+ /** @type emmet.resources */
+ var res = require('resources');
+
+ var info = require('editorUtils').outputInfo(editor);
+ var caretPos = editor.getCaretPos();
+ var nl = utils.getNewline();
+
+ if (_.include(['html', 'xml', 'xsl'], info.syntax)) {
+ var pad = res.getVariable('indentation');
+ // let's see if we're breaking newly created tag
+ var tag = require('htmlMatcher').tag(info.content, caretPos);
+ if (tag && !tag.innerRange.length()) {
+ editor.replaceContent(nl + pad + utils.getCaretPlaceholder() + nl, caretPos);
+ return true;
+ }
+ } else if (info.syntax == 'css') {
+ /** @type String */
+ var content = info.content;
+ if (caretPos && content.charAt(caretPos - 1) == '{') {
+ var append = prefs.get('css.closeBraceIndentation');
+ var pad = res.getVariable('indentation');
+
+ var hasCloseBrace = content.charAt(caretPos) == '}';
+ if (!hasCloseBrace) {
+ // do we really need special formatting here?
+ // check if this is really a newly created rule,
+ // look ahead for a closing brace
+ for (var i = caretPos, il = content.length, ch; i < il; i++) {
+ ch = content.charAt(i);
+ if (ch == '{') {
+ // ok, this is a new rule without closing brace
+ break;
+ }
+
+ if (ch == '}') {
+ // not a new rule, just add indentation
+ append = '';
+ hasCloseBrace = true;
+ break;
+ }
+ }
+ }
+
+ if (!hasCloseBrace) {
+ append += '}';
+ }
+
+ // defining rule set
+ var insValue = nl + pad + utils.getCaretPlaceholder() + append;
+ editor.replaceContent(insValue, caretPos);
+ return true;
+ }
+ }
+
+ return false;
+ }, {hidden: true});
+
+ /**
+ * Inserts newline character with proper indentation. This action is used in
+ * editors that doesn't have indentation control (like textarea element) to
+ * provide proper indentation
+ * @param {IEmmetEditor} editor Editor instance
+ */
+ actions.add('insert_formatted_line_break', function(editor) {
+ if (!actions.run('insert_formatted_line_break_only', editor)) {
+ var utils = require('utils');
+
+ var curPadding = require('editorUtils').getCurrentLinePadding(editor);
+ var content = String(editor.getContent());
+ var caretPos = editor.getCaretPos();
+ var len = content.length;
+ var nl = utils.getNewline();
+
+ // check out next line padding
+ var lineRange = editor.getCurrentLineRange();
+ var nextPadding = '';
+
+ for (var i = lineRange.end + 1, ch; i < len; i++) {
+ ch = content.charAt(i);
+ if (ch == ' ' || ch == '\t')
+ nextPadding += ch;
+ else
+ break;
+ }
+
+ if (nextPadding.length > curPadding.length)
+ editor.replaceContent(nl + nextPadding, caretPos, caretPos, true);
+ else
+ editor.replaceContent(nl, caretPos);
+ }
+
+ return true;
+ }, {hidden: true});
+});/**
+ * Merges selected lines or lines between XHTML tag pairs
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _) {
+ require('actions').add('merge_lines', function(editor) {
+ var matcher = require('htmlMatcher');
+ var utils = require('utils');
+ var editorUtils = require('editorUtils');
+ var info = editorUtils.outputInfo(editor);
+
+ /** @type Range */
+ var selection = require('range').create(editor.getSelectionRange());
+ if (!selection.length()) {
+ // find matching tag
+ var pair = matcher.find(info.content, editor.getCaretPos());
+ if (pair) {
+ selection = pair.outerRange;
+ }
+ }
+
+ if (selection.length()) {
+ // got range, merge lines
+ var text = selection.substring(info.content);
+ var lines = utils.splitByLines(text);
+
+ for (var i = 1; i < lines.length; i++) {
+ lines[i] = lines[i].replace(/^\s+/, '');
+ }
+
+ text = lines.join('').replace(/\s{2,}/, ' ');
+ var textLen = text.length;
+ text = utils.escapeText(text);
+ editor.replaceContent(text, selection.start, selection.end);
+ editor.createSelection(selection.start, selection.start + textLen);
+
+ return true;
+ }
+
+ return false;
+ });
+});/**
+ * Encodes/decodes image under cursor to/from base64
+ * @param {IEmmetEditor} editor
+ * @since 0.65
+ *
+ * @memberOf __base64ActionDefine
+ * @constructor
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _) {
+ require('actions').add('encode_decode_data_url', function(editor) {
+ var data = String(editor.getSelection());
+ var caretPos = editor.getCaretPos();
+
+ if (!data) {
+ // no selection, try to find image bounds from current caret position
+ var text = String(editor.getContent()), m;
+ while (caretPos-- >= 0) {
+ if (startsWith('src=', text, caretPos)) { // found
+ if (m = text.substr(caretPos).match(/^(src=(["'])?)([^'"<>\s]+)\1?/)) {
+ data = m[3];
+ caretPos += m[1].length;
+ }
+ break;
+ } else if (startsWith('url(', text, caretPos)) { // found CSS url() pattern
+ if (m = text.substr(caretPos).match(/^(url\((['"])?)([^'"\)\s]+)\1?/)) {
+ data = m[3];
+ caretPos += m[1].length;
+ }
+ break;
+ }
+ }
+ }
+
+ if (data) {
+ if (startsWith('data:', data))
+ return decodeFromBase64(editor, data, caretPos);
+ else
+ return encodeToBase64(editor, data, caretPos);
+ }
+
+ return false;
+ }, {label: 'Encode\\Decode data:URL image'});
+
+ /**
+ * Test if text
starts with token
at pos
+ * position. If pos
is omitted, search from beginning of text
+ * @param {String} token Token to test
+ * @param {String} text Where to search
+ * @param {Number} pos Position where to start search
+ * @return {Boolean}
+ * @since 0.65
+ */
+ function startsWith(token, text, pos) {
+ pos = pos || 0;
+ return text.charAt(pos) == token.charAt(0) && text.substr(pos, token.length) == token;
+ }
+
+ /**
+ * Encodes image to base64
+ *
+ * @param {IEmmetEditor} editor
+ * @param {String} imgPath Path to image
+ * @param {Number} pos Caret position where image is located in the editor
+ * @return {Boolean}
+ */
+ function encodeToBase64(editor, imgPath, pos) {
+ var file = require('file');
+ var actionUtils = require('actionUtils');
+
+ var editorFile = editor.getFilePath();
+ var defaultMimeType = 'application/octet-stream';
+
+ if (editorFile === null) {
+ throw "You should save your file before using this action";
+ }
+
+ // locate real image path
+ var realImgPath = file.locateFile(editorFile, imgPath);
+ if (realImgPath === null) {
+ throw "Can't find " + imgPath + ' file';
+ }
+
+ var b64 = require('base64').encode(String(file.read(realImgPath)));
+ if (!b64) {
+ throw "Can't encode file content to base64";
+ }
+
+ b64 = 'data:' + (actionUtils.mimeTypes[String(file.getExt(realImgPath))] || defaultMimeType) +
+ ';base64,' + b64;
+
+ editor.replaceContent('$0' + b64, pos, pos + imgPath.length);
+ return true;
+ }
+
+ /**
+ * Decodes base64 string back to file.
+ * @param {IEmmetEditor} editor
+ * @param {String} data Base64-encoded file content
+ * @param {Number} pos Caret position where image is located in the editor
+ */
+ function decodeFromBase64(editor, data, pos) {
+ // ask user to enter path to file
+ var filePath = String(editor.prompt('Enter path to file (absolute or relative)'));
+ if (!filePath)
+ return false;
+
+ var file = require('file');
+ var absPath = file.createPath(editor.getFilePath(), filePath);
+ if (!absPath) {
+ throw "Can't save file";
+ }
+
+ file.save(absPath, require('base64').decode( data.replace(/^data\:.+?;.+?,/, '') ));
+ editor.replaceContent('$0' + filePath, pos, pos + data.length);
+ return true;
+ }
+});
+/**
+ * Automatically updates image size attributes in HTML's <img> element or
+ * CSS rule
+ * @param {Function} require
+ * @param {Underscore} _
+ * @constructor
+ * @memberOf __updateImageSizeAction
+ */
+emmet.exec(function(require, _) {
+ /**
+ * Updates image size of <img src=""> tag
+ * @param {IEmmetEditor} editor
+ */
+ function updateImageSizeHTML(editor) {
+ var offset = editor.getCaretPos();
+
+ // find tag from current caret position
+ var info = require('editorUtils').outputInfo(editor);
+ var xmlElem = require('xmlEditTree').parseFromPosition(info.content, offset, true);
+ if (xmlElem && (xmlElem.name() || '').toLowerCase() == 'img') {
+
+ var size = getImageSizeForSource(editor, xmlElem.value('src'));
+ if (size) {
+ var compoundData = xmlElem.range(true);
+ xmlElem.value('width', size.width);
+ xmlElem.value('height', size.height, xmlElem.indexOf('width') + 1);
+
+ return _.extend(compoundData, {
+ data: xmlElem.toString(),
+ caret: offset
+ });
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Updates image size of CSS property
+ * @param {IEmmetEditor} editor
+ */
+ function updateImageSizeCSS(editor) {
+ var offset = editor.getCaretPos();
+
+ // find tag from current caret position
+ var info = require('editorUtils').outputInfo(editor);
+ var cssRule = require('cssEditTree').parseFromPosition(info.content, offset, true);
+ if (cssRule) {
+ // check if there is property with image under caret
+ var prop = cssRule.itemFromPosition(offset, true), m;
+ if (prop && (m = /url\((["']?)(.+?)\1\)/i.exec(prop.value() || ''))) {
+ var size = getImageSizeForSource(editor, m[2]);
+ if (size) {
+ var compoundData = cssRule.range(true);
+ cssRule.value('width', size.width + 'px');
+ cssRule.value('height', size.height + 'px', cssRule.indexOf('width') + 1);
+
+ return _.extend(compoundData, {
+ data: cssRule.toString(),
+ caret: offset
+ });
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Returns image dimensions for source
+ * @param {IEmmetEditor} editor
+ * @param {String} src Image source (path or data:url)
+ */
+ function getImageSizeForSource(editor, src) {
+ var fileContent;
+ if (src) {
+ // check if it is data:url
+ if (/^data:/.test(src)) {
+ fileContent = require('base64').decode( src.replace(/^data\:.+?;.+?,/, '') );
+ } else {
+ var file = require('file');
+ var absPath = file.locateFile(editor.getFilePath(), src);
+ if (absPath === null) {
+ throw "Can't find " + src + ' file';
+ }
+
+ fileContent = String(file.read(absPath));
+ }
+
+ return require('actionUtils').getImageSize(fileContent);
+ }
+ }
+
+ require('actions').add('update_image_size', function(editor) {
+ var result;
+ // this action will definitely won’t work in SASS dialect,
+ // but may work in SCSS or LESS
+ if (_.include(['css', 'less', 'scss'], String(editor.getSyntax()))) {
+ result = updateImageSizeCSS(editor);
+ } else {
+ result = updateImageSizeHTML(editor);
+ }
+
+ return require('actionUtils').compoundUpdate(editor, result);
+ });
+});/**
+ * Resolver for fast CSS typing. Handles abbreviations with the following
+ * notation:(-vendor prefix)?property(value)*(!)?
+ *
+ * foo
abbreviation will generate
+ * <foo></foo>
output.
+ * property-name: ${1}
snippets.
+ *
+ * Vendor prefixesbrad
+ * abbreviation generates border-radius: ${value};
snippet,
+ * the -brad
abbreviation should generate:
+ *
+ * -webkit-border-radius: ${value};
+ * -moz-border-radius: ${value};
+ * border-radius: ${value};
+ *
+ * Note that o and ms prefixes are omitted since Opera and IE
+ * supports unprefixed property.-wm-float
will produce
+ *
+ *
+ * -webkit-float: ${1};
+ * -moz-float: ${1};
+ * float: ${1};
+ *
+ *
+ * Although this example looks pointless, users can use this feature to write
+ * cutting-edge properties implemented by browser vendors recently.
+ *
+ * @constructor
+ * @memberOf __cssResolverDefine
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.define('cssResolver', function(require, _) {
+ /** Back-reference to module */
+ var module = null;
+
+ var prefixObj = {
+ /** Real vendor prefix name */
+ prefix: 'emmet',
+
+ /**
+ * Indicates this prefix is obsolete and should't be used when user
+ * wants to generate all-prefixed properties
+ */
+ obsolete: false,
+
+ /**
+ * Returns prefixed CSS property name
+ * @param {String} name Unprefixed CSS property
+ */
+ transformName: function(name) {
+ return '-' + this.prefix + '-' + name;
+ },
+
+ /**
+ * List of unprefixed CSS properties that supported by
+ * current prefix. This list is used to generate all-prefixed property
+ * @returns {Array}
+ */
+ properties: function() {
+ return getProperties('css.' + this.prefix + 'Properties') || [];
+ },
+
+ /**
+ * Check if given property is supported by current prefix
+ * @param name
+ */
+ supports: function(name) {
+ return _.include(this.properties(), name);
+ }
+ };
+
+
+ /**
+ * List of registered one-character prefixes. Key is a one-character prefix,
+ * value is an prefixObj
object
+ */
+ var vendorPrefixes = {};
+
+ var defaultValue = '${1};';
+
+ // XXX module preferences
+ var prefs = require('preferences');
+ prefs.define('css.valueSeparator', ': ',
+ 'Defines a symbol that should be placed between CSS property and '
+ + 'value when expanding CSS abbreviations.');
+ prefs.define('css.propertyEnd', ';',
+ 'Defines a symbol that should be placed at the end of CSS property '
+ + 'when expanding CSS abbreviations.');
+
+ prefs.define('stylus.valueSeparator', ' ',
+ 'Defines a symbol that should be placed between CSS property and '
+ + 'value when expanding CSS abbreviations in Stylus dialect.');
+ prefs.define('stylus.propertyEnd', '',
+ 'Defines a symbol that should be placed at the end of CSS property '
+ + 'when expanding CSS abbreviations in Stylus dialect.');
+
+ prefs.define('sass.propertyEnd', '',
+ 'Defines a symbol that should be placed at the end of CSS property '
+ + 'when expanding CSS abbreviations in SASS dialect.');
+
+ prefs.define('css.autoInsertVendorPrefixes', true,
+ 'Automatically generate vendor-prefixed copies of expanded CSS '
+ + 'property. By default, Emmet will generate vendor-prefixed '
+ + 'properties only when you put dash before abbreviation '
+ + '(e.g. -bxsh
). With this option enabled, you don’t '
+ + 'need dashes before abbreviations: Emmet will produce '
+ + 'vendor-prefixed properties for you.');
+
+ var descTemplate = _.template('A comma-separated list of CSS properties that may have '
+ + '<%= vendor %>
vendor prefix. This list is used to generate '
+ + 'a list of prefixed properties when expanding -property
'
+ + 'abbreviations. Empty list means that all possible CSS values may '
+ + 'have <%= vendor %>
prefix.');
+
+ var descAddonTemplate = _.template('A comma-separated list of additional CSS properties '
+ + 'for css.<%= vendor %>Preperties
preference. '
+ + 'You should use this list if you want to add or remove a few CSS '
+ + 'properties to original set. To add a new property, simply write its name, '
+ + 'to remove it, precede property with hyphen.foo, -border-radius
.');
+
+ // properties list is created from cssFeatures.html file
+ var props = {
+ 'webkit': 'animation, animation-delay, animation-direction, animation-duration, animation-fill-mode, animation-iteration-count, animation-name, animation-play-state, animation-timing-function, appearance, backface-visibility, background-clip, background-composite, background-origin, background-size, border-fit, border-horizontal-spacing, border-image, border-vertical-spacing, box-align, box-direction, box-flex, box-flex-group, box-lines, box-ordinal-group, box-orient, box-pack, box-reflect, box-shadow, color-correction, column-break-after, column-break-before, column-break-inside, column-count, column-gap, column-rule-color, column-rule-style, column-rule-width, column-span, column-width, dashboard-region, font-smoothing, highlight, hyphenate-character, hyphenate-limit-after, hyphenate-limit-before, hyphens, line-box-contain, line-break, line-clamp, locale, margin-before-collapse, margin-after-collapse, marquee-direction, marquee-increment, marquee-repetition, marquee-style, mask-attachment, mask-box-image, mask-box-image-outset, mask-box-image-repeat, mask-box-image-slice, mask-box-image-source, mask-box-image-width, mask-clip, mask-composite, mask-image, mask-origin, mask-position, mask-repeat, mask-size, nbsp-mode, perspective, perspective-origin, rtl-ordering, text-combine, text-decorations-in-effect, text-emphasis-color, text-emphasis-position, text-emphasis-style, text-fill-color, text-orientation, text-security, text-stroke-color, text-stroke-width, transform, transition, transform-origin, transform-style, transition-delay, transition-duration, transition-property, transition-timing-function, user-drag, user-modify, user-select, writing-mode, svg-shadow, box-sizing, border-radius',
+ 'moz': 'animation-delay, animation-direction, animation-duration, animation-fill-mode, animation-iteration-count, animation-name, animation-play-state, animation-timing-function, appearance, backface-visibility, background-inline-policy, binding, border-bottom-colors, border-image, border-left-colors, border-right-colors, border-top-colors, box-align, box-direction, box-flex, box-ordinal-group, box-orient, box-pack, box-shadow, box-sizing, column-count, column-gap, column-rule-color, column-rule-style, column-rule-width, column-width, float-edge, font-feature-settings, font-language-override, force-broken-image-icon, hyphens, image-region, orient, outline-radius-bottomleft, outline-radius-bottomright, outline-radius-topleft, outline-radius-topright, perspective, perspective-origin, stack-sizing, tab-size, text-blink, text-decoration-color, text-decoration-line, text-decoration-style, text-size-adjust, transform, transform-origin, transform-style, transition, transition-delay, transition-duration, transition-property, transition-timing-function, user-focus, user-input, user-modify, user-select, window-shadow, background-clip, border-radius',
+ 'ms': 'accelerator, backface-visibility, background-position-x, background-position-y, behavior, block-progression, box-align, box-direction, box-flex, box-line-progression, box-lines, box-ordinal-group, box-orient, box-pack, content-zoom-boundary, content-zoom-boundary-max, content-zoom-boundary-min, content-zoom-chaining, content-zoom-snap, content-zoom-snap-points, content-zoom-snap-type, content-zooming, filter, flow-from, flow-into, font-feature-settings, grid-column, grid-column-align, grid-column-span, grid-columns, grid-layer, grid-row, grid-row-align, grid-row-span, grid-rows, high-contrast-adjust, hyphenate-limit-chars, hyphenate-limit-lines, hyphenate-limit-zone, hyphens, ime-mode, interpolation-mode, layout-flow, layout-grid, layout-grid-char, layout-grid-line, layout-grid-mode, layout-grid-type, line-break, overflow-style, perspective, perspective-origin, perspective-origin-x, perspective-origin-y, scroll-boundary, scroll-boundary-bottom, scroll-boundary-left, scroll-boundary-right, scroll-boundary-top, scroll-chaining, scroll-rails, scroll-snap-points-x, scroll-snap-points-y, scroll-snap-type, scroll-snap-x, scroll-snap-y, scrollbar-arrow-color, scrollbar-base-color, scrollbar-darkshadow-color, scrollbar-face-color, scrollbar-highlight-color, scrollbar-shadow-color, scrollbar-track-color, text-align-last, text-autospace, text-justify, text-kashida-space, text-overflow, text-size-adjust, text-underline-position, touch-action, transform, transform-origin, transform-origin-x, transform-origin-y, transform-origin-z, transform-style, transition, transition-delay, transition-duration, transition-property, transition-timing-function, user-select, word-break, word-wrap, wrap-flow, wrap-margin, wrap-through, writing-mode',
+ 'o': 'dashboard-region, animation, animation-delay, animation-direction, animation-duration, animation-fill-mode, animation-iteration-count, animation-name, animation-play-state, animation-timing-function, border-image, link, link-source, object-fit, object-position, tab-size, table-baseline, transform, transform-origin, transition, transition-delay, transition-duration, transition-property, transition-timing-function, accesskey, input-format, input-required, marquee-dir, marquee-loop, marquee-speed, marquee-style'
+ };
+
+ _.each(props, function(v, k) {
+ prefs.define('css.' + k + 'Properties', v, descTemplate({vendor: k}));
+ prefs.define('css.' + k + 'PropertiesAddon', '', descAddonTemplate({vendor: k}));
+ });
+
+ prefs.define('css.unitlessProperties', 'z-index, line-height, opacity, font-weight, zoom',
+ 'The list of properties whose values must not contain units.');
+
+ prefs.define('css.intUnit', 'px', 'Default unit for integer values');
+ prefs.define('css.floatUnit', 'em', 'Default unit for float values');
+
+ prefs.define('css.keywords', 'auto, inherit',
+ 'A comma-separated list of valid keywords that can be used in CSS abbreviations.');
+
+ prefs.define('css.keywordAliases', 'a:auto, i:inherit, s:solid, da:dashed, do:dotted',
+ 'A comma-separated list of keyword aliases, used in CSS abbreviation. '
+ + 'Each alias should be defined as alias:keyword_name
.');
+
+ prefs.define('css.unitAliases', 'e:em, p:%, x:ex, r:rem',
+ 'A comma-separated list of unit aliases, used in CSS abbreviation. '
+ + 'Each alias should be defined as alias:unit_value
.');
+
+ prefs.define('css.color.short', true,
+ 'Should color values like #ffffff
be shortened to '
+ + '#fff
after abbreviation with color was expanded.');
+
+ prefs.define('css.color.case', 'keep',
+ 'Letter case of color values generated by abbreviations with color '
+ + '(like c#0
). Possible values are upper
, '
+ + 'lower
and keep
.');
+
+ prefs.define('css.fuzzySearch', true,
+ 'Enable fuzzy search among CSS snippet names. When enabled, every '
+ + 'unknown snippet will be scored against available snippet '
+ + 'names (not values or CSS properties!). The match with best score '
+ + 'will be used to resolve snippet value. For example, with this '
+ + 'preference enabled, the following abbreviations are equal: '
+ + 'ov:h
== ov-h
== o-h
== '
+ + 'oh
');
+
+ prefs.define('css.fuzzySearchMinScore', 0.3,
+ 'The minium score (from 0 to 1) that fuzzy-matched abbreviation should '
+ + 'achive. Lower values may produce many false-positive matches, '
+ + 'higher values may reduce possible matches.');
+
+
+ function isNumeric(ch) {
+ var code = ch && ch.charCodeAt(0);
+ return (ch && ch == '.' || (code > 47 && code < 58));
+ }
+
+ /**
+ * Check if provided snippet contains only one CSS property and value.
+ * @param {String} snippet
+ * @returns {Boolean}
+ */
+ function isSingleProperty(snippet) {
+ var utils = require('utils');
+ snippet = utils.trim(snippet);
+
+ // check if it doesn't contain a comment and a newline
+ if (~snippet.indexOf('/*') || /[\n\r]/.test(snippet)) {
+ return false;
+ }
+
+ // check if it's a valid snippet definition
+ if (!/^[a-z0-9\-]+\s*\:/i.test(snippet)) {
+ return false;
+ }
+
+ snippet = require('tabStops').processText(snippet, {
+ replaceCarets: true,
+ tabstop: function() {
+ return 'value';
+ }
+ });
+
+ return snippet.split(':').length == 2;
+ }
+
+ /**
+ * Normalizes abbreviated value to final CSS one
+ * @param {String} value
+ * @returns {String}
+ */
+ function normalizeValue(value) {
+ if (value.charAt(0) == '-' && !/^\-[\.\d]/) {
+ value = value.replace(/^\-+/, '');
+ }
+
+ if (value.charAt(0) == '#') {
+ return normalizeHexColor(value);
+ }
+
+ return getKeyword(value);
+ }
+
+ function normalizeHexColor(value) {
+ var hex = value.replace(/^#+/, '') || '0';
+ var repeat = require('utils').repeatString;
+ var color = null;
+ switch (hex.length) {
+ case 1:
+ color = repeat(hex, 6);
+ break;
+ case 2:
+ color = repeat(hex, 3);
+ break;
+ case 3:
+ color = hex.charAt(0) + hex.charAt(0) + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2);
+ break;
+ case 4:
+ color = hex + hex.substr(0, 2);
+ break;
+ case 5:
+ color = hex + hex.charAt(0);
+ break;
+ default:
+ color = hex.substr(0, 6);
+ }
+
+ // color must be shortened?
+ if (prefs.get('css.color.short')) {
+ var p = color.split('');
+ if (p[0] == p[1] && p[2] == p[3] && p[4] == p[5]) {
+ color = p[0] + p[2] + p[4];
+ }
+ }
+
+ // should transform case?
+ switch (prefs.get('css.color.case')) {
+ case 'upper':
+ color = color.toUpperCase();
+ break;
+ case 'lower':
+ color = color.toLowerCase();
+ break;
+ }
+
+ return '#' + color;
+ }
+
+ function getKeyword(name) {
+ var aliases = prefs.getDict('css.keywordAliases');
+ return name in aliases ? aliases[name] : name;
+ }
+
+ function getUnit(name) {
+ var aliases = prefs.getDict('css.unitAliases');
+ return name in aliases ? aliases[name] : name;
+ }
+
+ function isValidKeyword(keyword) {
+ return _.include(prefs.getArray('css.keywords'), getKeyword(keyword));
+ }
+
+ /**
+ * Split snippet into a CSS property-value pair
+ * @param {String} snippet
+ */
+ function splitSnippet(snippet) {
+ var utils = require('utils');
+ snippet = utils.trim(snippet);
+ if (snippet.indexOf(':') == -1) {
+ return {
+ name: snippet,
+ value: defaultValue
+ };
+ }
+
+ var pair = snippet.split(':');
+
+ return {
+ name: utils.trim(pair.shift()),
+ // replace ${0} tabstop to produce valid vendor-prefixed values
+ // where possible
+ value: utils.trim(pair.join(':')).replace(/^(\$\{0\}|\$0)(\s*;?)$/, '${1}$2')
+ };
+ }
+
+ /**
+ * Check if passed CSS property support specified vendor prefix
+ * @param {String} property
+ * @param {String} prefix
+ */
+ function hasPrefix(property, prefix) {
+ var info = vendorPrefixes[prefix];
+
+ if (!info)
+ info = _.find(vendorPrefixes, function(data) {
+ return data.prefix == prefix;
+ });
+
+ return info && info.supports(property);
+ }
+
+ /**
+ * Search for a list of supported prefixes for CSS property. This list
+ * is used to generate all-prefixed snippet
+ * @param {String} property CSS property name
+ * @returns {Array}
+ */
+ function findPrefixes(property, noAutofill) {
+ var result = [];
+ _.each(vendorPrefixes, function(obj, prefix) {
+ if (hasPrefix(property, prefix)) {
+ result.push(prefix);
+ }
+ });
+
+ if (!result.length && !noAutofill) {
+ // add all non-obsolete prefixes
+ _.each(vendorPrefixes, function(obj, prefix) {
+ if (!obj.obsolete)
+ result.push(prefix);
+ });
+ }
+
+ return result;
+ }
+
+ function addPrefix(name, obj) {
+ if (_.isString(obj))
+ obj = {prefix: obj};
+
+ vendorPrefixes[name] = _.extend({}, prefixObj, obj);
+ }
+
+ function getSyntaxPreference(name, syntax) {
+ if (syntax) {
+ var val = prefs.get(syntax + '.' + name);
+ if (!_.isUndefined(val))
+ return val;
+ }
+
+ return prefs.get('css.' + name);
+ }
+
+ /**
+ * Format CSS property according to current syntax dialect
+ * @param {String} property
+ * @param {String} syntax
+ * @returns {String}
+ */
+ function formatProperty(property, syntax) {
+ var ix = property.indexOf(':');
+ property = property.substring(0, ix).replace(/\s+$/, '')
+ + getSyntaxPreference('valueSeparator', syntax)
+ + require('utils').trim(property.substring(ix + 1));
+
+ return property.replace(/\s*;\s*$/, getSyntaxPreference('propertyEnd', syntax));
+ }
+
+ /**
+ * Transforms snippet value if required. For example, this transformation
+ * may add !important declaration to CSS property
+ * @param {String} snippet
+ * @param {Boolean} isImportant
+ * @returns {String}
+ */
+ function transformSnippet(snippet, isImportant, syntax) {
+ if (!_.isString(snippet))
+ snippet = snippet.data;
+
+ if (!isSingleProperty(snippet))
+ return snippet;
+
+ if (isImportant) {
+ if (~snippet.indexOf(';')) {
+ snippet = snippet.split(';').join(' !important;');
+ } else {
+ snippet += ' !important';
+ }
+ }
+
+ return formatProperty(snippet, syntax);
+ }
+
+ /**
+ * Helper function that parses comma-separated list of elements into array
+ * @param {String} list
+ * @returns {Array}
+ */
+ function parseList(list) {
+ var result = _.map((list || '').split(','), require('utils').trim);
+ return result.length ? result : null;
+ }
+
+ function getProperties(key) {
+ var list = prefs.getArray(key);
+ _.each(prefs.getArray(key + 'Addon'), function(prop) {
+ if (prop.charAt(0) == '-') {
+ list = _.without(list, prop.substr(1));
+ } else {
+ if (prop.charAt(0) == '+')
+ prop = prop.substr(1);
+
+ list.push(prop);
+ }
+ });
+
+ return list;
+ }
+
+
+ // TODO refactor, this looks awkward now
+ addPrefix('w', {
+ prefix: 'webkit'
+ });
+ addPrefix('m', {
+ prefix: 'moz'
+ });
+ addPrefix('s', {
+ prefix: 'ms'
+ });
+ addPrefix('o', {
+ prefix: 'o'
+ });
+
+ // I think nobody uses it
+// addPrefix('k', {
+// prefix: 'khtml',
+// obsolete: true
+// });
+
+ var cssSyntaxes = ['css', 'less', 'sass', 'scss', 'stylus'];
+
+ /**
+ * XXX register resolver
+ * @param {TreeNode} node
+ * @param {String} syntax
+ */
+ require('resources').addResolver(function(node, syntax) {
+ if (_.include(cssSyntaxes, syntax) && node.isElement()) {
+ return module.expandToSnippet(node.abbreviation, syntax);
+ }
+
+ return null;
+ });
+
+ var ea = require('expandAbbreviation');
+ /**
+ * For CSS-like syntaxes, we need to handle a special use case. Some editors
+ * (like Sublime Text 2) may insert semicolons automatically when user types
+ * abbreviation. After expansion, user receives a double semicolon. This
+ * handler automatically removes semicolon from generated content in such cases.
+ * @param {IEmmetEditor} editor
+ * @param {String} syntax
+ * @param {String} profile
+ */
+ ea.addHandler(function(editor, syntax, profile) {
+ if (!_.include(cssSyntaxes, syntax)) {
+ return false;
+ }
+
+ var caretPos = editor.getSelectionRange().end;
+ var abbr = ea.findAbbreviation(editor);
+
+ if (abbr) {
+ var content = emmet.expandAbbreviation(abbr, syntax, profile);
+ if (content) {
+ var replaceFrom = caretPos - abbr.length;
+ var replaceTo = caretPos;
+ if (editor.getContent().charAt(caretPos) == ';' && content.charAt(content.length - 1) == ';') {
+ replaceTo++;
+ }
+
+ editor.replaceContent(content, replaceFrom, replaceTo);
+ return true;
+ }
+ }
+
+ return false;
+ });
+
+ return module = {
+ /**
+ * Adds vendor prefix
+ * @param {String} name One-character prefix name
+ * @param {Object} obj Object describing vendor prefix
+ * @memberOf cssResolver
+ */
+ addPrefix: addPrefix,
+
+ /**
+ * Check if passed CSS property supports specified vendor prefix
+ * @param {String} property
+ * @param {String} prefix
+ */
+ supportsPrefix: hasPrefix,
+
+ /**
+ * Returns prefixed version of passed CSS property, only if this
+ * property supports such prefix
+ * @param {String} property
+ * @param {String} prefix
+ * @returns
+ */
+ prefixed: function(property, prefix) {
+ return hasPrefix(property, prefix)
+ ? '-' + prefix + '-' + property
+ : property;
+ },
+
+ /**
+ * Returns list of all registered vendor prefixes
+ * @returns {Array}
+ */
+ listPrefixes: function() {
+ return _.map(vendorPrefixes, function(obj) {
+ return obj.prefix;
+ });
+ },
+
+ /**
+ * Returns object describing vendor prefix
+ * @param {String} name
+ * @returns {Object}
+ */
+ getPrefix: function(name) {
+ return vendorPrefixes[name];
+ },
+
+ /**
+ * Removes prefix object
+ * @param {String} name
+ */
+ removePrefix: function(name) {
+ if (name in vendorPrefixes)
+ delete vendorPrefixes[name];
+ },
+
+ /**
+ * Extract vendor prefixes from abbreviation
+ * @param {String} abbr
+ * @returns {Object} Object containing array of prefixes and clean
+ * abbreviation name
+ */
+ extractPrefixes: function(abbr) {
+ if (abbr.charAt(0) != '-') {
+ return {
+ property: abbr,
+ prefixes: null
+ };
+ }
+
+ // abbreviation may either contain sequence of one-character prefixes
+ // or just dash, meaning that user wants to produce all possible
+ // prefixed properties
+ var i = 1, il = abbr.length, ch;
+ var prefixes = [];
+
+ while (i < il) {
+ ch = abbr.charAt(i);
+ if (ch == '-') {
+ // end-sequence character found, stop searching
+ i++;
+ break;
+ }
+
+ if (ch in vendorPrefixes) {
+ prefixes.push(ch);
+ } else {
+ // no prefix found, meaning user want to produce all
+ // vendor-prefixed properties
+ prefixes.length = 0;
+ i = 1;
+ break;
+ }
+
+ i++;
+ }
+
+ // reached end of abbreviation and no property name left
+ if (i == il -1) {
+ i = 1;
+ prefixes.length = 1;
+ }
+
+ return {
+ property: abbr.substring(i),
+ prefixes: prefixes.length ? prefixes : 'all'
+ };
+ },
+
+ /**
+ * Search for value substring in abbreviation
+ * @param {String} abbr
+ * @returns {String} Value substring
+ */
+ findValuesInAbbreviation: function(abbr, syntax) {
+ syntax = syntax || 'css';
+
+ var i = 0, il = abbr.length, value = '', ch;
+ while (i < il) {
+ ch = abbr.charAt(i);
+ if (isNumeric(ch) || ch == '#' || (ch == '-' && isNumeric(abbr.charAt(i + 1)))) {
+ value = abbr.substring(i);
+ break;
+ }
+
+ i++;
+ }
+
+ // try to find keywords in abbreviation
+ var property = abbr.substring(0, abbr.length - value.length);
+ var res = require('resources');
+ var keywords = [];
+ // try to extract some commonly-used properties
+ while (~property.indexOf('-') && !res.findSnippet(syntax, property)) {
+ var parts = property.split('-');
+ var lastPart = parts.pop();
+ if (!isValidKeyword(lastPart)) {
+ break;
+ }
+
+ keywords.unshift(lastPart);
+ property = parts.join('-');
+ }
+
+ return keywords.join('-') + value;
+ },
+
+ parseValues: function(str) {
+ /** @type StringStream */
+ var stream = require('stringStream').create(str);
+ var values = [];
+ var ch = null;
+
+ while (ch = stream.next()) {
+ if (ch == '#') {
+ stream.match(/^[0-9a-f]+/, true);
+ values.push(stream.current());
+ } else if (ch == '-') {
+ if (isValidKeyword(_.last(values)) ||
+ ( stream.start && isNumeric(str.charAt(stream.start - 1)) )
+ ) {
+ stream.start = stream.pos;
+ }
+
+ stream.match(/^\-?[0-9]*(\.[0-9]+)?[a-z%\.]*/, true);
+ values.push(stream.current());
+ } else {
+ stream.match(/^[0-9]*(\.[0-9]*)?[a-z%]*/, true);
+ values.push(stream.current());
+ }
+
+ stream.start = stream.pos;
+ }
+
+ return _.map(_.compact(values), normalizeValue);
+ },
+
+ /**
+ * Extracts values from abbreviation
+ * @param {String} abbr
+ * @returns {Object} Object containing array of values and clean
+ * abbreviation name
+ */
+ extractValues: function(abbr) {
+ // search for value start
+ var abbrValues = this.findValuesInAbbreviation(abbr);
+ if (!abbrValues) {
+ return {
+ property: abbr,
+ values: null
+ };
+ }
+
+ return {
+ property: abbr.substring(0, abbr.length - abbrValues.length).replace(/-$/, ''),
+ values: this.parseValues(abbrValues)
+ };
+ },
+
+ /**
+ * Normalizes value, defined in abbreviation.
+ * @param {String} value
+ * @param {String} property
+ * @returns {String}
+ */
+ normalizeValue: function(value, property) {
+ property = (property || '').toLowerCase();
+ var unitlessProps = prefs.getArray('css.unitlessProperties');
+ return value.replace(/^(\-?[0-9\.]+)([a-z]*)$/, function(str, val, unit) {
+ if (!unit && (val == '0' || _.include(unitlessProps, property)))
+ return val;
+
+ if (!unit)
+ return val.replace(/\.$/, '') + prefs.get(~val.indexOf('.') ? 'css.floatUnit' : 'css.intUnit');
+
+ return val + getUnit(unit);
+ });
+ },
+
+ /**
+ * Expands abbreviation into a snippet
+ * @param {String} abbr Abbreviation name to expand
+ * @param {String} value Abbreviation value
+ * @param {String} syntax Currect syntax or dialect. Default is 'css'
+ * @returns {Object} Array of CSS properties and values or predefined
+ * snippet (string or element)
+ */
+ expand: function(abbr, value, syntax) {
+ syntax = syntax || 'css';
+ var resources = require('resources');
+ var autoInsertPrefixes = prefs.get('css.autoInsertVendorPrefixes');
+
+ // check if snippet should be transformed to !important
+ var isImportant;
+ if (isImportant = /^(.+)\!$/.test(abbr)) {
+ abbr = RegExp.$1;
+ }
+
+ // check if we have abbreviated resource
+ var snippet = resources.findSnippet(syntax, abbr);
+ if (snippet && !autoInsertPrefixes) {
+ return transformSnippet(snippet, isImportant, syntax);
+ }
+
+ // no abbreviated resource, parse abbreviation
+ var prefixData = this.extractPrefixes(abbr);
+ var valuesData = this.extractValues(prefixData.property);
+ var abbrData = _.extend(prefixData, valuesData);
+
+ snippet = resources.findSnippet(syntax, abbrData.property);
+
+ // fallback to some old snippets like m:a
+// if (!snippet && ~abbrData.property.indexOf(':')) {
+// var parts = abbrData.property.split(':');
+// var propertyName = parts.shift();
+// snippet = resources.findSnippet(syntax, propertyName) || propertyName;
+// abbrData.values = this.parseValues(parts.join(':'));
+// }
+
+ if (!snippet && prefs.get('css.fuzzySearch')) {
+ // let’s try fuzzy search
+ snippet = resources.fuzzyFindSnippet(syntax, abbrData.property, parseFloat(prefs.get('css.fuzzySearchMinScore')));
+ }
+
+ if (!snippet) {
+ snippet = abbrData.property + ':' + defaultValue;
+ } else if (!_.isString(snippet)) {
+ snippet = snippet.data;
+ }
+
+ if (!isSingleProperty(snippet)) {
+ return snippet;
+ }
+
+ var snippetObj = splitSnippet(snippet);
+ var result = [];
+ if (!value && abbrData.values) {
+ value = _.map(abbrData.values, function(val) {
+ return this.normalizeValue(val, snippetObj.name);
+ }, this).join(' ') + ';';
+ }
+
+ snippetObj.value = value || snippetObj.value;
+
+ var prefixes = abbrData.prefixes == 'all' || (!abbrData.prefixes && autoInsertPrefixes)
+ ? findPrefixes(snippetObj.name, autoInsertPrefixes && abbrData.prefixes != 'all')
+ : abbrData.prefixes;
+
+ _.each(prefixes, function(p) {
+ if (p in vendorPrefixes) {
+ result.push(transformSnippet(
+ vendorPrefixes[p].transformName(snippetObj.name)
+ + ':' + snippetObj.value,
+ isImportant, syntax));
+
+ }
+ });
+
+ // put the original property
+ result.push(transformSnippet(snippetObj.name + ':' + snippetObj.value, isImportant, syntax));
+
+ return result;
+ },
+
+ /**
+ * Same as expand
method but transforms output into
+ * Emmet snippet
+ * @param {String} abbr
+ * @param {String} syntax
+ * @returns {String}
+ */
+ expandToSnippet: function(abbr, syntax) {
+ var snippet = this.expand(abbr, null, syntax);
+ if (_.isArray(snippet)) {
+ return snippet.join('\n');
+ }
+
+ if (!_.isString(snippet))
+ return snippet.data;
+
+ return String(snippet);
+ },
+
+ getSyntaxPreference: getSyntaxPreference,
+ transformSnippet: transformSnippet
+ };
+});
+/**
+ * 'Expand Abbreviation' handler that parses gradient definition from under
+ * cursor and updates CSS rule with vendor-prefixed values.
+ *
+ * @memberOf __cssGradientHandlerDefine
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.define('cssGradient', function(require, _) {
+ var defaultLinearDirections = ['top', 'to bottom', '0deg'];
+ /** Back-reference to current module */
+ var module = null;
+
+ var cssSyntaxes = ['css', 'less', 'sass', 'scss', 'stylus', 'styl'];
+
+ var reDeg = /\d+deg/i;
+ var reKeyword = /top|bottom|left|right/i;
+
+ // XXX define preferences
+ /** @type preferences */
+ var prefs = require('preferences');
+ prefs.define('css.gradient.prefixes', 'webkit, moz, o',
+ 'A comma-separated list of vendor-prefixes for which values should '
+ + 'be generated.');
+
+ prefs.define('css.gradient.oldWebkit', true,
+ 'Generate gradient definition for old Webkit implementations');
+
+ prefs.define('css.gradient.omitDefaultDirection', true,
+ 'Do not output default direction definition in generated gradients.');
+
+ prefs.define('css.gradient.defaultProperty', 'background-image',
+ 'When gradient expanded outside CSS value context, it will produce '
+ + 'properties with this name.');
+
+ prefs.define('css.gradient.fallback', false,
+ 'With this option enabled, CSS gradient generator will produce '
+ + 'background-color
property with gradient first color '
+ + 'as fallback for old browsers.');
+
+ function normalizeSpace(str) {
+ return require('utils').trim(str).replace(/\s+/g, ' ');
+ }
+
+ /**
+ * Parses linear gradient definition
+ * @param {String}
+ */
+ function parseLinearGradient(gradient) {
+ var direction = defaultLinearDirections[0];
+
+ // extract tokens
+ /** @type StringStream */
+ var stream = require('stringStream').create(require('utils').trim(gradient));
+ var colorStops = [], ch;
+ while (ch = stream.next()) {
+ if (stream.peek() == ',') {
+ colorStops.push(stream.current());
+ stream.next();
+ stream.eatSpace();
+ stream.start = stream.pos;
+ } else if (ch == '(') { // color definition, like 'rgb(0,0,0)'
+ stream.skipTo(')');
+ }
+ }
+
+ // add last token
+ colorStops.push(stream.current());
+ colorStops = _.compact(_.map(colorStops, normalizeSpace));
+
+ if (!colorStops.length)
+ return null;
+
+ // let's see if the first color stop is actually a direction
+ if (reDeg.test(colorStops[0]) || reKeyword.test(colorStops[0])) {
+ direction = colorStops.shift();
+ }
+
+ return {
+ type: 'linear',
+ direction: direction,
+ colorStops: _.map(colorStops, parseColorStop)
+ };
+ }
+
+ /**
+ * Parses color stop definition
+ * @param {String} colorStop
+ * @returns {Object}
+ */
+ function parseColorStop(colorStop) {
+ colorStop = normalizeSpace(colorStop);
+
+ // find color declaration
+ // first, try complex color declaration, like rgb(0,0,0)
+ var color = null;
+ colorStop = colorStop.replace(/^(\w+\(.+?\))\s*/, function(str, c) {
+ color = c;
+ return '';
+ });
+
+ if (!color) {
+ // try simple declaration, like yellow, #fco, #ffffff, etc.
+ var parts = colorStop.split(' ');
+ color = parts[0];
+ colorStop = parts[1] || '';
+ }
+
+ var result = {
+ color: color
+ };
+
+ if (colorStop) {
+ // there's position in color stop definition
+ colorStop.replace(/^(\-?[\d\.]+)([a-z%]+)?$/, function(str, pos, unit) {
+ result.position = pos;
+ if (~pos.indexOf('.')) {
+ unit = '';
+ } else if (!unit) {
+ unit = '%';
+ }
+
+ if (unit)
+ result.unit = unit;
+ });
+ }
+
+ return result;
+ }
+
+ /**
+ * Fills-out implied positions in color-stops. This function is useful for
+ * old Webkit gradient definitions
+ */
+ function fillImpliedPositions(colorStops) {
+ var from = 0;
+
+ _.each(colorStops, function(cs, i) {
+ // make sure that first and last positions are defined
+ if (!i)
+ return cs.position = cs.position || 0;
+
+ if (i == colorStops.length - 1 && !('position' in cs))
+ cs.position = 1;
+
+ if ('position' in cs) {
+ var start = colorStops[from].position || 0;
+ var step = (cs.position - start) / (i - from);
+ _.each(colorStops.slice(from, i), function(cs2, j) {
+ cs2.position = start + step * j;
+ });
+
+ from = i;
+ }
+ });
+ }
+
+ /**
+ * Returns textual version of direction expressed in degrees
+ * @param {String} direction
+ * @returns {String}
+ */
+ function textualDirection(direction) {
+ var angle = parseFloat(direction);
+
+ if(!_.isNaN(angle)) {
+ switch(angle % 360) {
+ case 0: return 'left';
+ case 90: return 'bottom';
+ case 180: return 'right';
+ case 240: return 'top';
+ }
+ }
+
+ return direction;
+ }
+
+ /**
+ * Creates direction definition for old Webkit gradients
+ * @param {String} direction
+ * @returns {String}
+ */
+ function oldWebkitDirection(direction) {
+ direction = textualDirection(direction);
+
+ if(reDeg.test(direction))
+ throw "The direction is an angle that can’t be converted.";
+
+ var v = function(pos) {
+ return ~direction.indexOf(pos) ? '100%' : '0';
+ };
+
+ return v('right') + ' ' + v('bottom') + ', ' + v('left') + ' ' + v('top');
+ }
+
+ function getPrefixedNames(name) {
+ var prefixes = prefs.getArray('css.gradient.prefixes');
+ var names = _.map(prefixes, function(p) {
+ return '-' + p + '-' + name;
+ });
+ names.push(name);
+
+ return names;
+ }
+
+ /**
+ * Returns list of CSS properties with gradient
+ * @param {Object} gradient
+ * @param {String} propertyName Original CSS property name
+ * @returns {Array}
+ */
+ function getPropertiesForGradient(gradient, propertyName) {
+ var props = [];
+ var css = require('cssResolver');
+
+ if (prefs.get('css.gradient.fallback') && ~propertyName.toLowerCase().indexOf('background')) {
+ props.push({
+ name: 'background-color',
+ value: '${1:' + gradient.colorStops[0].color + '}'
+ });
+ }
+
+ _.each(prefs.getArray('css.gradient.prefixes'), function(prefix) {
+ var name = css.prefixed(propertyName, prefix);
+ if (prefix == 'webkit' && prefs.get('css.gradient.oldWebkit')) {
+ try {
+ props.push({
+ name: name,
+ value: module.oldWebkitLinearGradient(gradient)
+ });
+ } catch(e) {}
+ }
+
+ props.push({
+ name: name,
+ value: module.toString(gradient, prefix)
+ });
+ });
+
+ return props.sort(function(a, b) {
+ return b.name.length - a.name.length;
+ });
+ }
+
+ /**
+ * Pastes gradient definition into CSS rule with correct vendor-prefixes
+ * @param {EditElement} property Matched CSS property
+ * @param {Object} gradient Parsed gradient
+ * @param {Range} valueRange If passed, only this range within property
+ * value will be replaced with gradient. Otherwise, full value will be
+ * replaced
+ */
+ function pasteGradient(property, gradient, valueRange) {
+ var rule = property.parent;
+ var utils = require('utils');
+
+ // first, remove all properties within CSS rule with the same name and
+ // gradient definition
+ _.each(rule.getAll(getPrefixedNames(property.name())), function(item) {
+ if (item != property && /gradient/i.test(item.value())) {
+ rule.remove(item);
+ }
+ });
+
+ var value = property.value();
+ if (!valueRange)
+ valueRange = require('range').create(0, property.value());
+
+ var val = function(v) {
+ return utils.replaceSubstring(value, v, valueRange);
+ };
+
+ // put vanilla-clean gradient definition into current rule
+ property.value(val(module.toString(gradient)) + '${2}');
+
+ // create list of properties to insert
+ var propsToInsert = getPropertiesForGradient(gradient, property.name());
+
+ // put vendor-prefixed definitions before current rule
+ _.each(propsToInsert, function(prop) {
+ rule.add(prop.name, prop.value, rule.indexOf(property));
+ });
+ }
+
+ /**
+ * Search for gradient definition inside CSS property value
+ */
+ function findGradient(cssProp) {
+ var value = cssProp.value();
+ var gradient = null;
+ var matchedPart = _.find(cssProp.valueParts(), function(part) {
+ return gradient = module.parse(part.substring(value));
+ });
+
+ if (matchedPart && gradient) {
+ return {
+ gradient: gradient,
+ valueRange: matchedPart
+ };
+ }
+
+ return null;
+ }
+
+ /**
+ * Tries to expand gradient outside CSS value
+ * @param {IEmmetEditor} editor
+ * @param {String} syntax
+ */
+ function expandGradientOutsideValue(editor, syntax) {
+ var propertyName = prefs.get('css.gradient.defaultProperty');
+
+ if (!propertyName)
+ return false;
+
+ // assuming that gradient definition is written on new line,
+ // do a simplified parsing
+ var content = String(editor.getContent());
+ /** @type Range */
+ var lineRange = require('range').create(editor.getCurrentLineRange());
+
+ // get line content and adjust range with padding
+ var line = lineRange.substring(content)
+ .replace(/^\s+/, function(pad) {
+ lineRange.start += pad.length;
+ return '';
+ })
+ .replace(/\s+$/, function(pad) {
+ lineRange.end -= pad.length;
+ return '';
+ });
+
+ var css = require('cssResolver');
+ var gradient = module.parse(line);
+ if (gradient) {
+ var props = getPropertiesForGradient(gradient, propertyName);
+ props.push({
+ name: propertyName,
+ value: module.toString(gradient) + '${2}'
+ });
+
+ var sep = css.getSyntaxPreference('valueSeparator', syntax);
+ var end = css.getSyntaxPreference('propertyEnd', syntax);
+ props = _.map(props, function(item) {
+ return item.name + sep + item.value + end;
+ });
+
+ editor.replaceContent(props.join('\n'), lineRange.start, lineRange.end);
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Search for gradient definition inside CSS value under cursor
+ * @param {String} content
+ * @param {Number} pos
+ * @returns {Object}
+ */
+ function findGradientFromPosition(content, pos) {
+ var cssProp = null;
+ /** @type EditContainer */
+ var cssRule = require('cssEditTree').parseFromPosition(content, pos, true);
+
+ if (cssRule) {
+ cssProp = cssRule.itemFromPosition(pos, true);
+ if (!cssProp) {
+ // in case user just started writing CSS property
+ // and didn't include semicolon–try another approach
+ cssProp = _.find(cssRule.list(), function(elem) {
+ return elem.range(true).end == pos;
+ });
+ }
+ }
+
+ return {
+ rule: cssRule,
+ property: cssProp
+ };
+ }
+
+ // XXX register expand abbreviation handler
+ /**
+ * @param {IEmmetEditor} editor
+ * @param {String} syntax
+ * @param {String} profile
+ */
+ require('expandAbbreviation').addHandler(function(editor, syntax, profile) {
+ var info = require('editorUtils').outputInfo(editor, syntax, profile);
+ if (!_.include(cssSyntaxes, info.syntax))
+ return false;
+
+ // let's see if we are expanding gradient definition
+ var caret = editor.getCaretPos();
+ var content = info.content;
+ var css = findGradientFromPosition(content, caret);
+
+ if (css.property) {
+ // make sure that caret is inside property value with gradient
+ // definition
+ var g = findGradient(css.property);
+ if (g) {
+ var ruleStart = css.rule.options.offset || 0;
+ var ruleEnd = ruleStart + css.rule.toString().length;
+
+ // Handle special case:
+ // user wrote gradient definition between existing CSS
+ // properties and did not finished it with semicolon.
+ // In this case, we have semicolon right after gradient
+ // definition and re-parse rule again
+ if (/[\n\r]/.test(css.property.value())) {
+ // insert semicolon at the end of gradient definition
+ var insertPos = css.property.valueRange(true).start + g.valueRange.end;
+ content = require('utils').replaceSubstring(content, ';', insertPos);
+ var newCss = findGradientFromPosition(content, caret);
+ if (newCss.property) {
+ g = findGradient(newCss.property);
+ css = newCss;
+ }
+ }
+
+ // make sure current property has terminating semicolon
+ css.property.end(';');
+
+ pasteGradient(css.property, g.gradient, g.valueRange);
+ editor.replaceContent(css.rule.toString(), ruleStart, ruleEnd, true);
+ return true;
+ }
+ }
+
+ return expandGradientOutsideValue(editor, syntax);
+ });
+
+ // XXX register "Reflect CSS Value" action delegate
+ /**
+ * @param {EditElement} property
+ */
+ require('reflectCSSValue').addHandler(function(property) {
+ var utils = require('utils');
+
+ var g = findGradient(property);
+ if (!g)
+ return false;
+
+ var value = property.value();
+ var val = function(v) {
+ return utils.replaceSubstring(value, v, g.valueRange);
+ };
+
+ // reflect value for properties with the same name
+ _.each(property.parent.getAll(getPrefixedNames(property.name())), function(prop) {
+ if (prop === property)
+ return;
+
+ // check if property value starts with gradient definition
+ var m = prop.value().match(/^\s*(\-([a-z]+)\-)?linear\-gradient/);
+ if (m) {
+ prop.value(val(module.toString(g.gradient, m[2] || '')));
+ } else if (m = prop.value().match(/\s*\-webkit\-gradient/)) {
+ // old webkit gradient definition
+ prop.value(val(module.oldWebkitLinearGradient(g.gradient)));
+ }
+ });
+
+ return true;
+ });
+
+ return module = {
+ /**
+ * Parses gradient definition
+ * @param {String} gradient
+ * @returns {Object}
+ */
+ parse: function(gradient) {
+ var result = null;
+ require('utils').trim(gradient).replace(/^([\w\-]+)\((.+?)\)$/, function(str, type, definition) {
+ // remove vendor prefix
+ type = type.toLowerCase().replace(/^\-[a-z]+\-/, '');
+ if (type == 'linear-gradient' || type == 'lg') {
+ result = parseLinearGradient(definition);
+ return '';
+ }
+
+ return str;
+ });
+
+ return result;
+ },
+
+ /**
+ * Produces linear gradient definition used in early Webkit
+ * implementations
+ * @param {Object} gradient Parsed gradient
+ * @returns {String}
+ */
+ oldWebkitLinearGradient: function(gradient) {
+ if (_.isString(gradient))
+ gradient = this.parse(gradient);
+
+ if (!gradient)
+ return null;
+
+ var colorStops = _.map(gradient.colorStops, _.clone);
+
+ // normalize color-stops position
+ _.each(colorStops, function(cs) {
+ if (!('position' in cs)) // implied position
+ return;
+
+ if (~cs.position.indexOf('.') || cs.unit == '%') {
+ cs.position = parseFloat(cs.position) / (cs.unit == '%' ? 100 : 1);
+ } else {
+ throw "Can't convert color stop '" + (cs.position + (cs.unit || '')) + "'";
+ }
+ });
+
+ fillImpliedPositions(colorStops);
+
+ // transform color-stops into string representation
+ colorStops = _.map(colorStops, function(cs, i) {
+ if (!cs.position && !i)
+ return 'from(' + cs.color + ')';
+
+ if (cs.position == 1 && i == colorStops.length - 1)
+ return 'to(' + cs.color + ')';
+
+ return 'color-stop(' + (cs.position.toFixed(2).replace(/\.?0+$/, '')) + ', ' + cs.color + ')';
+ });
+
+ return '-webkit-gradient(linear, '
+ + oldWebkitDirection(gradient.direction)
+ + ', '
+ + colorStops.join(', ')
+ + ')';
+ },
+
+ /**
+ * Returns string representation of parsed gradient
+ * @param {Object} gradient Parsed gradient
+ * @param {String} prefix Vendor prefix
+ * @returns {String}
+ */
+ toString: function(gradient, prefix) {
+ if (gradient.type == 'linear') {
+ var fn = (prefix ? '-' + prefix + '-' : '') + 'linear-gradient';
+
+ // transform color-stops
+ var colorStops = _.map(gradient.colorStops, function(cs) {
+ return cs.color + ('position' in cs
+ ? ' ' + cs.position + (cs.unit || '')
+ : '');
+ });
+
+ if (gradient.direction
+ && (!prefs.get('css.gradient.omitDefaultDirection')
+ || !_.include(defaultLinearDirections, gradient.direction))) {
+ colorStops.unshift(gradient.direction);
+ }
+
+ return fn + '(' + colorStops.join(', ') + ')';
+ }
+ }
+ };
+});/**
+ * Module adds support for generators: a regexp-based abbreviation resolver
+ * that can produce custom output.
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _) {
+ /** @type HandlerList */
+ var generators = require('handlerList').create();
+ var resources = require('resources');
+
+ _.extend(resources, {
+ /**
+ * Add generator. A generator function fn
will be called
+ * only if current abbreviation matches regexp
regular
+ * expression and this function should return null
if
+ * abbreviation cannot be resolved
+ * @param {RegExp} regexp Regular expression for abbreviation element name
+ * @param {Function} fn Resolver function
+ * @param {Object} options Options list as described in
+ * {@link HandlerList#add()} method
+ */
+ addGenerator: function(regexp, fn, options) {
+ if (_.isString(regexp))
+ regexp = new RegExp(regexp);
+
+ generators.add(function(node, syntax) {
+ var m;
+ if ((m = regexp.exec(node.name()))) {
+ return fn(m, node, syntax);
+ }
+
+ return null;
+ }, options);
+ }
+ });
+
+ resources.addResolver(function(node, syntax) {
+ return generators.exec(null, _.toArray(arguments));
+ });
+});/**
+ * Module for resolving tag names: returns best matched tag name for child
+ * element based on passed parent's tag name. Also provides utility function
+ * for element type detection (inline, block-level, empty)
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.define('tagName', function(require, _) {
+ var elementTypes = {
+ empty: 'area,base,basefont,br,col,frame,hr,img,input,isindex,link,meta,param,embed,keygen,command'.split(','),
+ blockLevel: 'address,applet,blockquote,button,center,dd,del,dir,div,dl,dt,fieldset,form,frameset,hr,iframe,ins,isindex,li,link,map,menu,noframes,noscript,object,ol,p,pre,script,table,tbody,td,tfoot,th,thead,tr,ul,h1,h2,h3,h4,h5,h6'.split(','),
+ inlineLevel: '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(',')
+ };
+
+ var 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'
+ };
+
+ return {
+ /**
+ * Returns best matched child element name for passed parent's
+ * tag name
+ * @param {String} name
+ * @returns {String}
+ * @memberOf tagName
+ */
+ resolve: function(name) {
+ name = (name || '').toLowerCase();
+
+ if (name in elementMap)
+ return this.getMapping(name);
+
+ if (this.isInlineLevel(name))
+ return 'span';
+
+ return 'div';
+ },
+
+ /**
+ * Returns mapped child element name for passed parent's name
+ * @param {String} name
+ * @returns {String}
+ */
+ getMapping: function(name) {
+ return elementMap[name.toLowerCase()];
+ },
+
+ /**
+ * Check if passed element name belongs to inline-level element
+ * @param {String} name
+ * @returns {Boolean}
+ */
+ isInlineLevel: function(name) {
+ return this.isTypeOf(name, 'inlineLevel');
+ },
+
+ /**
+ * Check if passed element belongs to block-level element.
+ * For better matching of unknown elements (for XML, for example),
+ * you should use !this.isInlineLevel(name)
+ * @returns {Boolean}
+ */
+ isBlockLevel: function(name) {
+ return this.isTypeOf(name, 'blockLevel');
+ },
+
+ /**
+ * Check if passed element is void (i.e. should not have closing tag).
+ * @returns {Boolean}
+ */
+ isEmptyElement: function(name) {
+ return this.isTypeOf(name, 'empty');
+ },
+
+ /**
+ * Generic function for testing if element name belongs to specified
+ * elements collection
+ * @param {String} name Element name
+ * @param {String} type Collection name
+ * @returns {Boolean}
+ */
+ isTypeOf: function(name, type) {
+ return _.include(elementTypes[type], name);
+ },
+
+ /**
+ * Adds new parent–child mapping
+ * @param {String} parent
+ * @param {String} child
+ */
+ addMapping: function(parent, child) {
+ elementMap[parent] = child;
+ },
+
+ /**
+ * Removes parent-child mapping
+ */
+ removeMapping: function(parent) {
+ if (parent in elementMap)
+ delete elementMap[parent];
+ },
+
+ /**
+ * Adds new element into collection
+ * @param {String} name Element name
+ * @param {String} collection Collection name
+ */
+ addElementToCollection: function(name, collection) {
+ if (!elementTypes[collection])
+ elementTypes[collection] = [];
+
+ var col = this.getCollection(collection);
+ if (!_.include(col, name))
+ col.push(name);
+ },
+
+ /**
+ * Removes element name from specified collection
+ * @param {String} name Element name
+ * @param {String} collection Collection name
+ * @returns
+ */
+ removeElementFromCollection: function(name, collection) {
+ if (collection in elementTypes) {
+ elementTypes[collection] = _.without(this.getCollection(collection), name);
+ }
+ },
+
+ /**
+ * Returns elements name collection
+ * @param {String} name Collection name
+ * @returns {Array}
+ */
+ getCollection: function(name) {
+ return elementTypes[name];
+ }
+ };
+});/**
+ * Filter for aiding of writing elements with complex class names as described
+ * in Yandex's BEM (Block, Element, Modifier) methodology. This filter will
+ * automatically inherit block and element names from parent elements and insert
+ * them into child element classes
+ * @memberOf __bemFilterDefine
+ * @constructor
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _) {
+ var prefs = require('preferences');
+ prefs.define('bem.elementSeparator', '__', 'Class name’s element separator.');
+ prefs.define('bem.modifierSeparator', '_', 'Class name’s modifier separator.');
+ prefs.define('bem.shortElementPrefix', '-',
+ 'Symbol for describing short “block-element” notation. Class names '
+ + 'prefixed with this symbol will be treated as element name for parent‘s '
+ + 'block name. Each symbol instance traverses one level up in parsed '
+ + 'tree for block name lookup. Empty value will disable short notation.');
+
+ var shouldRunHtmlFilter = false;
+
+ function getSeparators() {
+ return {
+ element: prefs.get('bem.elementSeparator'),
+ modifier: prefs.get('bem.modifierSeparator')
+ };
+ }
+
+ /**
+ * @param {AbbreviationNode} item
+ */
+ function bemParse(item) {
+ if (require('abbreviationUtils').isSnippet(item))
+ return item;
+
+ // save BEM stuff in cache for faster lookups
+ item.__bem = {
+ block: '',
+ element: '',
+ modifier: ''
+ };
+
+ var classNames = normalizeClassName(item.attribute('class')).split(' ');
+
+ // guess best match for block name
+ var reBlockName = /^[a-z]\-/i;
+ item.__bem.block = _.find(classNames, function(name) {
+ return reBlockName.test(name);
+ });
+
+ // guessing doesn't worked, pick first class name as block name
+ if (!item.__bem.block) {
+ reBlockName = /^[a-z]/i;
+ item.__bem.block = _.find(classNames, function(name) {
+ return reBlockName.test(name);
+ }) || '';
+ }
+
+ classNames = _.chain(classNames)
+ .map(function(name) {return processClassName(name, item);})
+ .flatten()
+ .uniq()
+ .value()
+ .join(' ');
+
+ if (classNames)
+ item.attribute('class', classNames);
+
+ return item;
+ }
+
+ /**
+ * @param {String} className
+ * @returns {String}
+ */
+ function normalizeClassName(className) {
+ var utils = require('utils');
+ className = (' ' + (className || '') + ' ').replace(/\s+/g, ' ');
+
+ var shortSymbol = prefs.get('bem.shortElementPrefix');
+ if (shortSymbol) {
+ var re = new RegExp('\\s(' + utils.escapeForRegexp(shortSymbol) + '+)', 'g');
+ className = className.replace(re, function(str, p1) {
+ return ' ' + utils.repeatString(getSeparators().element, p1.length);
+ });
+ }
+
+ return utils.trim(className);
+ }
+
+ /**
+ * Processes class name
+ * @param {String} name Class name item to process
+ * @param {AbbreviationNode} item Host node for provided class name
+ * @returns Processed class name. May return Array
of
+ * class names
+ */
+ function processClassName(name, item) {
+ name = transformClassName(name, item, 'element');
+ name = transformClassName(name, item, 'modifier');
+
+ // expand class name
+ // possible values:
+ // * block__element
+ // * block__element_modifier
+ // * block__element_modifier1_modifier2
+ // * block_modifier
+ var block = '', element = '', modifier = '';
+ var separators = getSeparators();
+ if (~name.indexOf(separators.element)) {
+ var blockElem = name.split(separators.element);
+ var elemModifiers = blockElem[1].split(separators.modifier);
+
+ block = blockElem[0];
+ element = elemModifiers.shift();
+ modifier = elemModifiers.join(separators.modifier);
+ } else if (~name.indexOf(separators.modifier)) {
+ var blockModifiers = name.split(separators.modifier);
+
+ block = blockModifiers.shift();
+ modifier = blockModifiers.join(separators.modifier);
+ }
+
+ if (block || element || modifier) {
+ if (!block) {
+ block = item.__bem.block;
+ }
+
+ // inherit parent bem element, if exists
+// if (item.parent && item.parent.__bem && item.parent.__bem.element)
+// element = item.parent.__bem.element + separators.element + element;
+
+ // produce multiple classes
+ var prefix = block;
+ var result = [];
+
+ if (element) {
+ prefix += separators.element + element;
+ result.push(prefix);
+ } else {
+ result.push(prefix);
+ }
+
+ if (modifier) {
+ result.push(prefix + separators.modifier + modifier);
+ }
+
+ item.__bem.block = block;
+ item.__bem.element = element;
+ item.__bem.modifier = modifier;
+
+ return result;
+ }
+
+ // ...otherwise, return processed or original class name
+ return name;
+ }
+
+ /**
+ * Low-level function to transform user-typed class name into full BEM class
+ * @param {String} name Class name item to process
+ * @param {AbbreviationNode} item Host node for provided class name
+ * @param {String} entityType Type of entity to be tried to transform
+ * ('element' or 'modifier')
+ * @returns {String} Processed class name or original one if it can't be
+ * transformed
+ */
+ function transformClassName(name, item, entityType) {
+ var separators = getSeparators();
+ var reSep = new RegExp('^(' + separators[entityType] + ')+', 'g');
+ if (reSep.test(name)) {
+ var depth = 0; // parent lookup depth
+ var cleanName = name.replace(reSep, function(str, p1) {
+ depth = str.length / separators[entityType].length;
+ return '';
+ });
+
+ // find donor element
+ var donor = item;
+ while (donor.parent && depth--) {
+ donor = donor.parent;
+ }
+
+ if (!donor || !donor.__bem)
+ donor = item;
+
+ if (donor && donor.__bem) {
+ var prefix = donor.__bem.block;
+
+ // decide if we should inherit element name
+// if (entityType == 'element') {
+// var curElem = cleanName.split(separators.modifier, 1)[0];
+// if (donor.__bem.element && donor.__bem.element != curElem)
+// prefix += separators.element + donor.__bem.element;
+// }
+
+ if (entityType == 'modifier' && donor.__bem.element)
+ prefix += separators.element + donor.__bem.element;
+
+ return prefix + separators[entityType] + cleanName;
+ }
+ }
+
+ return name;
+ }
+
+ /**
+ * Recursive function for processing tags, which extends class names
+ * according to BEM specs: http://bem.github.com/bem-method/pages/beginning/beginning.ru.html
+ * comment
filter is applied. This definition '
+ + 'is an ERB-style template passed to _.template()
'
+ + 'function (see Underscore.js docs for details). In template context, '
+ + 'the following properties and functions are availabe:\n'
+ + 'attr(name, before, after)
– a function that outputs'
+ + 'specified attribute value concatenated with before
'
+ + 'and after
strings. If attribute doesn\'t exists, the '
+ + 'empty string will be returned.node
– current node (instance of AbbreviationNode
)name
– name of current tagpadding
– current string padding, can be used '
+ + 'for formattingcomment
filter is applied. '
+ + 'For more info, read description of filter.commentAfter
'
+ + 'property');
+
+ prefs.define('filter.commentTrigger', 'id, class',
+ 'A comma-separated list of attribute names that should exist in abbreviatoin '
+ + 'where comment should be added. If you wish to add comment for '
+ + 'every element, set this option to *
');
+
+ /**
+ * Add comments to tag
+ * @param {AbbreviationNode} node
+ */
+ function addComments(node, templateBefore, templateAfter) {
+ var utils = require('utils');
+
+ // check if comments should be added
+ var trigger = prefs.get('filter.commentTrigger');
+ if (trigger != '*') {
+ var shouldAdd = _.find(trigger.split(','), function(name) {
+ return !!node.attribute(utils.trim(name));
+ });
+ if (!shouldAdd) return;
+ }
+
+ var ctx = {
+ node: node,
+ name: node.name(),
+ padding: node.parent ? node.parent.padding : '',
+ attr: function(name, before, after) {
+ var attr = node.attribute(name);
+ if (attr) {
+ return (before || '') + attr + (after || '');
+ }
+
+ return '';
+ }
+ };
+
+ var nodeBefore = utils.normalizeNewline(templateBefore ? templateBefore(ctx) : '');
+ var nodeAfter = utils.normalizeNewline(templateAfter ? templateAfter(ctx) : '');
+
+ node.start = node.start.replace(/, nodeBefore + '<');
+ node.end = node.end.replace(/>/, '>' + nodeAfter);
+ }
+
+ function process(tree, before, after) {
+ var abbrUtils = require('abbreviationUtils');
+ _.each(tree.children, function(item) {
+ if (abbrUtils.isBlock(item))
+ addComments(item, before, after);
+
+ process(item, before, after);
+ });
+
+ return tree;
+ }
+
+ require('filters').add('c', function(tree) {
+ var templateBefore = _.template(prefs.get('filter.commentBefore'));
+ var templateAfter = _.template(prefs.get('filter.commentAfter'));
+
+ return process(tree, templateBefore, templateAfter);
+ });
+});
+/**
+ * Filter for escaping unsafe XML characters: <, >, &
+ * @author Sergey Chikuyonok (serge.che@gmail.com)
+ * @link http://chikuyonok.ru
+ */
emmet.exec(function(require, _) {
+ var charMap = {
+ '<': '<',
+ '>': '>',
+ '&': '&'
+ };
+
+ function escapeChars(str) {
+ return str.replace(/([<>&])/g, function(str, p1){
+ return charMap[p1];
+ });
+ }
+
+ require('filters').add('e', function process(tree) {
+ _.each(tree.children, function(item) {
+ item.start = escapeChars(item.start);
+ item.end = escapeChars(item.end);
+ item.content = escapeChars(item.content);
+ process(item);
+ });
+
+ return tree;
+ });
+});/**
+ * Generic formatting filter: creates proper indentation for each tree node,
+ * placing "%s" placeholder where the actual output should be. You can use
+ * this filter to preformat tree and then replace %s placeholder to whatever you
+ * need. This filter should't be called directly from editor as a part
+ * of abbreviation.
+ * @author Sergey Chikuyonok (serge.che@gmail.com)
+ * @link http://chikuyonok.ru
+ * @constructor
+ * @memberOf __formatFilterDefine
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _){
+ var placeholder = '%s';
+
+ function getIndentation() {
+ return require('resources').getVariable('indentation');
+ }
+
+ /**
+ * Test if passed node has block-level sibling element
+ * @param {AbbreviationNode} item
+ * @return {Boolean}
+ */
+ function hasBlockSibling(item) {
+ return item.parent && require('abbreviationUtils').hasBlockChildren(item.parent);
+ }
+
+ /**
+ * Test if passed item is very first child in parsed tree
+ * @param {AbbreviationNode} item
+ */
+ function isVeryFirstChild(item) {
+ return item.parent && !item.parent.parent && !item.index();
+ }
+
+ /**
+ * Check if a newline should be added before element
+ * @param {AbbreviationNode} node
+ * @param {OutputProfile} profile
+ * @return {Boolean}
+ */
+ function shouldAddLineBreak(node, profile) {
+ var abbrUtils = require('abbreviationUtils');
+ if (profile.tag_nl === true || abbrUtils.isBlock(node))
+ return true;
+
+ if (!node.parent || !profile.inline_break)
+ return false;
+
+ // check if there are required amount of adjacent inline element
+ return shouldFormatInline(node.parent, profile);
+}
+
+ /**
+ * Need to add newline because item
has too many inline children
+ * @param {AbbreviationNode} node
+ * @param {OutputProfile} profile
+ */
+ function shouldBreakChild(node, profile) {
+ // we need to test only one child element, because
+ // hasBlockChildren() method will do the rest
+ return node.children.length && shouldAddLineBreak(node.children[0], profile);
+ }
+
+ function shouldFormatInline(node, profile) {
+ var nodeCount = 0;
+ var abbrUtils = require('abbreviationUtils');
+ return !!_.find(node.children, function(child) {
+ if (child.isTextNode() || !abbrUtils.isInline(child))
+ nodeCount = 0;
+ else if (abbrUtils.isInline(child))
+ nodeCount++;
+
+ if (nodeCount >= profile.inline_break)
+ return true;
+ });
+ }
+
+ function isRoot(item) {
+ return !item.parent;
+ }
+
+ /**
+ * Processes element with matched resource of type snippet
+ * @param {AbbreviationNode} item
+ * @param {OutputProfile} profile
+ * @param {Number} level Depth level
+ */
+ function processSnippet(item, profile, level) {
+ item.start = item.end = '';
+ if (!isVeryFirstChild(item) && profile.tag_nl !== false && shouldAddLineBreak(item, profile)) {
+ // check if we’re not inside inline element
+ if (isRoot(item.parent) || !require('abbreviationUtils').isInline(item.parent)) {
+ item.start = require('utils').getNewline() + item.start;
+ }
+ }
+
+ return item;
+ }
+
+ /**
+ * Check if we should add line breaks inside inline element
+ * @param {AbbreviationNode} node
+ * @param {OutputProfile} profile
+ * @return {Boolean}
+ */
+ function shouldBreakInsideInline(node, profile) {
+ var abbrUtils = require('abbreviationUtils');
+ var hasBlockElems = _.any(node.children, function(child) {
+ if (abbrUtils.isSnippet(child))
+ return false;
+
+ return !abbrUtils.isInline(child);
+ });
+
+ if (!hasBlockElems) {
+ return shouldFormatInline(node, profile);
+ }
+
+ return true;
+ }
+
+ /**
+ * Processes element with tag
type
+ * @param {AbbreviationNode} item
+ * @param {OutputProfile} profile
+ * @param {Number} level Depth level
+ */
+ function processTag(item, profile, level) {
+ item.start = item.end = placeholder;
+ var utils = require('utils');
+ var abbrUtils = require('abbreviationUtils');
+ var isUnary = abbrUtils.isUnary(item);
+ var nl = utils.getNewline();
+
+ // formatting output
+ if (profile.tag_nl !== false) {
+ var forceNl = profile.tag_nl === true && (profile.tag_nl_leaf || item.children.length);
+
+ // formatting block-level elements
+ if (!item.isTextNode()) {
+ if (shouldAddLineBreak(item, profile)) {
+ // - do not indent the very first element
+ // - do not indent first child of a snippet
+ if (!isVeryFirstChild(item) && (!abbrUtils.isSnippet(item.parent) || item.index()))
+ item.start = nl + item.start;
+
+ if (abbrUtils.hasBlockChildren(item) || shouldBreakChild(item, profile) || (forceNl && !isUnary))
+ item.end = nl + item.end;
+
+ if (abbrUtils.hasTagsInContent(item) || (forceNl && !item.children.length && !isUnary))
+ item.start += nl + getIndentation();
+ } else if (abbrUtils.isInline(item) && hasBlockSibling(item) && !isVeryFirstChild(item)) {
+ item.start = nl + item.start;
+ } else if (abbrUtils.isInline(item) && shouldBreakInsideInline(item, profile)) {
+ item.end = nl + item.end;
+ }
+
+ item.padding = getIndentation() ;
+ }
+ }
+
+ return item;
+ }
+
+ /**
+ * Processes simplified tree, making it suitable for output as HTML structure
+ * @param {AbbreviationNode} tree
+ * @param {OutputProfile} profile
+ * @param {Number} level Depth level
+ */
+ require('filters').add('_format', function process(tree, profile, level) {
+ level = level || 0;
+ var abbrUtils = require('abbreviationUtils');
+
+ _.each(tree.children, function(item) {
+ if (abbrUtils.isSnippet(item))
+ processSnippet(item, profile, level);
+ else
+ processTag(item, profile, level);
+
+ process(item, profile, level + 1);
+ });
+
+ return tree;
+ });
+});/**
+ * Filter for producing HAML code from abbreviation.
+ * @author Sergey Chikuyonok (serge.che@gmail.com)
+ * @link http://chikuyonok.ru
+ * @constructor
+ * @memberOf __hamlFilterDefine
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _) {
+ var childToken = '${child}';
+
+ function transformClassName(className) {
+ return require('utils').trim(className).replace(/\s+/g, '.');
+ }
+
+ /**
+ * Creates HAML attributes string from tag according to profile settings
+ * @param {AbbreviationNode} tag
+ * @param {Object} profile
+ */
+ function makeAttributesString(tag, profile) {
+ var attrs = '';
+ var otherAttrs = [];
+ var attrQuote = profile.attributeQuote();
+ var cursor = profile.cursor();
+
+ _.each(tag.attributeList(), function(a) {
+ var attrName = profile.attributeName(a.name);
+ switch (attrName.toLowerCase()) {
+ // use short notation for ID and CLASS attributes
+ case 'id':
+ attrs += '#' + (a.value || cursor);
+ break;
+ case 'class':
+ attrs += '.' + transformClassName(a.value || cursor);
+ break;
+ // process other attributes
+ default:
+ otherAttrs.push(':' +attrName + ' => ' + attrQuote + (a.value || cursor) + attrQuote);
+ }
+ });
+
+ if (otherAttrs.length)
+ attrs += '{' + otherAttrs.join(', ') + '}';
+
+ return attrs;
+ }
+
+ /**
+ * Test if passed node has block-level sibling element
+ * @param {AbbreviationNode} item
+ * @return {Boolean}
+ */
+ function hasBlockSibling(item) {
+ return item.parent && item.parent.hasBlockChildren();
+ }
+
+ /**
+ * Processes element with tag
type
+ * @param {AbbreviationNode} item
+ * @param {OutputProfile} profile
+ * @param {Number} level Depth level
+ */
+ function processTag(item, profile, level) {
+ if (!item.parent)
+ // looks like it's root element
+ return item;
+
+ var abbrUtils = require('abbreviationUtils');
+ var utils = require('utils');
+
+ var attrs = makeAttributesString(item, profile);
+ var cursor = profile.cursor();
+ var isUnary = abbrUtils.isUnary(item);
+ var selfClosing = profile.self_closing_tag && isUnary ? '/' : '';
+ var start= '';
+
+ // define tag name
+ var tagName = '%' + profile.tagName(item.name());
+ if (tagName.toLowerCase() == '%div' && attrs && attrs.indexOf('{') == -1)
+ // omit div tag
+ tagName = '';
+
+ item.end = '';
+ start = tagName + attrs + selfClosing + ' ';
+
+ var placeholder = '%s';
+ // We can't just replace placeholder with new value because
+ // JavaScript will treat double $ character as a single one, assuming
+ // we're using RegExp literal.
+ item.start = utils.replaceSubstring(item.start, start, item.start.indexOf(placeholder), placeholder);
+
+ if (!item.children.length && !isUnary)
+ item.start += cursor;
+
+ return item;
+ }
+
+ /**
+ * Processes simplified tree, making it suitable for output as HTML structure
+ * @param {AbbreviationNode} tree
+ * @param {Object} profile
+ * @param {Number} level Depth level
+ */
+ require('filters').add('haml', function process(tree, profile, level) {
+ level = level || 0;
+ var abbrUtils = require('abbreviationUtils');
+
+ if (!level) {
+ tree = require('filters').apply(tree, '_format', profile);
+ }
+
+ _.each(tree.children, function(item) {
+ if (!abbrUtils.isSnippet(item))
+ processTag(item, profile, level);
+
+ process(item, profile, level + 1);
+ });
+
+ return tree;
+ });
+});/**
+ * Filter that produces HTML tree
+ * @author Sergey Chikuyonok (serge.che@gmail.com)
+ * @link http://chikuyonok.ru
+ * @constructor
+ * @memberOf __htmlFilterDefine
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _) {
+ /**
+ * Creates HTML attributes string from tag according to profile settings
+ * @param {AbbreviationNode} node
+ * @param {OutputProfile} profile
+ */
+ function makeAttributesString(node, profile) {
+ var attrQuote = profile.attributeQuote();
+ var cursor = profile.cursor();
+
+ return _.map(node.attributeList(), function(a) {
+ var attrName = profile.attributeName(a.name);
+ return ' ' + attrName + '=' + attrQuote + (a.value || cursor) + attrQuote;
+ }).join('');
+ }
+
+ /**
+ * Processes element with tag
type
+ * @param {AbbreviationNode} item
+ * @param {OutputProfile} profile
+ * @param {Number} level Depth level
+ */
+ function processTag(item, profile, level) {
+ if (!item.parent) // looks like it's root element
+ return item;
+
+ var abbrUtils = require('abbreviationUtils');
+ var utils = require('utils');
+
+ var attrs = makeAttributesString(item, profile);
+ var cursor = profile.cursor();
+ var isUnary = abbrUtils.isUnary(item);
+ var start= '';
+ var end = '';
+
+ // define opening and closing tags
+ if (!item.isTextNode()) {
+ var tagName = profile.tagName(item.name());
+ if (isUnary) {
+ start = '<' + tagName + attrs + profile.selfClosing() + '>';
+ item.end = '';
+ } else {
+ start = '<' + tagName + attrs + '>';
+ end = '' + tagName + '>';
+ }
+ }
+
+ var placeholder = '%s';
+ // We can't just replace placeholder with new value because
+ // JavaScript will treat double $ character as a single one, assuming
+ // we're using RegExp literal.
+ item.start = utils.replaceSubstring(item.start, start, item.start.indexOf(placeholder), placeholder);
+ item.end = utils.replaceSubstring(item.end, end, item.end.indexOf(placeholder), placeholder);
+
+ if (!item.children.length && !isUnary && item.content.indexOf(cursor) == -1)
+ item.start += cursor;
+
+ return item;
+ }
+
+ /**
+ * Processes simplified tree, making it suitable for output as HTML structure
+ * @param {AbbreviationNode} tree
+ * @param {Object} profile
+ * @param {Number} level Depth level
+ */
+ require('filters').add('html', function process(tree, profile, level) {
+ level = level || 0;
+ var abbrUtils = require('abbreviationUtils');
+
+ if (!level) {
+ tree = require('filters').apply(tree, '_format', profile);
+ }
+
+ _.each(tree.children, function(item) {
+ if (!abbrUtils.isSnippet(item))
+ processTag(item, profile, level);
+
+ process(item, profile, level + 1);
+ });
+
+ return tree;
+ });
+});/**
+ * Output abbreviation on a single line (i.e. no line breaks)
+ * @author Sergey Chikuyonok (serge.che@gmail.com)
+ * @link http://chikuyonok.ru
+ * @constructor
+ * @memberOf __singleLineFilterDefine
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _) {
+ var rePad = /^\s+/;
+ var reNl = /[\n\r]/g;
+
+ require('filters').add('s', function process(tree, profile, level) {
+ var abbrUtils = require('abbreviationUtils');
+
+ _.each(tree.children, function(item) {
+ if (!abbrUtils.isSnippet(item)) {
+ // remove padding from item
+ item.start = item.start.replace(rePad, '');
+ item.end = item.end.replace(rePad, '');
+ }
+
+ // remove newlines
+ item.start = item.start.replace(reNl, '');
+ item.end = item.end.replace(reNl, '');
+ item.content = item.content.replace(reNl, '');
+
+ process(item);
+ });
+
+ return tree;
+ });
+});
/**
+ * Trim filter: removes characters at the beginning of the text
+ * content that indicates lists: numbers, #, *, -, etc.
+ *
+ * Useful for wrapping lists with abbreviation.
+ *
+ * @author Sergey Chikuyonok (serge.che@gmail.com)
+ * @link http://chikuyonok.ru
+ *
+ * @constructor
+ * @memberOf __trimFilterDefine
+ * @param {Function} require
+ * @param {Underscore} _
+ */
+emmet.exec(function(require, _) {
+ require('preferences').define('filter.trimRegexp', '[\\s|\\u00a0]*[\\d|#|\\-|\*|\\u2022]+\\.?\\s*',
+ 'Regular expression used to remove list markers (numbers, dashes, '
+ + 'bullets, etc.) in t
(trim) filter. The trim filter '
+ + 'is useful for wrapping with abbreviation lists, pased from other '
+ + 'documents (for example, Word documents).');
+
+ function process(tree, re) {
+ _.each(tree.children, function(item) {
+ if (item.content)
+ item.content = item.content.replace(re, '');
+
+ process(item, re);
+ });
+
+ return tree;
+ }
+
+ require('filters').add('t', function(tree) {
+ var re = new RegExp(require('preferences').get('filter.trimRegexp'));
+ return process(tree, re);
+ });
+});
/**
+ * Filter for trimming "select" attributes from some tags that contains
+ * child elements
+ * @author Sergey Chikuyonok (serge.che@gmail.com)
+ * @link http://chikuyonok.ru
+ *
+ * @constructor
+ * @memberOf __xslFilterDefine
+ * @param {Function} require
+ * @param {Underscore} _
+ */
emmet.exec(function(require, _) {
+ var tags = {
+ 'xsl:variable': 1,
+ 'xsl:with-param': 1
+ };
+
+ /**
+ * Removes "select" attribute from node
+ * @param {AbbreviationNode} node
+ */
+ function trimAttribute(node) {
+ node.start = node.start.replace(/\s+select\s*=\s*(['"]).*?\1/, '');
+ }
+
+ require('filters').add('xsl', function process(tree) {
+ var abbrUtils = require('abbreviationUtils');
+ _.each(tree.children, function(item) {
+ if (!abbrUtils.isSnippet(item)
+ && (item.name() || '').toLowerCase() in tags
+ && item.children.length)
+ trimAttribute(item);
+ process(item);
+ });
+
+ return tree;
+ });
+});/**
+ * "Lorem ipsum" text generator. Matches lipsum(num)?
or
+ * lorem(num)?
abbreviation.
+ * This code is based on Django's contribution:
+ * https://code.djangoproject.com/browser/django/trunk/django/contrib/webdesign/lorem_ipsum.py
+ * lipsum
– generates 30 words text.lipsum*6
– generates 6 paragraphs (autowrapped with <p> element) of text.ol>lipsum10*5
— generates ordered list with 5 list items (autowrapped with <li> tag)
+ * with text of 10 words on each linespan*3>lipsum20
– generates 3 paragraphs of 20-words text, each wrapped with <span> element .
+ * Each paragraph phrase is unique
+ * @param {Function} require
+ * @param {Underscore} _
+ * @constructor
+ * @memberOf __loremIpsumGeneratorDefine
+ */
+emmet.exec(function(require, _) {
+ /**
+ * @param {AbbreviationNode} tree
+ * @param {Object} options
+ */
+ require('abbreviationParser').addPreprocessor(function(tree, options) {
+ var re = /^(?:lorem|lipsum)(\d*)$/i, match;
+
+ /** @param {AbbreviationNode} node */
+ tree.findAll(function(node) {
+ if (node._name && (match = node._name.match(re))) {
+ var wordCound = match[1] || 30;
+
+ // force node name resolving if node should be repeated
+ // or contains attributes. In this case, node should be outputed
+ // as tag, otherwise as text-only node
+ node._name = '';
+ node.data('forceNameResolving', node.isRepeating() || node.attributeList().length);
+ node.data('pasteOverwrites', true);
+ node.data('paste', function(i, content) {
+ return paragraph(wordCound, !i);
+ });
+ }
+ });
+ });
+
+ var COMMON_P = 'lorem ipsum dolor sit amet consectetur adipisicing elit'.split(' ');
+
+ var 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'];
+
+ /**
+ * Returns random integer between from
and to
values
+ * @param {Number} from
+ * @param {Number} to
+ * @returns {Number}
+ */
+ function randint(from, to) {
+ return Math.round(Math.random() * (to - from) + from);
+ }
+
+ /**
+ * @param {Array} arr
+ * @param {Number} count
+ * @returns {Array}
+ */
+ function sample(arr, count) {
+ var len = arr.length;
+ var iterations = Math.min(len, count);
+ var result = [];
+ while (result.length < iterations) {
+ var randIx = randint(0, len - 1);
+ if (!_.include(result, randIx))
+ result.push(randIx);
+ }
+
+ return _.map(result, function(ix) {
+ return arr[ix];
+ });
+ }
+
+ function choice(val) {
+ if (_.isString(val))
+ return val.charAt(randint(0, val.length - 1));
+
+ return val[randint(0, val.length - 1)];
+ }
+
+ function sentence(words, end) {
+ if (words.length) {
+ words[0] = words[0].charAt(0).toUpperCase() + words[0].substring(1);
+ }
+
+ return words.join(' ') + (end || choice('?!...')); // more dots that question marks
+ }
+
+ /**
+ * Insert commas at randomly selected words. This function modifies values
+ * inside words
array
+ * @param {Array} words
+ */
+ function insertCommas(words) {
+ var len = words.length;
+ var totalCommas = 0;
+
+ if (len > 3 && len <= 6) {
+ totalCommas = randint(0, 1);
+ } else if (len > 6 && len <= 12) {
+ totalCommas = randint(0, 2);
+ } else {
+ totalCommas = randint(1, 4);
+ }
+
+ _.each(sample(_.range(totalCommas)), function(ix) {
+ words[ix] += ',';
+ });
+ }
+
+ /**
+ * Generate a paragraph of "Lorem ipsum" text
+ * @param {Number} wordCount Words count in paragraph
+ * @param {Boolean} startWithCommon Should paragraph start with common
+ * "lorem ipsum" sentence.
+ * @returns {String}
+ */
+ function paragraph(wordCount, startWithCommon) {
+ var result = [];
+ var totalWords = 0;
+ var words;
+
+ wordCount = parseInt(wordCount, 10);
+
+ if (startWithCommon) {
+ words = COMMON_P.slice(0, wordCount);
+ if (words.length > 5)
+ words[4] += ',';
+ totalWords += words.length;
+ result.push(sentence(words, '.'));
+ }
+
+ while (totalWords < wordCount) {
+ words = sample(WORDS, Math.min(randint(3, 12) * randint(1, 5), wordCount - totalWords));
+ totalWords += words.length;
+ insertCommas(words);
+ result.push(sentence(words));
+ }
+
+ return result.join(' ');
+ }
+});/**
+ * Select current line (for simple editors like browser's <textarea>)
+ */
+emmet.exec(function(require, _) {
+ require('actions').add('select_line', function(editor) {
+ var range = editor.getCurrentLineRange();
+ editor.createSelection(range.start, range.end);
+ return true;
+ });
+});emmet.exec(function(require, _){require('resources').setVocabulary({
+ "variables": {
+ "lang": "en",
+ "locale": "en-US",
+ "charset": "UTF-8",
+ "indentation": "\t",
+ "newline": "\n"
+ },
+
+ "css": {
+ "filters": "html",
+ "snippets": {
+ "@i": "@import url(|);",
+ "@m": "@media print {\n\t|\n}",
+ "@f": "@font-face {\n\tfont-family:|;\n\tsrc:url(|);\n}",
+ "@f+": "@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}",
+ "!": "!important",
+ "pos": "position:|;",
+ "pos:s": "position:static;",
+ "pos:a": "position:absolute;",
+ "pos:r": "position:relative;",
+ "pos:f": "position:fixed;",
+ "t": "top:|;",
+ "t:a": "top:auto;",
+ "r": "right:|;",
+ "r:a": "right:auto;",
+ "b": "bottom:|;",
+ "b:a": "bottom:auto;",
+ "l": "left:|;",
+ "l:a": "left:auto;",
+ "z": "z-index:|;",
+ "z:a": "z-index:auto;",
+ "fl": "float:|;",
+ "fl:n": "float:none;",
+ "fl:l": "float:left;",
+ "fl:r": "float:right;",
+ "cl": "clear:|;",
+ "cl:n": "clear:none;",
+ "cl:l": "clear:left;",
+ "cl:r": "clear:right;",
+ "cl:b": "clear:both;",
+ "d": "display:|;",
+ "d:n": "display:none;",
+ "d:b": "display:block;",
+ "d:i": "display:inline;",
+ "d:ib": "display:inline-block;",
+ "d:li": "display:list-item;",
+ "d:ri": "display:run-in;",
+ "d:cp": "display:compact;",
+ "d:tb": "display:table;",
+ "d:itb": "display:inline-table;",
+ "d:tbcp": "display:table-caption;",
+ "d:tbcl": "display:table-column;",
+ "d:tbclg": "display:table-column-group;",
+ "d:tbhg": "display:table-header-group;",
+ "d:tbfg": "display:table-footer-group;",
+ "d:tbr": "display:table-row;",
+ "d:tbrg": "display:table-row-group;",
+ "d:tbc": "display:table-cell;",
+ "d:rb": "display:ruby;",
+ "d:rbb": "display:ruby-base;",
+ "d:rbbg": "display:ruby-base-group;",
+ "d:rbt": "display:ruby-text;",
+ "d:rbtg": "display:ruby-text-group;",
+ "v": "visibility:|;",
+ "v:v": "visibility:visible;",
+ "v:h": "visibility:hidden;",
+ "v:c": "visibility:collapse;",
+ "ov": "overflow:|;",
+ "ov:v": "overflow:visible;",
+ "ov:h": "overflow:hidden;",
+ "ov:s": "overflow:scroll;",
+ "ov:a": "overflow:auto;",
+ "ovx": "overflow-x:|;",
+ "ovx:v": "overflow-x:visible;",
+ "ovx:h": "overflow-x:hidden;",
+ "ovx:s": "overflow-x:scroll;",
+ "ovx:a": "overflow-x:auto;",
+ "ovy": "overflow-y:|;",
+ "ovy:v": "overflow-y:visible;",
+ "ovy:h": "overflow-y:hidden;",
+ "ovy:s": "overflow-y:scroll;",
+ "ovy:a": "overflow-y:auto;",
+ "ovs": "overflow-style:|;",
+ "ovs:a": "overflow-style:auto;",
+ "ovs:s": "overflow-style:scrollbar;",
+ "ovs:p": "overflow-style:panner;",
+ "ovs:m": "overflow-style:move;",
+ "ovs:mq": "overflow-style:marquee;",
+ "zoo": "zoom:1;",
+ "cp": "clip:|;",
+ "cp:a": "clip:auto;",
+ "cp:r": "clip:rect(|);",
+ "bxz": "box-sizing:|;",
+ "bxz:cb": "box-sizing:content-box;",
+ "bxz:bb": "box-sizing:border-box;",
+ "bxsh": "box-shadow:${1:hoff} ${2:voff} ${3:radius} ${4:color};",
+ "bxsh:n": "box-shadow:none;",
+ "m": "margin:|;",
+ "m:a": "margin:auto;",
+ "mt": "margin-top:|;",
+ "mt:a": "margin-top:auto;",
+ "mr": "margin-right:|;",
+ "mr:a": "margin-right:auto;",
+ "mb": "margin-bottom:|;",
+ "mb:a": "margin-bottom:auto;",
+ "ml": "margin-left:|;",
+ "ml:a": "margin-left:auto;",
+ "p": "padding:|;",
+ "pt": "padding-top:|;",
+ "pr": "padding-right:|;",
+ "pb": "padding-bottom:|;",
+ "pl": "padding-left:|;",
+ "w": "width:|;",
+ "w:a": "width:auto;",
+ "h": "height:|;",
+ "h:a": "height:auto;",
+ "maw": "max-width:|;",
+ "maw:n": "max-width:none;",
+ "mah": "max-height:|;",
+ "mah:n": "max-height:none;",
+ "miw": "min-width:|;",
+ "mih": "min-height:|;",
+ "o": "outline:|;",
+ "o:n": "outline:none;",
+ "oo": "outline-offset:|;",
+ "ow": "outline-width:|;",
+ "os": "outline-style:|;",
+ "oc": "outline-color:#${1:000};",
+ "oc:i": "outline-color:invert;",
+ "bd": "border:|;",
+ "bd+": "border:${1:1px} ${2:solid} ${3:#000};",
+ "bd:n": "border:none;",
+ "bdbk": "border-break:|;",
+ "bdbk:c": "border-break:close;",
+ "bdcl": "border-collapse:|;",
+ "bdcl:c": "border-collapse:collapse;",
+ "bdcl:s": "border-collapse:separate;",
+ "bdc": "border-color:#${1:000};",
+ "bdi": "border-image:url(|);",
+ "bdi:n": "border-image:none;",
+ "bdti": "border-top-image:url(|);",
+ "bdti:n": "border-top-image:none;",
+ "bdri": "border-right-image:url(|);",
+ "bdri:n": "border-right-image:none;",
+ "bdbi": "border-bottom-image:url(|);",
+ "bdbi:n": "border-bottom-image:none;",
+ "bdli": "border-left-image:url(|);",
+ "bdli:n": "border-left-image:none;",
+ "bdci": "border-corner-image:url(|);",
+ "bdci:n": "border-corner-image:none;",
+ "bdci:c": "border-corner-image:continue;",
+ "bdtli": "border-top-left-image:url(|);",
+ "bdtli:n": "border-top-left-image:none;",
+ "bdtli:c": "border-top-left-image:continue;",
+ "bdtri": "border-top-right-image:url(|);",
+ "bdtri:n": "border-top-right-image:none;",
+ "bdtri:c": "border-top-right-image:continue;",
+ "bdbri": "border-bottom-right-image:url(|);",
+ "bdbri:n": "border-bottom-right-image:none;",
+ "bdbri:c": "border-bottom-right-image:continue;",
+ "bdbli": "border-bottom-left-image:url(|);",
+ "bdbli:n": "border-bottom-left-image:none;",
+ "bdbli:c": "border-bottom-left-image:continue;",
+ "bdf": "border-fit:|;",
+ "bdf:c": "border-fit:clip;",
+ "bdf:r": "border-fit:repeat;",
+ "bdf:sc": "border-fit:scale;",
+ "bdf:st": "border-fit:stretch;",
+ "bdf:ow": "border-fit:overwrite;",
+ "bdf:of": "border-fit:overflow;",
+ "bdf:sp": "border-fit:space;",
+ "bdl": "border-length:|;",
+ "bdl:a": "border-length:auto;",
+ "bdsp": "border-spacing:|;",
+ "bds": "border-style:|;",
+ "bds:n": "border-style:none;",
+ "bds:h": "border-style:hidden;",
+ "bds:dt": "border-style:dotted;",
+ "bds:ds": "border-style:dashed;",
+ "bds:s": "border-style:solid;",
+ "bds:db": "border-style:double;",
+ "bds:dtds": "border-style:dot-dash;",
+ "bds:dtdtds": "border-style:dot-dot-dash;",
+ "bds:w": "border-style:wave;",
+ "bds:g": "border-style:groove;",
+ "bds:r": "border-style:ridge;",
+ "bds:i": "border-style:inset;",
+ "bds:o": "border-style:outset;",
+ "bdw": "border-width:|;",
+ "bdt": "border-top:|;",
+ "bt": "border-top:|;",
+ "bdt+": "border-top:${1:1px} ${2:solid} ${3:#000};",
+ "bdt:n": "border-top:none;",
+ "bdtw": "border-top-width:|;",
+ "bdts": "border-top-style:|;",
+ "bdts:n": "border-top-style:none;",
+ "bdtc": "border-top-color:#${1:000};",
+ "bdr": "border-right:|;",
+ "br": "border-right:|;",
+ "bdr+": "border-right:${1:1px} ${2:solid} ${3:#000};",
+ "bdr:n": "border-right:none;",
+ "bdrw": "border-right-width:|;",
+ "bdrs": "border-right-style:|;",
+ "bdrs:n": "border-right-style:none;",
+ "bdrc": "border-right-color:#${1:000};",
+ "bdb": "border-bottom:|;",
+ "bb": "border-bottom:|;",
+ "bdb+": "border-bottom:${1:1px} ${2:solid} ${3:#000};",
+ "bdb:n": "border-bottom:none;",
+ "bdbw": "border-bottom-width:|;",
+ "bdbs": "border-bottom-style:|;",
+ "bdbs:n": "border-bottom-style:none;",
+ "bdbc": "border-bottom-color:#${1:000};",
+ "bdl": "border-left:|;",
+ "bl": "border-left:|;",
+ "bdl+": "border-left:${1:1px} ${2:solid} ${3:#000};",
+ "bdl:n": "border-left:none;",
+ "bdlw": "border-left-width:|;",
+ "bdls": "border-left-style:|;",
+ "bdls:n": "border-left-style:none;",
+ "bdlc": "border-left-color:#${1:000};",
+ "bdrs": "border-radius:|;",
+ "bdtrrs": "border-top-right-radius:|;",
+ "bdtlrs": "border-top-left-radius:|;",
+ "bdbrrs": "border-bottom-right-radius:|;",
+ "bdblrs": "border-bottom-left-radius:|;",
+ "bg": "background:|;",
+ "bg+": "background:${1:#fff} url(${2}) ${3:0} ${4:0} ${5:no-repeat};",
+ "bg:n": "background:none;",
+ "bg:ie": "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='${1:x}.png',sizingMethod='${2:crop}');",
+ "bgc": "background-color:#${1:fff};",
+ "bgi": "background-image:url(|);",
+ "bgi:n": "background-image:none;",
+ "bgr": "background-repeat:|;",
+ "bgr:n": "background-repeat:no-repeat;",
+ "bgr:x": "background-repeat:repeat-x;",
+ "bgr:y": "background-repeat:repeat-y;",
+ "bga": "background-attachment:|;",
+ "bga:f": "background-attachment:fixed;",
+ "bga:s": "background-attachment:scroll;",
+ "bgp": "background-position:${1:0} ${2:0};",
+ "bgpx": "background-position-x:|;",
+ "bgpy": "background-position-y:|;",
+ "bgbk": "background-break:|;",
+ "bgbk:bb": "background-break:bounding-box;",
+ "bgbk:eb": "background-break:each-box;",
+ "bgbk:c": "background-break:continuous;",
+ "bgcp": "background-clip:|;",
+ "bgcp:bb": "background-clip:border-box;",
+ "bgcp:pb": "background-clip:padding-box;",
+ "bgcp:cb": "background-clip:content-box;",
+ "bgcp:nc": "background-clip:no-clip;",
+ "bgo": "background-origin:|;",
+ "bgo:pb": "background-origin:padding-box;",
+ "bgo:bb": "background-origin:border-box;",
+ "bgo:cb": "background-origin:content-box;",
+ "bgz": "background-size:|;",
+ "bgz:a": "background-size:auto;",
+ "bgz:ct": "background-size:contain;",
+ "bgz:cv": "background-size:cover;",
+ "c": "color:#${1:000};",
+ "cm": "/* |${child} */",
+ "cn": "content:|;",
+ "tbl": "table-layout:|;",
+ "tbl:a": "table-layout:auto;",
+ "tbl:f": "table-layout:fixed;",
+ "cps": "caption-side:|;",
+ "cps:t": "caption-side:top;",
+ "cps:b": "caption-side:bottom;",
+ "ec": "empty-cells:|;",
+ "ec:s": "empty-cells:show;",
+ "ec:h": "empty-cells:hide;",
+ "lis": "list-style:|;",
+ "lis:n": "list-style:none;",
+ "lisp": "list-style-position:|;",
+ "lisp:i": "list-style-position:inside;",
+ "lisp:o": "list-style-position:outside;",
+ "list": "list-style-type:|;",
+ "list:n": "list-style-type:none;",
+ "list:d": "list-style-type:disc;",
+ "list:c": "list-style-type:circle;",
+ "list:s": "list-style-type:square;",
+ "list:dc": "list-style-type:decimal;",
+ "list:dclz": "list-style-type:decimal-leading-zero;",
+ "list:lr": "list-style-type:lower-roman;",
+ "list:ur": "list-style-type:upper-roman;",
+ "lisi": "list-style-image:|;",
+ "lisi:n": "list-style-image:none;",
+ "q": "quotes:|;",
+ "q:n": "quotes:none;",
+ "q:ru": "quotes:'\\00AB' '\\00BB' '\\201E' '\\201C';",
+ "q:en": "quotes:'\\201C' '\\201D' '\\2018' '\\2019';",
+ "ct": "content:|;",
+ "ct:n": "content:normal;",
+ "ct:oq": "content:open-quote;",
+ "ct:noq": "content:no-open-quote;",
+ "ct:cq": "content:close-quote;",
+ "ct:ncq": "content:no-close-quote;",
+ "ct:a": "content:attr(|);",
+ "ct:c": "content:counter(|);",
+ "ct:cs": "content:counters(|);",
+ "coi": "counter-increment:|;",
+ "cor": "counter-reset:|;",
+ "va": "vertical-align:|;",
+ "va:sup": "vertical-align:super;",
+ "va:t": "vertical-align:top;",
+ "va:tt": "vertical-align:text-top;",
+ "va:m": "vertical-align:middle;",
+ "va:bl": "vertical-align:baseline;",
+ "va:b": "vertical-align:bottom;",
+ "va:tb": "vertical-align:text-bottom;",
+ "va:sub": "vertical-align:sub;",
+ "ta": "text-align:|;",
+ "ta:l": "text-align:left;",
+ "ta:c": "text-align:center;",
+ "ta:r": "text-align:right;",
+ "tal": "text-align-last:|;",
+ "tal:a": "text-align-last:auto;",
+ "tal:l": "text-align-last:left;",
+ "tal:c": "text-align-last:center;",
+ "tal:r": "text-align-last:right;",
+ "td": "text-decoration:|;",
+ "td:n": "text-decoration:none;",
+ "td:u": "text-decoration:underline;",
+ "td:o": "text-decoration:overline;",
+ "td:l": "text-decoration:line-through;",
+ "te": "text-emphasis:|;",
+ "te:n": "text-emphasis:none;",
+ "te:ac": "text-emphasis:accent;",
+ "te:dt": "text-emphasis:dot;",
+ "te:c": "text-emphasis:circle;",
+ "te:ds": "text-emphasis:disc;",
+ "te:b": "text-emphasis:before;",
+ "te:a": "text-emphasis:after;",
+ "th": "text-height:|;",
+ "th:a": "text-height:auto;",
+ "th:f": "text-height:font-size;",
+ "th:t": "text-height:text-size;",
+ "th:m": "text-height:max-size;",
+ "ti": "text-indent:|;",
+ "ti:-": "text-indent:-9999px;",
+ "tj": "text-justify:|;",
+ "tj:a": "text-justify:auto;",
+ "tj:iw": "text-justify:inter-word;",
+ "tj:ii": "text-justify:inter-ideograph;",
+ "tj:ic": "text-justify:inter-cluster;",
+ "tj:d": "text-justify:distribute;",
+ "tj:k": "text-justify:kashida;",
+ "tj:t": "text-justify:tibetan;",
+ "to": "text-outline:|;",
+ "to+": "text-outline:${1:0} ${2:0} ${3:#000};",
+ "to:n": "text-outline:none;",
+ "tr": "text-replace:|;",
+ "tr:n": "text-replace:none;",
+ "tt": "text-transform:|;",
+ "tt:n": "text-transform:none;",
+ "tt:c": "text-transform:capitalize;",
+ "tt:u": "text-transform:uppercase;",
+ "tt:l": "text-transform:lowercase;",
+ "tw": "text-wrap:|;",
+ "tw:n": "text-wrap:normal;",
+ "tw:no": "text-wrap:none;",
+ "tw:u": "text-wrap:unrestricted;",
+ "tw:s": "text-wrap:suppress;",
+ "tsh": "text-shadow:${1:hoff} ${2:voff} ${3:blur} ${4:#000};",
+ "tsh+": "text-shadow:${1:0} ${2:0} ${3:0} ${4:#000};",
+ "tsh:n": "text-shadow:none;",
+ "trf": "transform:|;",
+ "trf:skx": "transform: skewX(${1:angle});",
+ "trf:sky": "transform: skewY(${1:angle});",
+ "trf:sc": "transform: scale(${1:x}, ${2:y});",
+ "trf:scx": "transform: scaleX(${1:x});",
+ "trf:scy": "transform: scaleY(${1:y});",
+ "trf:r": "transform: rotate(${1:angle});",
+ "trf:t": "transform: translate(${1:x}, ${2:y});",
+ "trf:tx": "transform: translateX(${1:x});",
+ "trf:ty": "transform: translateY(${1:y});",
+ "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:tfunc};",
+ "lh": "line-height:|;",
+ "whs": "white-space:|;",
+ "whs:n": "white-space:normal;",
+ "whs:p": "white-space:pre;",
+ "whs:nw": "white-space:nowrap;",
+ "whs:pw": "white-space:pre-wrap;",
+ "whs:pl": "white-space:pre-line;",
+ "whsc": "white-space-collapse:|;",
+ "whsc:n": "white-space-collapse:normal;",
+ "whsc:k": "white-space-collapse:keep-all;",
+ "whsc:l": "white-space-collapse:loose;",
+ "whsc:bs": "white-space-collapse:break-strict;",
+ "whsc:ba": "white-space-collapse:break-all;",
+ "wob": "word-break:|;",
+ "wob:n": "word-break:normal;",
+ "wob:k": "word-break:keep-all;",
+ "wob:l": "word-break:loose;",
+ "wob:bs": "word-break:break-strict;",
+ "wob:ba": "word-break:break-all;",
+ "wos": "word-spacing:|;",
+ "wow": "word-wrap:|;",
+ "wow:nm": "word-wrap:normal;",
+ "wow:n": "word-wrap:none;",
+ "wow:u": "word-wrap:unrestricted;",
+ "wow:s": "word-wrap:suppress;",
+ "lts": "letter-spacing:|;",
+ "f": "font:|;",
+ "f+": "font:${1:1em} ${2:Arial,sans-serif};",
+ "fw": "font-weight:|;",
+ "fw:n": "font-weight:normal;",
+ "fw:b": "font-weight:bold;",
+ "fw:br": "font-weight:bolder;",
+ "fw:lr": "font-weight:lighter;",
+ "fs": "font-style:|;",
+ "fs:n": "font-style:normal;",
+ "fs:i": "font-style:italic;",
+ "fs:o": "font-style:oblique;",
+ "fv": "font-variant:|;",
+ "fv:n": "font-variant:normal;",
+ "fv:sc": "font-variant:small-caps;",
+ "fz": "font-size:|;",
+ "fza": "font-size-adjust:|;",
+ "fza:n": "font-size-adjust:none;",
+ "ff": "font-family:|;",
+ "ff:s": "font-family:serif;",
+ "ff:ss": "font-family:sans-serif;",
+ "ff:c": "font-family:cursive;",
+ "ff:f": "font-family:fantasy;",
+ "ff:m": "font-family:monospace;",
+ "fef": "font-effect:|;",
+ "fef:n": "font-effect:none;",
+ "fef:eg": "font-effect:engrave;",
+ "fef:eb": "font-effect:emboss;",
+ "fef:o": "font-effect:outline;",
+ "fem": "font-emphasize:|;",
+ "femp": "font-emphasize-position:|;",
+ "femp:b": "font-emphasize-position:before;",
+ "femp:a": "font-emphasize-position:after;",
+ "fems": "font-emphasize-style:|;",
+ "fems:n": "font-emphasize-style:none;",
+ "fems:ac": "font-emphasize-style:accent;",
+ "fems:dt": "font-emphasize-style:dot;",
+ "fems:c": "font-emphasize-style:circle;",
+ "fems:ds": "font-emphasize-style:disc;",
+ "fsm": "font-smooth:|;",
+ "fsm:a": "font-smooth:auto;",
+ "fsm:n": "font-smooth:never;",
+ "fsm:aw": "font-smooth:always;",
+ "fst": "font-stretch:|;",
+ "fst:n": "font-stretch:normal;",
+ "fst:uc": "font-stretch:ultra-condensed;",
+ "fst:ec": "font-stretch:extra-condensed;",
+ "fst:c": "font-stretch:condensed;",
+ "fst:sc": "font-stretch:semi-condensed;",
+ "fst:se": "font-stretch:semi-expanded;",
+ "fst:e": "font-stretch:expanded;",
+ "fst:ee": "font-stretch:extra-expanded;",
+ "fst:ue": "font-stretch:ultra-expanded;",
+ "op": "opacity:|;",
+ "op:ie": "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity=100);",
+ "op:ms": "-ms-filter:'progid:DXImageTransform.Microsoft.Alpha(Opacity=100)';",
+ "rz": "resize:|;",
+ "rz:n": "resize:none;",
+ "rz:b": "resize:both;",
+ "rz:h": "resize:horizontal;",
+ "rz:v": "resize:vertical;",
+ "cur": "cursor:|;",
+ "cur:a": "cursor:auto;",
+ "cur:d": "cursor:default;",
+ "cur:c": "cursor:crosshair;",
+ "cur:ha": "cursor:hand;",
+ "cur:he": "cursor:help;",
+ "cur:m": "cursor:move;",
+ "cur:p": "cursor:pointer;",
+ "cur:t": "cursor:text;",
+ "pgbb": "page-break-before:|;",
+ "pgbb:au": "page-break-before:auto;",
+ "pgbb:al": "page-break-before:always;",
+ "pgbb:l": "page-break-before:left;",
+ "pgbb:r": "page-break-before:right;",
+ "pgbi": "page-break-inside:|;",
+ "pgbi:au": "page-break-inside:auto;",
+ "pgbi:av": "page-break-inside:avoid;",
+ "pgba": "page-break-after:|;",
+ "pgba:au": "page-break-after:auto;",
+ "pgba:al": "page-break-after:always;",
+ "pgba:l": "page-break-after:left;",
+ "pgba:r": "page-break-after:right;",
+ "orp": "orphans:|;",
+ "wid": "widows:|;"
+ }
+ },
+
+ "html": {
+ "filters": "html",
+ "profile": "html",
+ "snippets": {
+ "c": "",
+ "cc:ie6": "",
+ "cc:ie": "",
+ "cc:noie": "\n\t${child}|\n",
+ "html:4t": "\n\n\n\t\n\t