// Underscore.js 1.3.3 // (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc. // Underscore is freely distributable under the MIT license. // Portions of Underscore are inspired or borrowed from Prototype, // Oliver Steele's Functional, and John Resig's Micro-Templating. // For all details and documentation: // http://documentcloud.github.com/underscore var _ = (function() { // Baseline setup // -------------- // Establish the root object, `window` in the browser, or `global` on the server. var root = this; // Save the previous value of the `_` variable. var previousUnderscore = root._; // Establish the object that gets returned to break out of a loop iteration. var breaker = {}; // Save bytes in the minified (but not gzipped) version: var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; // Create quick reference variables for speed access to core prototypes. var slice = ArrayProto.slice, unshift = ArrayProto.unshift, toString = ObjProto.toString, hasOwnProperty = ObjProto.hasOwnProperty; // All **ECMAScript 5** native function implementations that we hope to use // are declared here. var nativeForEach = ArrayProto.forEach, nativeMap = ArrayProto.map, nativeReduce = ArrayProto.reduce, nativeReduceRight = ArrayProto.reduceRight, nativeFilter = ArrayProto.filter, nativeEvery = ArrayProto.every, nativeSome = ArrayProto.some, nativeIndexOf = ArrayProto.indexOf, nativeLastIndexOf = ArrayProto.lastIndexOf, nativeIsArray = Array.isArray, nativeKeys = Object.keys, nativeBind = FuncProto.bind; // Create a safe reference to the Underscore object for use below. var _ = function(obj) { return new wrapper(obj); }; // Export the Underscore object for **Node.js**, with // backwards-compatibility for the old `require()` API. If we're in // the browser, add `_` as a global object via a string identifier, // for Closure Compiler "advanced" mode. if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { exports = module.exports = _; } exports._ = _; } else { root['_'] = _; } // Current version. _.VERSION = '1.3.3'; // Collection Functions // -------------------- // The cornerstone, an `each` implementation, aka `forEach`. // Handles objects with the built-in `forEach`, arrays, and raw objects. // Delegates to **ECMAScript 5**'s native `forEach` if available. var each = _.each = _.forEach = function(obj, iterator, context) { if (obj == null) return; if (nativeForEach && obj.forEach === nativeForEach) { obj.forEach(iterator, context); } else if (obj.length === +obj.length) { for (var i = 0, l = obj.length; i < l; i++) { if (i in obj && iterator.call(context, obj[i], i, obj) === breaker) return; } } else { for (var key in obj) { if (_.has(obj, key)) { if (iterator.call(context, obj[key], key, obj) === breaker) return; } } } }; // Return the results of applying the iterator to each element. // Delegates to **ECMAScript 5**'s native `map` if available. _.map = _.collect = function(obj, iterator, context) { var results = []; if (obj == null) return results; if (nativeMap && obj.map === nativeMap) return obj.map(iterator, context); each(obj, function(value, index, list) { results[results.length] = iterator.call(context, value, index, list); }); if (obj.length === +obj.length) results.length = obj.length; return results; }; // **Reduce** builds up a single result from a list of values, aka `inject`, // or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available. _.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) { var initial = arguments.length > 2; if (obj == null) obj = []; if (nativeReduce && obj.reduce === nativeReduce) { if (context) iterator = _.bind(iterator, context); return initial ? obj.reduce(iterator, memo) : obj.reduce(iterator); } each(obj, function(value, index, list) { if (!initial) { memo = value; initial = true; } else { memo = iterator.call(context, memo, value, index, list); } }); if (!initial) throw new TypeError('Reduce of empty array with no initial value'); return memo; }; // The right-associative version of reduce, also known as `foldr`. // Delegates to **ECMAScript 5**'s native `reduceRight` if available. _.reduceRight = _.foldr = function(obj, iterator, memo, context) { var initial = arguments.length > 2; if (obj == null) obj = []; if (nativeReduceRight && obj.reduceRight === nativeReduceRight) { if (context) iterator = _.bind(iterator, context); return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator); } var reversed = _.toArray(obj).reverse(); if (context && !initial) iterator = _.bind(iterator, context); return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator); }; // Return the first value which passes a truth test. Aliased as `detect`. _.find = _.detect = function(obj, iterator, context) { var result; any(obj, function(value, index, list) { if (iterator.call(context, value, index, list)) { result = value; return true; } }); return result; }; // Return all the elements that pass a truth test. // Delegates to **ECMAScript 5**'s native `filter` if available. // Aliased as `select`. _.filter = _.select = function(obj, iterator, context) { var results = []; if (obj == null) return results; if (nativeFilter && obj.filter === nativeFilter) return obj.filter(iterator, context); each(obj, function(value, index, list) { if (iterator.call(context, value, index, list)) results[results.length] = value; }); return results; }; // Return all the elements for which a truth test fails. _.reject = function(obj, iterator, context) { var results = []; if (obj == null) return results; each(obj, function(value, index, list) { if (!iterator.call(context, value, index, list)) results[results.length] = value; }); return results; }; // Determine whether all of the elements match a truth test. // Delegates to **ECMAScript 5**'s native `every` if available. // Aliased as `all`. _.every = _.all = function(obj, iterator, context) { var result = true; if (obj == null) return result; if (nativeEvery && obj.every === nativeEvery) return obj.every(iterator, context); each(obj, function(value, index, list) { if (!(result = result && iterator.call(context, value, index, list))) return breaker; }); return !!result; }; // Determine if at least one element in the object matches a truth test. // Delegates to **ECMAScript 5**'s native `some` if available. // Aliased as `any`. var any = _.some = _.any = function(obj, iterator, context) { iterator || (iterator = _.identity); var result = false; if (obj == null) return result; if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context); each(obj, function(value, index, list) { if (result || (result = iterator.call(context, value, index, list))) return breaker; }); return !!result; }; // Determine if a given value is included in the array or object using `===`. // Aliased as `contains`. _.include = _.contains = function(obj, target) { var found = false; if (obj == null) return found; if (nativeIndexOf && obj.indexOf === nativeIndexOf) return obj.indexOf(target) != -1; found = any(obj, function(value) { return value === target; }); return found; }; // Invoke a method (with arguments) on every item in a collection. _.invoke = function(obj, method) { var args = slice.call(arguments, 2); return _.map(obj, function(value) { return (_.isFunction(method) ? method || value : value[method]).apply(value, args); }); }; // Convenience version of a common use case of `map`: fetching a property. _.pluck = function(obj, key) { return _.map(obj, function(value){ return value[key]; }); }; // Return the maximum element or (element-based computation). _.max = function(obj, iterator, context) { if (!iterator && _.isArray(obj) && obj[0] === +obj[0]) return Math.max.apply(Math, obj); if (!iterator && _.isEmpty(obj)) return -Infinity; var result = {computed : -Infinity}; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; computed >= result.computed && (result = {value : value, computed : computed}); }); return result.value; }; // Return the minimum element (or element-based computation). _.min = function(obj, iterator, context) { if (!iterator && _.isArray(obj) && obj[0] === +obj[0]) return Math.min.apply(Math, obj); if (!iterator && _.isEmpty(obj)) return Infinity; var result = {computed : Infinity}; each(obj, function(value, index, list) { var computed = iterator ? iterator.call(context, value, index, list) : value; computed < result.computed && (result = {value : value, computed : computed}); }); return result.value; }; // Shuffle an array. _.shuffle = function(obj) { var shuffled = [], rand; each(obj, function(value, index, list) { rand = Math.floor(Math.random() * (index + 1)); shuffled[index] = shuffled[rand]; shuffled[rand] = value; }); return shuffled; }; // Sort the object's values by a criterion produced by an iterator. _.sortBy = function(obj, val, context) { var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; }; return _.pluck(_.map(obj, function(value, index, list) { return { value : value, criteria : iterator.call(context, value, index, list) }; }).sort(function(left, right) { var a = left.criteria, b = right.criteria; if (a === void 0) return 1; if (b === void 0) return -1; return a < b ? -1 : a > b ? 1 : 0; }), 'value'); }; // Groups the object's values by a criterion. Pass either a string attribute // to group by, or a function that returns the criterion. _.groupBy = function(obj, val) { var result = {}; var iterator = _.isFunction(val) ? val : function(obj) { return obj[val]; }; each(obj, function(value, index) { var key = iterator(value, index); (result[key] || (result[key] = [])).push(value); }); return result; }; // Use a comparator function to figure out at what index an object should // be inserted so as to maintain order. Uses binary search. _.sortedIndex = function(array, obj, iterator) { iterator || (iterator = _.identity); var low = 0, high = array.length; while (low < high) { var mid = (low + high) >> 1; iterator(array[mid]) < iterator(obj) ? low = mid + 1 : high = mid; } return low; }; // Safely convert anything iterable into a real, live array. _.toArray = function(obj) { if (!obj) return []; if (_.isArray(obj)) return slice.call(obj); if (_.isArguments(obj)) return slice.call(obj); if (obj.toArray && _.isFunction(obj.toArray)) return obj.toArray(); return _.values(obj); }; // Return the number of elements in an object. _.size = function(obj) { return _.isArray(obj) ? obj.length : _.keys(obj).length; }; // Array Functions // --------------- // Get the first element of an array. Passing **n** will return the first N // values in the array. Aliased as `head` and `take`. The **guard** check // allows it to work with `_.map`. _.first = _.head = _.take = function(array, n, guard) { return (n != null) && !guard ? slice.call(array, 0, n) : array[0]; }; // Returns everything but the last entry of the array. Especcialy useful on // the arguments object. Passing **n** will return all the values in // the array, excluding the last N. The **guard** check allows it to work with // `_.map`. _.initial = function(array, n, guard) { return slice.call(array, 0, array.length - ((n == null) || guard ? 1 : n)); }; // Get the last element of an array. Passing **n** will return the last N // values in the array. The **guard** check allows it to work with `_.map`. _.last = function(array, n, guard) { if ((n != null) && !guard) { return slice.call(array, Math.max(array.length - n, 0)); } else { return array[array.length - 1]; } }; // Returns everything but the first entry of the array. Aliased as `tail`. // Especially useful on the arguments object. Passing an **index** will return // the rest of the values in the array from that index onward. The **guard** // check allows it to work with `_.map`. _.rest = _.tail = function(array, index, guard) { return slice.call(array, (index == null) || guard ? 1 : index); }; // Trim out all falsy values from an array. _.compact = function(array) { return _.filter(array, function(value){ return !!value; }); }; // Return a completely flattened version of an array. _.flatten = function(array, shallow) { return _.reduce(array, function(memo, value) { if (_.isArray(value)) return memo.concat(shallow ? value : _.flatten(value)); memo[memo.length] = value; return memo; }, []); }; // Return a version of the array that does not contain the specified value(s). _.without = function(array) { return _.difference(array, slice.call(arguments, 1)); }; // Produce a duplicate-free version of the array. If the array has already // been sorted, you have the option of using a faster algorithm. // Aliased as `unique`. _.uniq = _.unique = function(array, isSorted, iterator) { var initial = iterator ? _.map(array, iterator) : array; var results = []; // The `isSorted` flag is irrelevant if the array only contains two elements. if (array.length < 3) isSorted = true; _.reduce(initial, function (memo, value, index) { if (isSorted ? _.last(memo) !== value || !memo.length : !_.include(memo, value)) { memo.push(value); results.push(array[index]); } return memo; }, []); return results; }; // Produce an array that contains the union: each distinct element from all of // the passed-in arrays. _.union = function() { return _.uniq(_.flatten(arguments, true)); }; // Produce an array that contains every item shared between all the // passed-in arrays. (Aliased as "intersect" for back-compat.) _.intersection = _.intersect = function(array) { var rest = slice.call(arguments, 1); return _.filter(_.uniq(array), function(item) { return _.every(rest, function(other) { return _.indexOf(other, item) >= 0; }); }); }; // Take the difference between one array and a number of other arrays. // Only the elements present in just the first array will remain. _.difference = function(array) { var rest = _.flatten(slice.call(arguments, 1), true); return _.filter(array, function(value){ return !_.include(rest, value); }); }; // Zip together multiple lists into a single array -- elements that share // an index go together. _.zip = function() { var args = slice.call(arguments); var length = _.max(_.pluck(args, 'length')); var results = new Array(length); for (var i = 0; i < length; i++) results[i] = _.pluck(args, "" + i); return results; }; // If the browser doesn't supply us with indexOf (I'm looking at you, **MSIE**), // we need this function. Return the position of the first occurrence of an // item in an array, or -1 if the item is not included in the array. // Delegates to **ECMAScript 5**'s native `indexOf` if available. // If the array is large and already in sort order, pass `true` // for **isSorted** to use binary search. _.indexOf = function(array, item, isSorted) { if (array == null) return -1; var i, l; if (isSorted) { i = _.sortedIndex(array, item); return array[i] === item ? i : -1; } if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item); for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i; return -1; }; // Delegates to **ECMAScript 5**'s native `lastIndexOf` if available. _.lastIndexOf = function(array, item) { if (array == null) return -1; if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item); var i = array.length; while (i--) if (i in array && array[i] === item) return i; return -1; }; // Generate an integer Array containing an arithmetic progression. A port of // the native Python `range()` function. See // [the Python documentation](http://docs.python.org/library/functions.html#range). _.range = function(start, stop, step) { if (arguments.length <= 1) { stop = start || 0; start = 0; } step = arguments[2] || 1; var len = Math.max(Math.ceil((stop - start) / step), 0); var idx = 0; var range = new Array(len); while(idx < len) { range[idx++] = start; start += step; } return range; }; // Function (ahem) Functions // ------------------ // Reusable constructor function for prototype setting. var ctor = function(){}; // Create a function bound to a given object (assigning `this`, and arguments, // optionally). Binding with arguments is also known as `curry`. // Delegates to **ECMAScript 5**'s native `Function.bind` if available. // We check for `func.bind` first, to fail fast when `func` is undefined. _.bind = function bind(func, context) { var bound, args; if (func.bind === nativeBind && nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); if (!_.isFunction(func)) throw new TypeError; args = slice.call(arguments, 2); return bound = function() { if (!(this instanceof bound)) return func.apply(context, args.concat(slice.call(arguments))); ctor.prototype = func.prototype; var self = new ctor; var result = func.apply(self, args.concat(slice.call(arguments))); if (Object(result) === result) return result; return self; }; }; // Bind all of an object's methods to that object. Useful for ensuring that // all callbacks defined on an object belong to it. _.bindAll = function(obj) { var funcs = slice.call(arguments, 1); if (funcs.length == 0) funcs = _.functions(obj); each(funcs, function(f) { obj[f] = _.bind(obj[f], obj); }); return obj; }; // Memoize an expensive function by storing its results. _.memoize = function(func, hasher) { var memo = {}; hasher || (hasher = _.identity); return function() { var key = hasher.apply(this, arguments); return _.has(memo, key) ? memo[key] : (memo[key] = func.apply(this, arguments)); }; }; // Delays a function for the given number of milliseconds, and then calls // it with the arguments supplied. _.delay = function(func, wait) { var args = slice.call(arguments, 2); return setTimeout(function(){ return func.apply(null, args); }, wait); }; // Defers a function, scheduling it to run after the current call stack has // cleared. _.defer = function(func) { return _.delay.apply(_, [func, 1].concat(slice.call(arguments, 1))); }; // Returns a function, that, when invoked, will only be triggered at most once // during a given window of time. _.throttle = function(func, wait) { var context, args, timeout, throttling, more, result; var whenDone = _.debounce(function(){ more = throttling = false; }, wait); return function() { context = this; args = arguments; var later = function() { timeout = null; if (more) func.apply(context, args); whenDone(); }; if (!timeout) timeout = setTimeout(later, wait); if (throttling) { more = true; } else { result = func.apply(context, args); } whenDone(); throttling = true; return result; }; }; // Returns a function, that, as long as it continues to be invoked, will not // be triggered. The function will be called after it stops being called for // N milliseconds. If `immediate` is passed, trigger the function on the // leading edge, instead of the trailing. _.debounce = function(func, wait, immediate) { var timeout; return function() { var context = this, args = arguments; var later = function() { timeout = null; if (!immediate) func.apply(context, args); }; if (immediate && !timeout) func.apply(context, args); clearTimeout(timeout); timeout = setTimeout(later, wait); }; }; // Returns a function that will be executed at most one time, no matter how // often you call it. Useful for lazy initialization. _.once = function(func) { var ran = false, memo; return function() { if (ran) return memo; ran = true; return memo = func.apply(this, arguments); }; }; // Returns the first function passed as an argument to the second, // allowing you to adjust arguments, run code before and after, and // conditionally execute the original function. _.wrap = function(func, wrapper) { return function() { var args = [func].concat(slice.call(arguments, 0)); return wrapper.apply(this, args); }; }; // Returns a function that is the composition of a list of functions, each // consuming the return value of the function that follows. _.compose = function() { var funcs = arguments; return function() { var args = arguments; for (var i = funcs.length - 1; i >= 0; i--) { args = [funcs[i].apply(this, args)]; } return args[0]; }; }; // Returns a function that will only be executed after being called N times. _.after = function(times, func) { if (times <= 0) return func(); return function() { if (--times < 1) { return func.apply(this, arguments); } }; }; // Object Functions // ---------------- // Retrieve the names of an object's properties. // Delegates to **ECMAScript 5**'s native `Object.keys` _.keys = nativeKeys || function(obj) { if (obj !== Object(obj)) throw new TypeError('Invalid object'); var keys = []; for (var key in obj) if (_.has(obj, key)) keys[keys.length] = key; return keys; }; // Retrieve the values of an object's properties. _.values = function(obj) { return _.map(obj, _.identity); }; // Return a sorted list of the function names available on the object. // Aliased as `methods` _.functions = _.methods = function(obj) { var names = []; for (var key in obj) { if (_.isFunction(obj[key])) names.push(key); } return names.sort(); }; // Extend a given object with all the properties in passed-in object(s). _.extend = function(obj) { each(slice.call(arguments, 1), function(source) { for (var prop in source) { obj[prop] = source[prop]; } }); return obj; }; // Return a copy of the object only containing the whitelisted properties. _.pick = function(obj) { var result = {}; each(_.flatten(slice.call(arguments, 1)), function(key) { if (key in obj) result[key] = obj[key]; }); return result; }; // Fill in a given object with default properties. _.defaults = function(obj) { each(slice.call(arguments, 1), function(source) { for (var prop in source) { if (obj[prop] == null) obj[prop] = source[prop]; } }); return obj; }; // Create a (shallow-cloned) duplicate of an object. _.clone = function(obj) { if (!_.isObject(obj)) return obj; return _.isArray(obj) ? obj.slice() : _.extend({}, obj); }; // Invokes interceptor with the obj, and then returns obj. // The primary purpose of this method is to "tap into" a method chain, in // order to perform operations on intermediate results within the chain. _.tap = function(obj, interceptor) { interceptor(obj); return obj; }; // Internal recursive comparison function. function eq(a, b, stack) { // Identical objects are equal. `0 === -0`, but they aren't identical. // See the Harmony `egal` proposal: http://wiki.ecmascript.org/doku.php?id=harmony:egal. if (a === b) return a !== 0 || 1 / a == 1 / b; // A strict comparison is necessary because `null == undefined`. if (a == null || b == null) return a === b; // Unwrap any wrapped objects. if (a._chain) a = a._wrapped; if (b._chain) b = b._wrapped; // Invoke a custom `isEqual` method if one is provided. if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b); if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a); // Compare `[[Class]]` names. var className = toString.call(a); if (className != toString.call(b)) return false; switch (className) { // Strings, numbers, dates, and booleans are compared by value. case '[object String]': // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // equivalent to `new String("5")`. return a == String(b); case '[object Number]': // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for // other numeric values. return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b); case '[object Date]': case '[object Boolean]': // Coerce dates and booleans to numeric primitive values. Dates are compared by their // millisecond representations. Note that invalid dates with millisecond representations // of `NaN` are not equivalent. return +a == +b; // RegExps are compared by their source patterns and flags. case '[object RegExp]': return a.source == b.source && a.global == b.global && a.multiline == b.multiline && a.ignoreCase == b.ignoreCase; } if (typeof a != 'object' || typeof b != 'object') return false; // Assume equality for cyclic structures. The algorithm for detecting cyclic // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. var length = stack.length; while (length--) { // Linear search. Performance is inversely proportional to the number of // unique nested structures. if (stack[length] == a) return true; } // Add the first object to the stack of traversed objects. stack.push(a); var size = 0, result = true; // Recursively compare objects and arrays. if (className == '[object Array]') { // Compare array lengths to determine if a deep comparison is necessary. size = a.length; result = size == b.length; if (result) { // Deep compare the contents, ignoring non-numeric properties. while (size--) { // Ensure commutative equality for sparse arrays. if (!(result = size in a == size in b && eq(a[size], b[size], stack))) break; } } } else { // Objects with different constructors are not equivalent. if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false; // Deep compare objects. for (var key in a) { if (_.has(a, key)) { // Count the expected number of properties. size++; // Deep compare each member. if (!(result = _.has(b, key) && eq(a[key], b[key], stack))) break; } } // Ensure that both objects contain the same number of properties. if (result) { for (key in b) { if (_.has(b, key) && !(size--)) break; } result = !size; } } // Remove the first object from the stack of traversed objects. stack.pop(); return result; } // Perform a deep comparison to check if two objects are equal. _.isEqual = function(a, b) { return eq(a, b, []); }; // Is a given array, string, or object empty? // An "empty" object has no enumerable own-properties. _.isEmpty = function(obj) { if (obj == null) return true; if (_.isArray(obj) || _.isString(obj)) return obj.length === 0; for (var key in obj) if (_.has(obj, key)) return false; return true; }; // Is a given value a DOM element? _.isElement = function(obj) { return !!(obj && obj.nodeType == 1); }; // Is a given value an array? // Delegates to ECMA5's native Array.isArray _.isArray = nativeIsArray || function(obj) { return toString.call(obj) == '[object Array]'; }; // Is a given variable an object? _.isObject = function(obj) { return obj === Object(obj); }; // Is a given variable an arguments object? _.isArguments = function(obj) { return toString.call(obj) == '[object Arguments]'; }; if (!_.isArguments(arguments)) { _.isArguments = function(obj) { return !!(obj && _.has(obj, 'callee')); }; } // Is a given value a function? _.isFunction = function(obj) { return toString.call(obj) == '[object Function]'; }; // Is a given value a string? _.isString = function(obj) { return toString.call(obj) == '[object String]'; }; // Is a given value a number? _.isNumber = function(obj) { return toString.call(obj) == '[object Number]'; }; // Is a given object a finite number? _.isFinite = function(obj) { return _.isNumber(obj) && isFinite(obj); }; // Is the given value `NaN`? _.isNaN = function(obj) { // `NaN` is the only value for which `===` is not reflexive. return obj !== obj; }; // Is a given value a boolean? _.isBoolean = function(obj) { return obj === true || obj === false || toString.call(obj) == '[object Boolean]'; }; // Is a given value a date? _.isDate = function(obj) { return toString.call(obj) == '[object Date]'; }; // Is the given value a regular expression? _.isRegExp = function(obj) { return toString.call(obj) == '[object RegExp]'; }; // Is a given value equal to null? _.isNull = function(obj) { return obj === null; }; // Is a given variable undefined? _.isUndefined = function(obj) { return obj === void 0; }; // Has own property? _.has = function(obj, key) { return hasOwnProperty.call(obj, key); }; // Utility Functions // ----------------- // Run Underscore.js in *noConflict* mode, returning the `_` variable to its // previous owner. Returns a reference to the Underscore object. _.noConflict = function() { root._ = previousUnderscore; return this; }; // Keep the identity function around for default iterators. _.identity = function(value) { return value; }; // Run a function **n** times. _.times = function (n, iterator, context) { for (var i = 0; i < n; i++) iterator.call(context, i); }; // Escape a string for HTML interpolation. _.escape = function(string) { return (''+string).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, ''').replace(/\//g,'/'); }; // If the value of the named property is a function then invoke it; // otherwise, return it. _.result = function(object, property) { if (object == null) return null; var value = object[property]; return _.isFunction(value) ? value.call(object) : value; }; // Add your own custom functions to the Underscore object, ensuring that // they're correctly added to the OOP wrapper as well. _.mixin = function(obj) { each(_.functions(obj), function(name){ addToWrapper(name, _[name] = obj[name]); }); }; // Generate a unique integer id (unique within the entire client session). // Useful for temporary DOM ids. var idCounter = 0; _.uniqueId = function(prefix) { var id = idCounter++; return prefix ? prefix + id : id; }; // By default, Underscore uses ERB-style template delimiters, change the // following template settings to use alternative delimiters. _.templateSettings = { evaluate : /<%([\s\S]+?)%>/g, interpolate : /<%=([\s\S]+?)%>/g, escape : /<%-([\s\S]+?)%>/g }; // When customizing `templateSettings`, if you don't want to define an // interpolation, evaluation or escaping regex, we need one that is // guaranteed not to match. var noMatch = /.^/; // Certain characters need to be escaped so that they can be put into a // string literal. var escapes = { '\\': '\\', "'": "'", 'r': '\r', 'n': '\n', 't': '\t', 'u2028': '\u2028', 'u2029': '\u2029' }; for (var p in escapes) escapes[escapes[p]] = p; var escaper = /\\|'|\r|\n|\t|\u2028|\u2029/g; var unescaper = /\\(\\|'|r|n|t|u2028|u2029)/g; // Within an interpolation, evaluation, or escaping, remove HTML escaping // that had been previously added. var unescape = function(code) { return code.replace(unescaper, function(match, escape) { return escapes[escape]; }); }; // JavaScript micro-templating, similar to John Resig's implementation. // Underscore templating handles arbitrary delimiters, preserves whitespace, // and correctly escapes quotes within interpolated code. _.template = function(text, data, settings) { settings = _.defaults(settings || {}, _.templateSettings); // Compile the template source, taking care to escape characters that // cannot be included in a string literal and then unescape them in code // blocks. var source = "__p+='" + text .replace(escaper, function(match) { return '\\' + escapes[match]; }) .replace(settings.escape || noMatch, function(match, code) { return "'+\n_.escape(" + unescape(code) + ")+\n'"; }) .replace(settings.interpolate || noMatch, function(match, code) { return "'+\n(" + unescape(code) + ")+\n'"; }) .replace(settings.evaluate || noMatch, function(match, code) { return "';\n" + unescape(code) + "\n;__p+='"; }) + "';\n"; // If a variable is not specified, place data values in local scope. if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; source = "var __p='';" + "var print=function(){__p+=Array.prototype.join.call(arguments, '')};\n" + source + "return __p;\n"; var render = new Function(settings.variable || 'obj', '_', source); if (data) return render(data, _); var template = function(data) { return render.call(this, data, _); }; // Provide the compiled function source as a convenience for build time // precompilation. template.source = 'function(' + (settings.variable || 'obj') + '){\n' + source + '}'; return template; }; // Add a "chain" function, which will delegate to the wrapper. _.chain = function(obj) { return _(obj).chain(); }; // The OOP Wrapper // --------------- // If Underscore is called as a function, it returns a wrapped object that // can be used OO-style. This wrapper holds altered versions of all the // underscore functions. Wrapped objects may be chained. var wrapper = function(obj) { this._wrapped = obj; }; // Expose `wrapper.prototype` as `_.prototype` _.prototype = wrapper.prototype; // Helper function to continue chaining intermediate results. var result = function(obj, chain) { return chain ? _(obj).chain() : obj; }; // A method to easily add functions to the OOP wrapper. var addToWrapper = function(name, func) { wrapper.prototype[name] = function() { var args = slice.call(arguments); unshift.call(args, this._wrapped); return result(func.apply(_, args), this._chain); }; }; // Add all of the Underscore functions to the wrapper object. _.mixin(_); // Add all mutator Array functions to the wrapper. each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { var method = ArrayProto[name]; wrapper.prototype[name] = function() { var wrapped = this._wrapped; method.apply(wrapped, arguments); var length = wrapped.length; if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0]; return result(wrapped, this._chain); }; }); // Add all accessor Array functions to the wrapper. each(['concat', 'join', 'slice'], function(name) { var method = ArrayProto[name]; wrapper.prototype[name] = function() { return result(method.apply(this._wrapped, arguments), this._chain); }; }); // Start chaining a wrapped Underscore object. wrapper.prototype.chain = function() { this._chain = true; return this; }; // Extracts the result from a wrapped and chained object. wrapper.prototype.value = function() { return this._wrapped; }; return _; }).call({}); /** * Core Emmet object, available in global scope */ var emmet = (function(global) { var defaultSyntax = 'html'; var defaultProfile = 'plain'; if (typeof _ == 'undefined') { try { // avoid collisions with RequireJS loader // also, JS obfuscators tends to translate // a["name"] to a.name, which also breaks RequireJS _ = global[['require'][0]]('underscore'); // node.js } catch (e) {} } if (typeof _ == 'undefined') { throw 'Cannot access to Underscore.js lib'; } /** List of registered modules */ var modules = { _ : _ }; /** * Shared empty constructor function to aid in prototype-chain creation. */ var ctor = function(){}; /** * Helper function to correctly set up the prototype chain, for subclasses. * Similar to `goog.inherits`, but uses a hash of prototype properties and * class properties to be extended. * Took it from Backbone. * @param {Object} parent * @param {Object} protoProps * @param {Object} staticProps * @returns {Object} */ function inherits(parent, protoProps, staticProps) { var child; // The constructor function for the new subclass is either defined by // you (the "constructor" property in your `extend` definition), or // defaulted by us to simply call the parent's constructor. if (protoProps && protoProps.hasOwnProperty('constructor')) { child = protoProps.constructor; } else { child = function() { parent.apply(this, arguments); }; } // Inherit class (static) properties from parent. _.extend(child, parent); // Set the prototype chain to inherit from `parent`, without calling // `parent`'s constructor function. ctor.prototype = parent.prototype; child.prototype = new ctor(); // Add prototype properties (instance properties) to the subclass, // if supplied. if (protoProps) _.extend(child.prototype, protoProps); // Add static properties to the constructor function, if supplied. if (staticProps) _.extend(child, staticProps); // Correctly set child's `prototype.constructor`. child.prototype.constructor = child; // Set a convenience property in case the parent's prototype is needed // later. child.__super__ = parent.prototype; return child; }; /** * @type Function Function that loads module definition if it's not defined */ var moduleLoader = null; /** * Generic Emmet module loader (actually, it doesn’t load anything, just * returns module reference). Not using `require` name to avoid conflicts * with Node.js and RequireJS */ function r(name) { if (!(name in modules) && moduleLoader) moduleLoader(name); return modules[name]; } return { /** * Simple, AMD-like module definition. The module will be added into * 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; } // export as Require.js module if (typeof define !== 'undefined') { define(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); }); return this; }, /** * 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, maxCount; i >= 0; i--) { child = node.children[i]; if (child.isRepeating()) { maxCount = j = child.repeatCount; child.repeatCount = 1; child.updateProperty('counter', 1); child.updateProperty('maxCount', maxCount); while (--j > 0) { child.parent.addChild(child.clone(), i + 1) .updateProperty('counter', j + 1) .updateProperty('maxCount', maxCount); } } } // 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, node.maxCount); }); 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 * Special thanks to all of the contributors listed here https://github.com/joshaven/string_score * MIT license: http://www.opensource.org/licenses/mit-license.php * * Date: Tue Mar 1 2011 */ /** * Scores a string against another string. * 'Hello World'.score('he'); //=> 0.5931818181818181 * 'Hello World'.score('Hello'); //=> 0.7318181818181818 */ emmet.define('string-score', function(require, _) { return { score: function(string, abbreviation, fuzziness) { // If the string is equal to the abbreviation, perfect match. if (string == abbreviation) {return 1;} //if it's not a perfect match and is empty return 0 if(abbreviation == "") {return 0;} var total_character_score = 0, abbreviation_length = abbreviation.length, string_length = string.length, start_of_string_bonus, abbreviation_score, fuzzies=1, final_score; // Walk through abbreviation and add up scores. for (var i = 0, character_score/* = 0*/, index_in_string/* = 0*/, c/* = ''*/, index_c_lowercase/* = 0*/, index_c_uppercase/* = 0*/, min_index/* = 0*/; i < abbreviation_length; ++i) { // Find the first case-insensitive match of a character. c = abbreviation.charAt(i); index_c_lowercase = string.indexOf(c.toLowerCase()); index_c_uppercase = string.indexOf(c.toUpperCase()); min_index = Math.min(index_c_lowercase, index_c_uppercase); index_in_string = (min_index > -1) ? min_index : Math.max(index_c_lowercase, index_c_uppercase); if (index_in_string === -1) { if (fuzziness) { fuzzies += 1-fuzziness; continue; } else { return 0; } } else { character_score = 0.1; } // Set base score for matching 'c'. // Same case bonus. if (string[index_in_string] === c) { character_score += 0.1; } // Consecutive letter & start-of-string Bonus if (index_in_string === 0) { // Increase the score when matching first character of the remainder of the string character_score += 0.6; if (i === 0) { // If match is the first character of the string // & the first character of abbreviation, add a // start-of-string match bonus. start_of_string_bonus = 1; //true; } } else { // Acronym Bonus // Weighing Logic: Typing the first character of an acronym is as if you // preceded it with two perfect character matches. if (string.charAt(index_in_string - 1) === ' ') { character_score += 0.8; // * Math.min(index_in_string, 5); // Cap bonus at 0.4 * 5 } } // Left trim the already matched part of the string // (forces sequential matching). string = string.substring(index_in_string + 1, string_length); total_character_score += character_score; } // end of for loop // Uncomment to weigh smaller words higher. // return total_character_score / string_length; abbreviation_score = total_character_score / abbreviation_length; //percentage_of_matched_string = abbreviation_length / string_length; //word_score = abbreviation_score * percentage_of_matched_string; // Reduce penalty for longer strings. //final_score = (word_score + abbreviation_score) / 2; final_score = ((abbreviation_score * (abbreviation_length / string_length)) + abbreviation_score) / 2; final_score = final_score / fuzzies; if (start_of_string_bonus && (final_score + 0.15 < 1)) { final_score += 0.15; } return final_score; } }; });/** * Utility module for Emmet * @param {Function} require * @param {Underscore} _ */ emmet.define('utils', function(require, _) { /** * Special token used as a placeholder for caret positions inside * generated output */ var caretPlaceholder = '${0}'; /** * A simple string builder, optimized for faster text concatenation * @param {String} value Initial value */ function StringBuilder(value) { this._data = []; this.length = 0; if (value) this.append(value); } StringBuilder.prototype = { /** * Append string * @param {String} text */ append: function(text) { this._data.push(text); this.length += text.length; }, /** * @returns {String} */ toString: function() { return this._data.join(''); }, /** * @returns {String} */ valueOf: function() { return this.toString(); } }; return { /** @memberOf utils */ reTag: /<\/?[\w:\-]+(?:\s+[\w\-:]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*\s*(\/?)>$/, /** * Test if passed string ends with XHTML tag. This method is used for testing * '>' character: it belongs to tag or it's a part of abbreviation? * @param {String} str * @return {Boolean} */ endsWithTag: function(str) { return this.reTag.test(str); }, /** * Check if passed symbol is a number * @param {String} ch * @returns {Boolean} */ isNumeric: function(ch) { if (typeof(ch) == 'string') ch = ch.charCodeAt(0); return (ch && ch > 47 && ch < 58); }, /** * Trim whitespace from string * @param {String} text * @return {String} */ trim: function(text) { return (text || "").replace(/^\s+|\s+$/g, ""); }, /** * Returns newline character * @returns {String} */ getNewline: function() { var res = require('resources'); if (!res) { return '\n'; } var nl = res.getVariable('newline'); return _.isString(nl) ? nl : '\n'; }, /** * Sets new newline character that will be used in output * @param {String} str */ setNewline: function(str) { var res = require('resources'); res.setVariable('newline', str); res.setVariable('nl', str); }, /** * Split text into lines. Set 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(''); }, /** * Returns list of paddings that should be used to align passed string * @param {Array} strings * @returns {Array} */ getStringsPads: function(strings) { var lengths = _.map(strings, function(s) { return _.isString(s) ? s.length : +s; }); var max = _.max(lengths); return _.map(lengths, function(l) { var pad = max - l; return pad ? this.repeatString(' ', pad) : ''; }, this); }, /** * 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 character should be replaced * @param {String} value New value * @return {String} */ replaceCounter: function(str, value, total) { var symbol = '$'; // in case we received strings from Java, convert the to native strings str = String(str); value = String(value); if (/^\-?\d+$/.test(value)) { value = +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++; var pad = j - pos; // get counter base var base = 0, decrement = false, m; if (m = str.substr(j).match(/^@(\-?)(\d*)/)) { j += m[0].length; if (m[1]) { decrement = true; } base = parseInt(m[2] || 1) - 1; } if (decrement && total && _.isNumber(value)) { value = total - value + 1; } value += base; return [str.substring(pos, j), that.zeroPadString(value + '', pad)]; }); }, /** * 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:

* order : (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 {AbbreviationNode} 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:
* label : (String) – Human-readable action name. * May contain '/' symbols as submenu separators
* hidden : (Boolean) – 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.
or
self_closing_tag: 'xhtml', // Profile-level output filters, re-defines syntax filters filters: '', // Additional filters applied to abbreviation. // Unlike "filters", this preference doesn't override default filters // but add the instead every time given profile is chosen extraFilters: '' }; /** * @constructor * @type OutputProfile * @param {Object} options */ function OutputProfile(options) { _.extend(this, defaultProfile, options); } OutputProfile.prototype = { /** * Transforms tag name case depending on current profile settings * @param {String} name String to transform * @returns {String} */ tagName: function(name) { return stringCase(name, this.tag_case); }, /** * Transforms attribute name case depending on current profile settings * @param {String} name String to transform * @returns {String} */ attributeName: function(name) { return stringCase(name, this.attr_case); }, /** * Returns quote character for current profile * @returns {String} */ attributeQuote: function() { return this.attr_quotes == 'single' ? "'" : '"'; }, /** * Returns self-closing tag symbol for current profile * @param {String} param * @returns {String} */ selfClosing: function(param) { if (this.self_closing_tag == 'xhtml') return ' /'; if (this.self_closing_tag === true) return '/'; return ''; }, /** * Returns cursor token based on current profile settings * @returns {String} */ cursor: function() { return this.place_cursor ? require('utils').getCaretPlaceholder() : ''; } }; /** * 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} */ 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) */ emmet.define('editorUtils', function(require, _) { return { /** * Check if cursor is placed inside XHTML tag * @param {String} html Contents of the document * @param {Number} caretPos Current caret position inside tag * @return {Boolean} */ isInsideTag: function(html, caretPos) { var reTag = /^<\/?\w[\w\:\-]*.*?>/; // search left to find opening brace var pos = caretPos; while (pos > -1) { if (html.charAt(pos) == '<') break; pos--; } if (pos != -1) { var m = reTag.exec(html.substring(pos)); if (m && caretPos > pos && caretPos < pos + m[0].length) return true; } return false; }, /** * Sanitizes incoming editor data and provides default values for * output-specific info * @param {IEmmetEditor} editor * @param {String} syntax * @param {String} profile */ outputInfo: function(editor, syntax, profile) { // most of this code makes sense for Java/Rhino environment // because string that comes from Java are not actually JS string // but Java String object so the have to be explicitly converted // to native string profile = profile || editor.getProfileName(); return { /** @memberOf outputInfo */ syntax: String(syntax || editor.getSyntax()), profile: profile ? String(profile) : null, content: String(editor.getContent()) }; }, /** * Unindent content, thus preparing text for tag wrapping * @param {IEmmetEditor} editor Editor instance * @param {String} text * @return {String} */ unindent: function(editor, text) { return require('utils').unindentString(text, this.getCurrentLinePadding(editor)); }, /** * Returns padding of current editor's line * @param {IEmmetEditor} Editor instance * @return {String} */ getCurrentLinePadding: function(editor) { return require('utils').getLinePadding(editor.getCurrentLine()); } }; }); /** * Utility methods for Emmet actions * @param {Function} require * @param {Underscore} _ * @author Sergey Chikuyonok (serge.che@gmail.com) */ emmet.define('actionUtils', function(require, _) { return { mimeTypes: { 'gif' : 'image/gif', 'png' : 'image/png', 'jpg' : 'image/jpeg', 'jpeg': 'image/jpeg', 'svg' : 'image/svg+xml', 'html': 'text/html', 'htm' : 'text/html' }, /** * Extracts abbreviations from text stream, starting from the end * @param {String} str * @return {String} Abbreviation or empty string * @memberOf emmet.actionUtils */ extractAbbreviation: function(str) { var curOffset = str.length; var startIndex = -1; var groupCount = 0; var braceCount = 0; var textCount = 0; var utils = require('utils'); var parser = require('abbreviationParser'); while (true) { curOffset--; if (curOffset < 0) { // moved to the beginning of the line startIndex = 0; break; } var ch = str.charAt(curOffset); if (ch == ']') { braceCount++; } else if (ch == '[') { if (!braceCount) { // unexpected brace startIndex = curOffset + 1; break; } braceCount--; } else if (ch == '}') { textCount++; } else if (ch == '{') { if (!textCount) { // unexpected brace startIndex = curOffset + 1; break; } textCount--; } else if (ch == ')') { groupCount++; } else if (ch == '(') { if (!groupCount) { // unexpected brace startIndex = curOffset + 1; break; } groupCount--; } else { if (braceCount || textCount) // respect all characters inside attribute sets or text nodes continue; else if (!parser.isAllowedChar(ch) || (ch == '>' && utils.endsWithTag(str.substring(0, curOffset + 1)))) { // found stop symbol startIndex = curOffset + 1; break; } } } if (startIndex != -1 && !textCount && !braceCount && !groupCount) // found something, remove some invalid symbols from the // beginning and return abbreviation return str.substring(startIndex).replace(/^[\*\+\>\^]+/, ''); else return ''; }, /** * Gets image size from image byte stream. * @author http://romeda.org/rePublish/ * @param {String} stream Image byte stream (use 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) { if (node.children.length || node._text || this.isSnippet(node)) { return false; } var r = node.matchedResource(); return r && r.is_empty; }, /** * 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) { if (open.range.cmp(pos, 'lt', 'gt')) { // inside self-closing tag, found match break; } // outside self-closing tag, continue continue; } 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
break; } open = null; } else if (matches(text, i, '-->')) { // skip back to comment start for (var j = i - 1; j >= 0; j--) { if (matches(text, j, '-->')) { // found another comment end, do nothing break; } else if (matches(text, j, '')) { j += 3; break; } } open = comment(i, j); break; } } if (open) { var outerRange = null; var innerRange = null; if (close) { outerRange = range.create2(open.range.start, close.range.end); innerRange = range.create2(open.range.end, close.range.start); } else { outerRange = innerRange = range.create2(open.range.start, open.range.end); } if (open.type == 'comment') { // adjust positions of inner range for comment var _c = outerRange.substring(text); innerRange.start += _c.length - _c.replace(/^<\!--\s*/, '').length; innerRange.end -= _c.length - _c.replace(/\s*-->$/, '').length; } return { open: open, close: close, type: open.type == 'comment' ? 'comment' : 'tag', innerRange: innerRange, innerContent: function() { return this.innerRange.substring(text); }, outerRange: outerRange, outerContent: function() { return this.outerRange.substring(text); }, range: !innerRange.length() || !innerRange.cmp(pos, 'lte', 'gte') ? outerRange : innerRange, content: function() { return this.range.substring(text); }, source: text }; } }, /** * The same as 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:
* * replaceCarets : Boolean — replace all default * caret placeholders (like {%::emmet-caret::%}) with ${0:caret}
* * escape : 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. '\\$')
* * tabstop : 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) && attr !== str) { return attr; } var varValue = res.getVariable(varName); if (varValue) return varValue; // output as placeholder if (!placeholderMemo[varName]) placeholderMemo[varName] = startPlaceholderNum++; return '${' + placeholderMemo[varName] + ':' + varName + '}'; }; }, /** * 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.

* * This module can also be used to list all available properties to create * UI for updating properties * * @memberOf __preferencesDefine * @constructor * @param {Function} require * @param {Underscore} _ */ emmet.define('preferences', function(require, _) { var preferences = {}; var defaults = {}; var _dbgDefaults = null; var _dbgPreferences = null; function toBoolean(val) { if (_.isString(val)) { val = val.toLowerCase(); return val == 'yes' || val == 'true' || val == '1'; } return !!val; } function isValueObj(obj) { return _.isObject(obj) && 'value' in obj && _.keys(obj).length < 3; } return { /** * Creates new preference item with default value * @param {String} name Preference name. You can also pass object * with many options * @param {Object} value Preference default value * @param {String} description Item textual description * @memberOf preferences */ define: function(name, value, description) { var prefs = name; if (_.isString(name)) { prefs = {}; prefs[name] = { value: value, description: description }; } _.each(prefs, function(v, k) { defaults[k] = isValueObj(v) ? v : {value: v}; }); }, /** * Updates preference item value. Preference value should be defined * first with 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 if (v !== null) { 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 === null || val === '') { return null; } val = _.map(val.split(','), require('utils').trim); if (!val.length) { return 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) */ emmet.define('filters', function(require, _) { /** List of registered filters */ var registeredFilters = {}; /** Filters that will be applied for unknown syntax */ var basicFilters = 'html'; function list(filters) { if (!filters) return []; if (_.isString(filters)) return filters.split(/[\|,]/g); return filters; } return { /** * Register new filter * @param {String} name Filter name * @param {Function} fn Filter function */ add: function(name, fn) { registeredFilters[name] = fn; }, /** * Apply filters for final output tree * @param {AbbreviationNode} tree Output tree * @param {Array} filters List of filters to apply. Might be a * 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. *

* For example, developer can create an edit tree from CSS rule and add or * remove properties from it–all changes will be immediately reflected in the * original source. *

* All classes defined in this module should be extended the same way as in * Backbone framework: using 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. *

* This behavior can be overridden with custom handlers which can perform * different actions when 'Expand Abbreviation' action is called. * For example, a CSS gradient handler that produces vendor-prefixed gradient * definitions registers its own expand abbreviation handler. * * @constructor * @memberOf __expandAbbreviationActionDefine * @param {Function} require * @param {Underscore} _ */ emmet.define('expandAbbreviation', function(require, _) { /** * @type HandlerList List of registered handlers */ var handlers = require('handlerList').create(); /** Back-reference to module */ var module = null; var actions = require('actions'); /** * 'Expand abbreviation' editor action * @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 */ 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:
* <div></div> → <div /> : join
* <div /> → <div></div> : split * @param {Function} require * @param {Underscore} _ * @memberOf __splitJoinTagAction * @constructor */ emmet.exec(function(require, _) { /** * @param {IEmmetEditor} editor * @param {Object} profile * @param {Object} tag */ function joinTag(editor, profile, tag) { /** @type emmet.utils */ var utils = require('utils'); // empty closing slash is a nonsense for this action var slash = profile.selfClosing() || ' /'; var content = tag.open.range.substring(tag.source).replace(/\s*>$/, slash + '>'); var caretPos = editor.getCaretPos(); // update caret position if (content.length + tag.outerRange.start < caretPos) { caretPos = content.length + tag.outerRange.start; } content = utils.escapeText(content); editor.replaceContent(content, tag.outerRange.start, tag.outerRange.end); editor.setCaretPos(caretPos); return true; } function splitTag(editor, profile, tag) { /** @type emmet.utils */ var utils = require('utils'); var nl = utils.getNewline(); var pad = require('resources').getVariable('indentation'); var caretPos = editor.getCaretPos(); // define tag content depending on profile var tagContent = (profile.tag_nl === true) ? nl + pad + nl : ''; var content = tag.outerContent().replace(/\s*\/>$/, '>'); caretPos = tag.outerRange.start + content.length; content += tagContent + ''; content = utils.escapeText(content); editor.replaceContent(content, tag.outerRange.start, tag.outerRange.end); editor.setCaretPos(caretPos); return true; } require('actions').add('split_join_tag', function(editor, profileName) { var matcher = require('htmlMatcher'); var info = require('editorUtils').outputInfo(editor, null, profileName); var profile = require('profile').get(info.profile); // find tag at current position var tag = matcher.tag(info.content, editor.getCaretPos()); if (tag) { return tag.close ? joinTag(editor, profile, tag) : splitTag(editor, profile, tag); } return false; }, {label: 'HTML/Split\\Join Tag Declaration'}); });/** * Reflect CSS value: takes rule's value under caret and pastes it for the same * rules with vendor prefixes * @constructor * @memberOf __reflectCSSActionDefine * @param {Function} require * @param {Underscore} _ */ emmet.define('reflectCSSValue', function(require, _) { /** * @type HandlerList List of registered handlers */ var handlers = require('handlerList').create(); require('actions').add('reflect_css_value', function(editor) { if (editor.getSyntax() != 'css') return false; return require('actionUtils').compoundUpdate(editor, doCSSReflection(editor)); }, {label: 'CSS/Reflect Value'}); function doCSSReflection(editor) { /** @type emmet.cssEditTree */ var cssEditTree = require('cssEditTree'); var outputInfo = require('editorUtils').outputInfo(editor); var caretPos = editor.getCaretPos(); var cssRule = cssEditTree.parseFromPosition(outputInfo.content, caretPos); if (!cssRule) return; var property = cssRule.itemFromPosition(caretPos, true); // no property under cursor, nothing to reflect if (!property) return; var oldRule = cssRule.source; var offset = cssRule.options.offset; var caretDelta = caretPos - offset - property.range().start; handlers.exec(false, [property]); if (oldRule !== cssRule.source) { return { data: cssRule.source, start: offset, end: offset + oldRule.length, caret: offset + property.range().start + caretDelta }; } } /** * Returns regexp that should match reflected CSS property names * @param {String} name Current CSS property name * @return {RegExp} */ function getReflectedCSSName(name) { name = require('cssEditTree').baseName(name); var vendorPrefix = '^(?:\\-\\w+\\-)?', m; if (name == 'opacity' || name == 'filter') { return new RegExp(vendorPrefix + '(?:opacity|filter)$'); } else if (m = name.match(/^border-radius-(top|bottom)(left|right)/)) { // Mozilla-style border radius return new RegExp(vendorPrefix + '(?:' + name + '|border-' + m[1] + '-' + m[2] + '-radius)$'); } else if (m = name.match(/^border-(top|bottom)-(left|right)-radius/)) { return new RegExp(vendorPrefix + '(?:' + name + '|border-radius-' + m[1] + m[2] + ')$'); } return new RegExp(vendorPrefix + name + '$'); } /** * Reflects value from 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);

* * This function does value conversion between different CSS properties * * @param {String} curName Current CSS property name * @param {String} curValue Current CSS property value * @param {String} refName Receiver CSS property's name * @param {String} refValue Receiver CSS property's value * @return {String} New value for receiver property */ function getReflectedValue(curName, curValue, refName, refValue) { var cssEditTree = require('cssEditTree'); var utils = require('utils'); curName = cssEditTree.baseName(curName); refName = cssEditTree.baseName(refName); if (curName == 'opacity' && refName == 'filter') { return refValue.replace(/opacity=[^)]*/i, 'opacity=' + Math.floor(parseFloat(curValue) * 100)); } else if (curName == 'filter' && refName == 'opacity') { var m = curValue.match(/opacity=([^)]*)/i); return m ? utils.prettifyNumber(parseInt(m[1]) / 100) : refValue; } return curValue; } // XXX add default handler handlers.add(function(property) { var reName = getReflectedCSSName(property.name()); _.each(property.parent.list(), function(p) { if (reName.test(p.name())) { reflectValue(property, p); } }); }, {order: -1}); return { /** * Adds custom reflect handler. The passed function will receive matched * CSS property (as 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'; } file.read(realImgPath, function(err, content) { if (err) { throw 'Unable to read ' + realImgPath + ': ' + err; } var b64 = require('base64').encode(String(content)); 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') { getImageSizeForSource(editor, xmlElem.value('src'), function(size) { if (size) { var compoundData = xmlElem.range(true); xmlElem.value('width', size.width); xmlElem.value('height', size.height, xmlElem.indexOf('width') + 1); require('actionUtils').compoundUpdate(editor, _.extend(compoundData, { data: xmlElem.toString(), caret: offset })); } }); } } /** * 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() || ''))) { getImageSizeForSource(editor, m[2], function(size) { if (size) { var compoundData = cssRule.range(true); cssRule.value('width', size.width + 'px'); cssRule.value('height', size.height + 'px', cssRule.indexOf('width') + 1); require('actionUtils').compoundUpdate(editor, _.extend(compoundData, { data: cssRule.toString(), caret: offset })); } }); } } } /** * Returns image dimensions for source * @param {IEmmetEditor} editor * @param {String} src Image source (path or data:url) */ function getImageSizeForSource(editor, src, callback) { var fileContent; var au = require('actionUtils'); if (src) { // check if it is data:url if (/^data:/.test(src)) { fileContent = require('base64').decode( src.replace(/^data\:.+?;.+?,/, '') ); return callback(au.getImageSize(fileContent)); } var file = require('file'); var absPath = file.locateFile(editor.getFilePath(), src); if (absPath === null) { throw "Can't find " + src + ' file'; } file.read(absPath, function(err, content) { if (err) { throw 'Unable to read ' + absPath + ': ' + err; } content = String(content); callback(au.getImageSize(content)); }); } } require('actions').add('update_image_size', function(editor) { // 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()))) { updateImageSizeCSS(editor); } else { updateImageSizeHTML(editor); } return true; }); });/** * Resolver for fast CSS typing. Handles abbreviations with the following * notation:
* * (-vendor prefix)?property(value)*(!)? * *

* Abbreviation handling
* * By default, Emmet searches for matching snippet definition for provided abbreviation. * If snippet wasn't found, Emmet automatically generates element with * abbreviation's name. For example, foo abbreviation will generate * <foo></foo> output. *

* This module will capture all expanded properties and upgrade them with values, * vendor prefixes and !important declarations. All unmatched abbreviations will * be automatically transformed into property-name: ${1} snippets. * * Vendor prefixes
* * If CSS-property is preceded with dash, resolver should output property with * all known vendor prefixes. For example, if brad * 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.

* * Users can also provide an explicit list of one-character prefixes for any * CSS property. For example, -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.
' + 'For example, to add foo property and remove border-radius one, ' + 'the preference value will look like this: 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, t:transparent', '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.'); prefs.define('css.alignVendor', false, 'If set to true, all generated vendor-prefixed properties ' + 'will be aligned by real property name.'); 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]/.test(value)) { value = value.replace(/^\-+/, ''); } if (value.charAt(0) == '#') { return normalizeHexColor(value); } return getKeyword(value); } function normalizeHexColor(value) { var hex = value.replace(/^#+/, '') || '0'; if (hex.toLowerCase() == 't') { return 'transparent'; } 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)); } /** * 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(/^t|[0-9a-f]+/i, 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); if (!snippet) { snippet = resources.findSnippet(syntax, abbrData.property); } else { abbrData.values = null; } 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 = this.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; var names = [], propName; _.each(prefixes, function(p) { if (p in vendorPrefixes) { propName = vendorPrefixes[p].transformName(snippetObj.name); names.push(propName); result.push(transformSnippet(propName + ':' + snippetObj.value, isImportant, syntax)); } }); // put the original property result.push(transformSnippet(snippetObj.name + ':' + snippetObj.value, isImportant, syntax)); names.push(snippetObj.name); if (prefs.get('css.alignVendor')) { var pads = require('utils').getStringsPads(names); result = _.map(result, function(prop, i) { return pads[i] + prop; }); } 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); }, /** * Split snippet into a CSS property-value pair * @param {String} snippet */ splitSnippet: function(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') }; }, 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; } /** * Resolves property name (abbreviation): searches for snippet definition in * 'resources' and returns new name of matched property */ function resolvePropertyName(name, syntax) { var res = require('resources'); var prefs = require('preferences'); var snippet = res.findSnippet(syntax, name); if (!snippet && prefs.get('css.fuzzySearch')) { snippet = res.fuzzyFindSnippet(syntax, name, parseFloat(prefs.get('css.fuzzySearchMinScore'))); } if (snippet) { if (!_.isString(snippet)) { snippet = snippet.data; } return require('cssResolver').splitSnippet(snippet).name; } } /** * 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 = prefixes ? _.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'); var alignVendor = require('preferences').get('css.alignVendor'); // we may have aligned gradient definitions: find the smallest value // separator var sep = property.styleSeparator; var before = property.styleBefore; // 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())) { if (item.styleSeparator.length < sep.length) { sep = item.styleSeparator; } if (item.styleBefore.length < before.length) { before = item.styleBefore; } rule.remove(item); } }); if (alignVendor) { // update prefix if (before != property.styleBefore) { var fullRange = property.fullRange(); rule._updateSource(before, fullRange.start, fullRange.start + property.styleBefore.length); property.styleBefore = before; } // update separator value if (sep != property.styleSeparator) { rule._updateSource(sep, property.nameRange().end, property.valueRange().start); property.styleSeparator = sep; } } 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()); // align prefixed values if (alignVendor) { var values = _.pluck(propsToInsert, 'value'); var names = _.pluck(propsToInsert, 'name'); values.push(property.value()); names.push(property.name()); var valuePads = utils.getStringsPads(_.map(values, function(v) { return v.substring(0, v.indexOf('(')); })); var namePads = utils.getStringsPads(names); property.name(_.last(namePads) + property.name()); _.each(propsToInsert, function(prop, i) { prop.name = namePads[i] + prop.name; prop.value = valuePads[i] + prop.value; }); property.value(_.last(valuePads) + property.value()); } // 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); if (require('preferences').get('css.alignVendor')) { var pads = require('utils').getStringsPads(_.map(props, function(prop) { return prop.value.substring(0, prop.value.indexOf('(')); })); _.each(props, function(prop, i) { prop.value = pads[i] + prop.value; }); } 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(';'); // resolve CSS property name var resolvedName = resolvePropertyName(css.property.name(), syntax); if (resolvedName) { css.property.name(resolvedName); } 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(','), empty: [], 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 *

* It does several things:
*
    *
  • Expands complex class name (according to BEM symbol semantics): * .block__elem_modifier → .block.block__elem.block__elem_modifier *
  • *
  • Inherits block name on child elements: * .b-block > .__el > .__el → .b-block > .b-block__el > .b-block__el__el *
  • *
  • Treats first dash symbol as '__'
  • *
  • Double underscore (or typographic '–') is also treated as an element * level lookup, e.g. ____el will search for element definition in parent’s * parent element: * .b-block > .__el1 > .____el2 → .b-block > .b-block__el1 > .b-block__el2 *
  • *
* * @param {AbbreviationNode} tree * @param {Object} profile */ function process(tree, profile) { if (tree.name) bemParse(tree, profile); var abbrUtils = require('abbreviationUtils'); _.each(tree.children, function(item) { process(item, profile); if (!abbrUtils.isSnippet(item) && item.start) shouldRunHtmlFilter = true; }); return tree; }; require('filters').add('bem', function(tree, profile) { shouldRunHtmlFilter = false; tree = process(tree, profile); // in case 'bem' filter is applied after 'html' filter: run it again // to update output if (shouldRunHtmlFilter) { tree = require('filters').apply(tree, 'html', profile); } return tree; }); }); /** * Comment important tags (with 'id' and 'class' attributes) * @author Sergey Chikuyonok (serge.che@gmail.com) * @link http://chikuyonok.ru * @constructor * @memberOf __commentFilterDefine * @param {Function} require * @param {Underscore} _ */ emmet.exec(function(require, _) { // define some preferences /** @type emmet.preferences */ var prefs = require('preferences'); prefs.define('filter.commentAfter', '\n', 'A definition of comment that should be placed after matched ' + 'element when 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 tag
  • ' + '
  • padding – current string padding, can be used ' + 'for formatting
  • ' +'
'); prefs.define('filter.commentBefore', '', 'A definition of comment that should be placed before matched ' + 'element when comment 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(//, '>' + 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'; /** @type preferences */ var prefs = require('preferences'); prefs.define('format.noIndentTags', 'html', 'A comma-separated list of tag names that should not get inner indentation.'); prefs.define('format.forceIndentationForTags', 'body', 'A comma-separated list of tag names that should always get inner indentation.'); /** * Get indentation for given node * @param {AbbreviationNode} node * @returns {String} */ function getIndentation(node) { if (_.include(prefs.getArray('format.noIndentTags') || [], node.name())) { return ''; } 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(); var indent = getIndentation(item); // formatting output if (profile.tag_nl !== false) { var forceNl = profile.tag_nl === true && (profile.tag_nl_leaf || item.children.length); if (!forceNl) { forceNl = _.include(prefs.getArray('format.forceIndentationForTags') || [], item.name()); } // 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 + indent; } 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 = indent; } } 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 = ''; } } 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); // should we put caret placeholder after opening tag? if ( !item.children.length && !isUnary && !~item.content.indexOf(cursor) && !require('tabStops').extract(item.content).tabstops.length ) { 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 *

* Examples to test:
* 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 line
* span*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.define('lorem', function(require, _) { var langs = { en: { common: ['lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipisicing', 'elit'], words: ['exercitationem', 'perferendis', 'perspiciatis', 'laborum', 'eveniet', 'sunt', 'iure', 'nam', 'nobis', 'eum', 'cum', 'officiis', 'excepturi', 'odio', 'consectetur', 'quasi', 'aut', 'quisquam', 'vel', 'eligendi', 'itaque', 'non', 'odit', 'tempore', 'quaerat', 'dignissimos', 'facilis', 'neque', 'nihil', 'expedita', 'vitae', 'vero', 'ipsum', 'nisi', 'animi', 'cumque', 'pariatur', 'velit', 'modi', 'natus', 'iusto', 'eaque', 'sequi', 'illo', 'sed', 'ex', 'et', 'voluptatibus', 'tempora', 'veritatis', 'ratione', 'assumenda', 'incidunt', 'nostrum', 'placeat', 'aliquid', 'fuga', 'provident', 'praesentium', 'rem', 'necessitatibus', 'suscipit', 'adipisci', 'quidem', 'possimus', 'voluptas', 'debitis', 'sint', 'accusantium', 'unde', 'sapiente', 'voluptate', 'qui', 'aspernatur', 'laudantium', 'soluta', 'amet', 'quo', 'aliquam', 'saepe', 'culpa', 'libero', 'ipsa', 'dicta', 'reiciendis', 'nesciunt', 'doloribus', 'autem', 'impedit', 'minima', 'maiores', 'repudiandae', 'ipsam', 'obcaecati', 'ullam', 'enim', 'totam', 'delectus', 'ducimus', 'quis', 'voluptates', 'dolores', 'molestiae', 'harum', 'dolorem', 'quia', 'voluptatem', 'molestias', 'magni', 'distinctio', 'omnis', 'illum', 'dolorum', 'voluptatum', 'ea', 'quas', 'quam', 'corporis', 'quae', 'blanditiis', 'atque', 'deserunt', 'laboriosam', 'earum', 'consequuntur', 'hic', 'cupiditate', 'quibusdam', 'accusamus', 'ut', 'rerum', 'error', 'minus', 'eius', 'ab', 'ad', 'nemo', 'fugit', 'officia', 'at', 'in', 'id', 'quos', 'reprehenderit', 'numquam', 'iste', 'fugiat', 'sit', 'inventore', 'beatae', 'repellendus', 'magnam', 'recusandae', 'quod', 'explicabo', 'doloremque', 'aperiam', 'consequatur', 'asperiores', 'commodi', 'optio', 'dolor', 'labore', 'temporibus', 'repellat', 'veniam', 'architecto', 'est', 'esse', 'mollitia', 'nulla', 'a', 'similique', 'eos', 'alias', 'dolore', 'tenetur', 'deleniti', 'porro', 'facere', 'maxime', 'corrupti'] }, ru: { common: ['далеко-далеко', 'за', 'словесными', 'горами', 'в стране', 'гласных', 'и согласных', 'живут', 'рыбные', 'тексты'], words: ['вдали', 'от всех', 'они', 'буквенных', 'домах', 'на берегу', 'семантика', 'большого', 'языкового', 'океана', 'маленький', 'ручеек', 'даль', 'журчит', 'по всей', 'обеспечивает', 'ее','всеми', 'необходимыми', 'правилами', 'эта', 'парадигматическая', 'страна', 'которой', 'жаренные', 'предложения', 'залетают', 'прямо', 'рот', 'даже', 'всемогущая', 'пунктуация', 'не', 'имеет', 'власти', 'над', 'рыбными', 'текстами', 'ведущими', 'безорфографичный', 'образ', 'жизни', 'однажды', 'одна', 'маленькая', 'строчка','рыбного', 'текста', 'имени', 'lorem', 'ipsum', 'решила', 'выйти', 'большой', 'мир', 'грамматики', 'великий', 'оксмокс', 'предупреждал', 'о', 'злых', 'запятых', 'диких', 'знаках', 'вопроса', 'коварных', 'точках', 'запятой', 'но', 'текст', 'дал', 'сбить', 'себя', 'толку', 'он', 'собрал', 'семь', 'своих', 'заглавных', 'букв', 'подпоясал', 'инициал', 'за', 'пояс', 'пустился', 'дорогу', 'взобравшись', 'первую', 'вершину', 'курсивных', 'гор', 'бросил', 'последний', 'взгляд', 'назад', 'силуэт', 'своего', 'родного', 'города', 'буквоград', 'заголовок', 'деревни', 'алфавит', 'подзаголовок', 'своего', 'переулка', 'грустный', 'реторический', 'вопрос', 'скатился', 'его', 'щеке', 'продолжил', 'свой', 'путь', 'дороге', 'встретил', 'рукопись', 'она', 'предупредила', 'моей', 'все', 'переписывается', 'несколько', 'раз', 'единственное', 'что', 'меня', 'осталось', 'это', 'приставка', 'возвращайся', 'ты', 'лучше', 'свою', 'безопасную', 'страну', 'послушавшись', 'рукописи', 'наш', 'продолжил', 'свой', 'путь', 'вскоре', 'ему', 'повстречался', 'коварный', 'составитель', 'рекламных', 'текстов', 'напоивший', 'языком', 'речью', 'заманивший', 'свое', 'агенство', 'которое', 'использовало', 'снова', 'снова', 'своих', 'проектах', 'если', 'переписали', 'то', 'живет', 'там', 'до', 'сих', 'пор'] } }; var prefs = require('preferences'); prefs.define('lorem.defaultLang', 'en'); /** * @param {AbbreviationNode} tree * @param {Object} options */ require('abbreviationParser').addPreprocessor(function(tree, options) { var re = /^(?:lorem|lipsum)([a-z]{2})?(\d*)$/i, match; /** @param {AbbreviationNode} node */ tree.findAll(function(node) { if (node._name && (match = node._name.match(re))) { var wordCound = match[2] || 30; var lang = match[1] || prefs.get('lorem.defaultLang') || 'en'; // 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(lang, wordCound, !i); }); } }); }); /** * 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(_.range(totalCommas), function(ix) { if (ix < words.length - 1) { 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(lang, wordCount, startWithCommon) { var data = langs[lang]; if (!data) { return ''; } var result = []; var totalWords = 0; var words; wordCount = parseInt(wordCount, 10); if (startWithCommon && data.common) { words = data.common.slice(0, wordCount); if (words.length > 5) words[4] += ','; totalWords += words.length; result.push(sentence(words, '.')); } while (totalWords < wordCount) { words = sample(data.words, Math.min(randint(3, 12) * randint(1, 5), wordCount - totalWords)); totalWords += words.length; insertCommas(words); result.push(sentence(words)); } return result.join(' '); } return { /** * Adds new language words for Lorem Ipsum generator * @param {String} lang Two-letter lang definition * @param {Object} data Words for language. Maight be either a space-separated * list of words (String), Array of words or object with text and * common properties */ addLang: function(lang, data) { if (_.isString(data)) { data = {words: _.compact(data.split(' '))}; } else if (_.isArray(data)) { data = {words: data}; } langs[lang] = data; } } });/** * A back-end bootstrap module with commonly used methods for loading user data * @param {Function} require * @param {Underscore} _ */ emmet.define('bootstrap', function(require, _) { /** * Returns file name part from path * @param {String} path Path to file * @return {String} */ function getFileName(path) { var re = /([\w\.\-]+)$/i; var m = re.exec(path); return m ? m[1] : ''; } /** * Returns base path (path to folder of file) * @param {String} path Path to file * @return {String} */ function getBasePath(path) { return path.substring(0, path.length - getFileName(path).length); } /** * Normalizes profile definition: converts some * properties to valid data types * @param {Object} profile * @return {Object} */ function normalizeProfile(profile) { if (_.isObject(profile)) { if ('indent' in profile) { profile.indent = !!profile.indent; } if ('self_closing_tag' in profile) { if (_.isNumber(profile.self_closing_tag)) { profile.self_closing_tag = !!profile.self_closing_tag; } } } return profile; } return { /** * Loads Emmet extensions. Extensions are simple .js files that * uses Emmet modules and resources to create new actions, modify * existing ones etc. * @param {Array} fileList List of absolute paths to files in extensions * folder. Back-end app should not filter this list (e.g. by extension) * but return it "as-is" so bootstrap can decide how to load contents * of each file. * This method requires a file module of IEmmetFile * interface to be implemented. * @memberOf bootstrap */ loadExtensions: function(fileList) { var file = require('file'); var payload = {}; var utils = require('utils'); var userSnippets = null; var that = this; var reader = _.bind(file.readText || file.read, file); var next = function() { if (fileList.length) { var f = fileList.shift(); reader(f, function(err, content) { if (err) { emmet.log('Unable to read "' + f + '" file: '+ err); return next(); } switch (file.getExt(f)) { case 'js': try { eval(content); } catch (e) { emmet.log('Unable to eval "' + f + '" file: '+ e); } break; case 'json': var fileName = getFileName(f).toLowerCase().replace(/\.json$/, ''); if (/^snippets/.test(fileName)) { if (fileName === 'snippets') { // data in snippets.json is more important to user userSnippets = that.parseJSON(content); } else { payload.snippets = utils.deepMerge(payload.snippets || {}, that.parseJSON(content)); } } else { payload[fileName] = content; } break; } next(); }); } else { // complete if (userSnippets) { payload.snippets = utils.deepMerge(payload.snippets || {}, userSnippets); } that.loadUserData(payload); } }; next(); }, /** * Loads preferences from JSON object (or string representation of JSON) * @param {Object} data * @returns */ loadPreferences: function(data) { require('preferences').load(this.parseJSON(data)); }, /** * Loads user snippets and abbreviations. It doesn’t replace current * user resource vocabulary but merges it with passed one. If you need * to replaces user snippets you should call * resetSnippets() method first */ loadSnippets: function(data) { data = this.parseJSON(data); var res = require('resources'); var userData = res.getVocabulary('user') || {}; res.setVocabulary(require('utils').deepMerge(userData, data), 'user'); }, /** * Helper function that loads default snippets, defined in project’s * snippets.json * @param {Object} data */ loadSystemSnippets: function(data) { require('resources').setVocabulary(this.parseJSON(data), 'system'); }, /** * Removes all user-defined snippets */ resetSnippets: function() { require('resources').setVocabulary({}, 'user'); }, /** * Helper function that loads all user data (snippets and preferences) * defined as a single JSON object. This is useful for loading data * stored in a common storage, for example NSUserDefaults * @param {Object} data */ loadUserData: function(data) { data = this.parseJSON(data); if (data.snippets) { this.loadSnippets(data.snippets); } if (data.preferences) { this.loadPreferences(data.preferences); } if (data.profiles) { this.loadProfiles(data.profiles); } var profiles = data.syntaxProfiles || data.syntaxprofiles; if (profiles) { this.loadSyntaxProfiles(profiles); } }, /** * Resets all user-defined data: preferences, snippets etc. * @returns */ resetUserData: function() { this.resetSnippets(); require('preferences').reset(); require('profile').reset(); }, /** * Load syntax-specific output profiles. These are essentially * an extension to syntax snippets * @param {Object} profiles Dictionary of profiles */ loadSyntaxProfiles: function(profiles) { profiles = this.parseJSON(profiles); var snippets = {}; _.each(profiles, function(options, syntax) { if (!(syntax in snippets)) { snippets[syntax] = {}; } snippets[syntax].profile = normalizeProfile(options); }); this.loadSnippets(snippets); }, /** * Load named profiles * @param {Object} profiles */ loadProfiles: function(profiles) { var profile = require('profile'); _.each(this.parseJSON(profiles), function(options, name) { profile.create(name, normalizeProfile(options)); }); }, /** * Dead simple string-to-JSON parser * @param {String} str * @returns {Object} */ parseJSON: function(str) { if (_.isObject(str)) return str; try { return (new Function('return ' + str))(); } catch(e) { return {}; } } }; });